# Jouskaio.me

The value of an idea lies in the using of it

- Site: https://blog.jouskaio.me/

## Auditing and Consolidating a Self-Hosted Media Stack: Hardlinks, Storage Cleanup, and Safe Migration

- Link: https://blog.jouskaio.me/auditing-and-consolidating-a-self-hosted-media-stack-hardlinks-storage-cleanup-and-safe-migration/
- Published: 2026-09-22T19:40:39+02:00
- Author: Manon Salsou

# Auditing and Consolidating a Self-Hosted Media Stack

After running a home media server in production for several weeks, storage fragmentation
and orphaned files inevitably accumulate. This article documents a comprehensive
September 2026 audit to reclaim space, validate architecture integrity, and establish
a repeatable maintenance process — with emphasis on hardlink preservation and safe
cleanup strategies.

## The Initial Problem: Fragmented Mount Paths

The stack initially worked, but the storage layout was not path-clean. Instead of
a single unified mount, media was spread across three separate NFS mount points:

 * `/mnt/movies` — Radarr import destination
 * `/mnt/tv` — Sonarr import destination
 * `/mnt/downloads` — qBittorrent download source

This fragmentation prevented hardlinking between the download directory and the 
library directories. When Radarr imported a film from `/mnt/downloads` to `/mnt/
movies`, it had to use `cp()` instead of `link()`, doubling disk usage for every
single import. Over time, the impact was massive: a 60 GB film imported to Radarr
would consume 120 GB total (60 GB in downloads, 60 GB in movies).

## The Solution: Unified /data Mount Architecture

The refactored architecture consolidates all media under a single NFS mount exposed
consistently to all containers:

### On the Host

    ```language-shell
    mount -t nfs 192.168.1.19:/volume1/Media /mnt/media
    ```

### In Docker Compose

Every download and media-management service uses the same bind mount:

    ```language-yaml
    services:
      qbittorrent:
        volumes:
          - /mnt/media:/data
      radarr:
        volumes:
          - /mnt/media:/data
      sonarr:
        volumes:
          - /mnt/media:/data
    ```

### Inside the Containers

All services see a single unified layout:

    ```language-text
    /data/downloads  → qBittorrent save path
    /data/movies     → Radarr root folder
    /data/series     → Sonarr root folder
    ```

### Why This Matters

Since all three directories are on the same NFS filesystem (same inode table), hardlinks
work seamlessly across services. When qBittorrent downloads a 60 GB film to `/data/
downloads` and Radarr imports it to `/data/movies`, Radarr uses `link()` instead
of `cp()`. The result: one physical copy, two logical paths, zero additional disk
usage.

## Audit Results: September 2026

  |  Component |  Initial State |  After Audit |  Outcome |  
   |  **Radarr orphaned films** |  ~176 leftover downloads |  ~115 properly imported |  ~150 GiB reclaimed |  
 |  **Sonarr episode orphans** |  Unknown |  0 true orphans found |  Series library confirmed clean |  
 |  **qBittorrent Sonarr torrents** |  96 total entries |  59 active torrents |  37 inactive torrents removed |  
 |  **Empty directories** |  11 orphaned |  0 remaining |  Filesystem cleaned |  
 |  **Hardlink verification** |  Unconfirmed |  5 active hardlinks verified |  Hardlink architecture validated |

## Audit Methodology

### 1. Radarr Orphan Detection

Cross-reference Radarr’s database of imported films with the actual files in `/mnt/
media/downloads`. Mark a file as a safe orphan candidate if:

 * It is NOT referenced by any active Radarr entry
 * It has `links=1` (no hardlink protection)
 * It is not associated with any active qBittorrent torrent

**Result:** ~61 films safely deleted, freeing approximately 150 GiB.

### 2. Sonarr Episode Audit

Similarly audit Sonarr series, checking for episodes that:

 * Do not belong to any active Sonarr series entry
 * Are not associated with active qBittorrent torrents
 * Have `links=1` (not hardlink-protected)

**Result:** 0 orphans found. The Sonarr library was completely clean — all existing
episodes were accounted for and properly imported.

### 3. qBittorrent Inactive Torrent Cleanup

Identify torrents that are safe to remove from qBittorrent without reclaiming significant
disk space. Filter by:

 * `category=sonarr` (only Sonarr-managed entries)
 * `progress=0.0` (never started downloading)
 * State in `{queuedDL, metaDL}` (stuck in queue, not downloading)

These torrents were added by Sonarr but never actually began their download. Removing
them did not reclaim significant space, but it reduced database clutter and made
qBittorrent easier to reason about.

**Result:** 37 inactive torrents removed. qBittorrent database reduced from 96 to
59 entries.

### 4. Hardlink Verification

For any imported content, verify that hardlinks are actually in place and protecting
the data:

    ```language-shell
    find /mnt/media/series -type f -exec stat -c '%h %n' {} \; | awk '$1 > 1'
    ```

This command finds all files in the series directory with a link count greater than
1.

**Result:** 5 files with `links=2` confirmed in `/mnt/media/series/Chernobyl/Season
01`. These represent hardlinked copies between the download directory and the series
import directory. One physical file, two logical paths.

## Key Lessons Learned

### Container Path Mapping Is Critical

When auditing, never mix container-internal paths with host paths without first 
validating the mapping. In this case, qBittorrent reported content under `/data/
downloads`, but the host filesystem showed `/mnt/media/downloads`. A naive audit
script could incorrectly label active torrent directories as orphans if it did not
account for this mapping.

Always validate the bind mount before trusting any cleanup decision:

    ```language-shell
    docker inspect qbittorrent --format '{{range .Mounts}}{{println .Source " -> " .Destination}}{{end}}'
    ```

In this case, the critical mapping was: `/mnt/media` (host) → `/data` (container).

### Torrent State vs. Physical Content

A torrent in `queuedUP` state with `progress=1.0` is actively seeding completed 
content that is already hardlinked into your library. Do NOT delete the torrent 
simply because it is not actively downloading. Removing it stops peer contribution
but reclaims zero disk space (the hardlinked files remain in the library). The content
is protected by being referenced from both the downloads directory and the library
directory.

### Hardlinks Change Storage Math

Seeing the same episode in both a download directory and a library directory does
not automatically mean the file is duplicated. Check the link count first. If `links
> 1`, the file is hardlinked and deleting either copy while the other exists will
reduce the link count but not reclaim disk space until the last link is removed.

### Safe Cleanup Requires Conservative Assumptions

When in doubt, keep the file. The cost of keeping extra data is measured in storage;
the cost of deleting something irreplaceable is immeasurable. Always cross-reference
with at least two sources (Radarr/Sonarr database, qBittorrent content mapping, 
filesystem link count) before marking a file for deletion.

## Recommended Maintenance Schedule

 * **Monthly:** Review Radarr leftovers in `/mnt/media/downloads`. Films accumulate
   quickly as new releases are added.
 * **Quarterly:** Verify Sonarr hardlinks and active torrent mappings. Ensure the
   Docker network and storage paths are still consistent.
 * **After any container recreation:** Validate that the `/mnt/media:/data` bind
   mount is still in place and that all services still see a unified filesystem.
 * **Before any major storage operation:** Audit empty directories, queued torrents,
   and save-path consistency to avoid unexpected space issues.

## Security Considerations

If you expose the stack to the internet, keep the public surface minimal. Only Jellyfin
and Jellyseerr should be externally accessible. qBittorrent, Radarr, Sonarr, and
Prowlarr must stay behind local-only access controls, a VPN, or strict reverse-proxy
middleware. Exposing these management interfaces publicly creates significant security
risks.

## Why This Audit Mattered

Running a media stack on a fixed-size storage device requires discipline. Without
a repeatable audit process, storage gradually fragments, old downloads accumulate
invisibly, and cleanup becomes a risky guessing game. By establishing clear criteria
for what is safe to delete, validating hardlink protection, and verifying Docker
path mappings, the audit process became trustworthy enough to automate.

The consolidated `/data` layout is not just cleaner — it makes audits dramatically
simpler. All media lives under one filesystem, all services see consistent paths,
and hardlink validation is straightforward. Future audits will be faster and safer.

---

_Stack: Jellyfin, Jellyseerr, Radarr, Sonarr, Prowlarr, qBittorrent, Bazarr. Infrastructure:
Proxmox LXC, Synology NAS over NFS, Docker Compose. Audit conducted September 2026._

## Setting Up SSH and Locale Configuration on Proxmox

- Link: https://blog.jouskaio.me/setting-up-ssh-and-locale-configuration-on-proxmox/
- Published: 2026-09-07T21:35:25+02:00
- Author: Manon Salsou

In this article, we will guide you through the process of setting up SSH access 
on a Proxmox server, including two methods for authentication: using SSH keys and
using a password. Additionally, we will cover how to configure the system locale
to ensure proper character encoding and language settings.

# Part 1: Setting Up SSH Access

## Method 1: SSH Key Authentication

Using SSH keys is a more secure method for authenticating to your server. Here’s
how to set it up:

### 1. Generate SSH Key Pair on Client Machine

Open a terminal on your local machine and run the following command:

    ```EnlighterJSRAW
    ssh-keygen -t rsa -b 2048
    ```

This will generate a public and private key pair. By default, the keys will be stored
in ⁠`~/.ssh/id_rsa` and ⁠`~/.ssh/id_rsa.pub`.

### 2. Copy the Public Key to the Proxmox Server:

Use the following command to copy your public key to the Proxmox server:

    ```EnlighterJSRAW
    ssh-copy-id username@your_proxmox_ip
    ```

Replace ⁠username with your actual username and ⁠your_proxmox_ip with the server’s
IP address.

### 3. Connect to the Server:

Now you can connect to your Proxmox server without a password:

    ```EnlighterJSRAW
    ssh username@your_proxmox_ip
    ```

### 4. (Optional) Disable Password Authentication:

For enhanced security, you can disable password authentication. Edit the SSH configuration
file:

    ```EnlighterJSRAW
    sudo nano /etc/ssh/sshd_config
    ```

Find the line ⁠PasswordAuthentication yes and change it to:

    ```EnlighterJSRAW
    PasswordAuthentication no
    ```

Restart the SSH service:

    ```EnlighterJSRAW
    sudo systemctl restart sshd
    ```

## Method 2: Password Authentication

If you prefer to use password authentication, follow these steps:

### 1. Edit the SSH Configuration:

Open the SSH configuration file:

    ```EnlighterJSRAW
    sudo nano /etc/ssh/sshd_config
    ```

Ensure the following line is set to ⁠yes:

    ```EnlighterJSRAW
    PasswordAuthentication yes
    ```

### 2. Restart the SSH Service:

Apply the changes by restarting the SSH service:

    ```EnlighterJSRAW
     sudo systemctl restart sshd
    ```

### 3. Connect to the Server:

Use the following command to connect to your Proxmox server:

    ```EnlighterJSRAW
    ssh username@your_proxmox_ip
    ```

You will be prompted to enter your password.

## Part 2 : Configuring Locale Settings

Proper locale settings are crucial for ensuring that your system can handle character
encoding correctly. Here’s how to configure the locale on your Proxmox server:

### 1. Check Current Locale Settings:

Run the following command to see your current locale settings:

    ```EnlighterJSRAW
    locale
    ```

### 2. Install Locales Package:

If you don’t have the locales package installed, you can install it with:

    ```EnlighterJSRAW
    sudo apt-get install locales
    ```

### 3. Generate the Desired Locale:

To properly generate and select your locales (e.g., French or English), run the 
interactive reconfigure tool:

    ```EnlighterJSRAW
    sudo dpkg-reconfigure locales
    ```

Use the arrow keys to navigate, the **Spacebar** to select your preferred locales(
e.g., `fr_FR.UTF-8 UTF-8 `and `en_US.UTF-8 UTF-8`), then press **Tab** to select
_OK_ and hit **Enter**. Finally, select your default system locale on the next screen.

Alternatively, you can generate a specific locale directly:

    ```EnlighterJSRAW
    sudo locale-gen fr_FR.UTF-8
    ```

### 4. Edit Locale Configuration:

Open the locale configuration file:

    ```EnlighterJSRAW
    sudo nano /etc/default/locale
    ```

Ensure it contains the following lines:

    ```EnlighterJSRAW
    LANG=fr_FR.UTF-8
    LANGUAGE=fr_FR:fr
    LC_ALL=fr_FR.UTF-8
    ```

### 5. Update Locale Settings:

Apply the changes by running:

    ```EnlighterJSRAW
    sudo update-locale
    ```

### 6. Verify Locale Configuration:

Check the locale settings again to ensure they are correct:

    ```EnlighterJSRAW
    locale
    ```

### 7. Restart the Server or Session:

For the changes to take effect, either restart your server or log out and log back
in.

## Troubleshooting: Fixing “-bash: warning: setlocale” Errors

When connecting via SSH, you might encounter warnings like this:

    ```EnlighterJSRAW
    -bash: warning: setlocale: LC_CTYPE: cannot change locale (UTF-8): No such file or directory
    -bash: warning: setlocale: LC_COLLATE: cannot change locale (fr_FR.UTF-): No such file or directory
    ```

This happens because your local SSH client automatically forwards its own language
environment variables to the remote Proxmox server, but the server doesn’t have 
those specific configuration files generated or encounters a malformed string.

You can fix this using one of two methods:

### Fix 1: Generate Missing Locales on the Server (Recommended)

Make sure the server supports the locales your client is trying to send. Run:

    ```EnlighterJSRAW
    sudo dpkg-reconfigure locales
    ```

Select the matching locale from the list (e.g., `fr_FR.UTF-8 UTF-8`), validate, 
and restart your SSH session.

### Fix 2: Stop SSH Client from Forwarding Locales

If you prefer not to alter the server settings, you can instruct your local machine’s
SSH client to stop sending your local environment variables to remote hosts.

On your **local machine** (Mac or Linux client), edit the SSH configuration file:

    ```EnlighterJSRAW
    sudo nano /etc/ssh/ssh_config
    ```

Locate the following line and comment it out by adding a `#` at the beginning:

    ```EnlighterJSRAW
    # SendEnv LANG LC_*
    ```

Save the file and reconnect. The warning will disappear.

## **Conclusion**

By following these steps, you can successfully set up SSH access on your Proxmox
server using either SSH keys or password authentication. Additionally, configuring
the locale—and knowing how to troubleshoot `setlocale` warnings—will ensure that
your server handles character encoding and language settings correctly. With these
configurations in place, your Proxmox server will be user-friendly.

---

## Building a Self-Hosted Home Media Server: A Complete Guide

- Link: https://blog.jouskaio.me/building-a-self-hosted-home-media-server-a-complete-guide/
- Published: 2026-09-06T11:27:23+02:00
- Author: Manon Salsou

# **From NAS Storage to a Netflix-Like Experience Using Open-Source Tools**

## Introduction

Streaming services have become the norm for consuming movies and TV shows. Netflix,
Disney+, Amazon Prime — they all offer convenience, but at a cost: monthly subscriptions
that add up, content that disappears overnight, geo-restrictions, and zero control
over what you watch and how you watch it. What if you could build your own streaming
platform, hosted at home, that looks and feels like Netflix, automatically downloads
content on demand, manages subtitles, and streams to any device including your TV—
all for free ?

This is exactly what a self-hosted home media server achieves. It is a personal,
private streaming ecosystem that you fully own and control. This article walks you
through the full architecture, the technologies involved, the step-by-step build
process, **and — based on real-world operation — how to keep it stable and troubleshoot
it when things go wrong.**

## The Goal: What Are We Building ?

The end result is a system that allows you to:

 * Browse and stream your movie and TV show library from any device (TV, phone, 
   tablet, browser)
 * Request new content by simply searching for a title (the system handles the download
   automatically)
 * Access it from anywhere via a secure public domain name (e.g. ⁠jellyfin.yourdomain.
   com)
 * Manage subtitles automatically in multiple languages
 * Monitor server health, download activity, and library stats from a single dashboard
 * Use a mobile app (Moonfin on iOS) that looks and feels like a premium streaming
   app
 * Store everything on a NAS (Network Attached Storage) like a Synology
 * Run everything inside an isolated, easy-to-back-up virtual environment (Proxmox
   LXC container)

The philosophy behind this project is called _the arr stack_ — a collection of open-
source tools that work together seamlessly to create a fully automated media pipeline.
In its current form, the stack relies on a single shared `/data` path inside containers,
backed by one NFS mount on the host, to keep downloads, imports, and hardlinks consistent
across all services.

The Technologies Involved

### Jellyfin — The Media Server

Jellyfin is the heart of the system. It is a free and open-source media server that
organizes your movies, TV shows, and music into a beautiful library with posters,
descriptions, ratings, and trailers — all pulled automatically from online databases
like TMDb.

Jellyfin streams your media to any client: a web browser, a smart TV app, a mobile
app, or a dedicated player. It handles transcoding on-the-fly, meaning it can convert
video formats in real-time so any device can play any file regardless of its original
format.

Key features:

 * Beautiful Netflix-like UI (especially with the JellyFlix CSS theme)
 * Multi-user support with individual watch history and parental controls
 * Hardware-accelerated transcoding (Intel QuickSync, NVENC)
 * Plugin system (Open Subtitles, TMDb, Trakt, etc.)
 * Fully open-source, no telemetry, no subscription

### Jellyseerr — The Request Manager

Jellyseerr is the user-facing request portal. When you want to watch a movie that
is not in your library yet, you go to Jellyseerr, search for it, and click “Request”.
Jellyseerr then automatically notifies the download tools to fetch it.

It provides:

 * A polished Netflix-style discovery interface
 * Integration with TMDb for rich metadata
 * Request tracking (Pending / Downloading / Available)
 * User management — friends and family can request content
 * Notifications (email, Discord, Pushover, etc.)

### Radarr — The Movie Manager

Radarr is an automated movie collection manager. Once Jellyseerr sends it a request,
Radarr:

 1. Searches configured torrent indexers for the best quality match
 2. Sends the torrent to qBittorrent for download
 3. Monitors the download and renames/moves the file to the correct folder
 4. Upgrades quality automatically if a better version is released (e.g. CAM → BluRay)

### Sonarr — The Series Manager

Sonarr does the same as Radarr but for TV series. It monitors entire shows, automatically
downloads new episodes when they air, and keeps your library up to date season by
season.

### Prowlarr — The Indexer Manager

Prowlarr is the centralized indexer manager. Instead of configuring torrent sources
separately in Radarr and Sonarr, you configure them once in Prowlarr and it syncs
to all *arr apps automatically. It supports hundreds of public and private torrent
indexers.

### qBittorrent — The Download Client

qBittorrent is the actual torrent download engine. It receives torrent jobs from
Radarr/Sonarr via its web API, downloads the files, and places them in the correct
directory where Jellyfin can find them. It runs headlessly in Docker with a web 
UI for management.

### Bazarr — The Subtitle Manager

Bazarr works hand-in-hand with Radarr and Sonarr. It scans your entire library, 
detects missing subtitles in the languages you configure, and automatically downloads
them from providers like OpenSubtitles. It also connects to Sonarr and Radarr via**
SignalR** for real-time updates whenever new content is added.

### Homepage — The Dashboard

Homepage is a highly customizable startpage/dashboard that aggregates live statistics
from every service in the stack into a single glanceable view: Jellyfin library 
counts, Radarr/Sonarr wanted/missing counts, qBittorrent transfer speeds, Prowlarr
indexer stats, and more — all in one place, accessible from any browser on your 
network.

### Jellystat — The Analytics Engine

Jellystat connects directly to Jellyfin’s API and a dedicated PostgreSQL database
to provide rich, historical playback statistics: most-watched movies and series,
active sessions, per-user viewing habits, and library growth over time.

### Maintainerr — The Automatic Cleanup Tool

Maintainerr monitors your library for content that hasn’t been watched in a configurable
period and automatically removes it (or flags it for review) to reclaim storage 
space, based on rules you define — extremely useful to prevent your NAS from silently
filling up.

### Fetcharr — The Metadata/Availability Sync Tool

Fetcharr keeps metadata and request statuses synchronized across the stack, reducing
manual bookkeeping between Jellyseerr, Radarr, and Sonarr.

Synology NAS — The Storage Layer

A Synology NAS provides reliable, always-on storage with RAID protection for your
media files. In this architecture, the NAS is mounted as a network share (SMB/NFS)
directly to the container running Docker, meaning all applications read and write
files directly to the NAS without the NAS needing to run any media software itself.

### Proxmox + LXC — The Virtualization Layer

Rather than running Docker directly on bare metal or inside a full VM, this setup
runs the **entire Docker stack inside a single unprivileged LXC container** managed
by Proxmox VE. This gives you:

 * Near-native performance (LXC has virtually no virtualization overhead compared
   to a full VM)
 * Easy snapshots and backups at the container level, independent of Docker
 * Simple resource control (CPU, RAM limits) directly from the Proxmox web UI
 * The ability to run other unrelated services (other CTs/VMs) on the same physical
   host

> ⚠️ **Important lesson learned:** LXC containers can develop a **stale/frozen network
> namespace state** after repeated ⁠docker compose down/up cycles or Docker daemon
> restarts inside the container. If containers can ping each other by IP but not
> by hostname, or if the whole Docker network seems “stuck”, a full ⁠pct stop &&
> pct start of the LXC container (from the Proxmox host, not from inside the container)
> resolves it by giving the container a completely fresh network stack.

Docker & Docker Compose — The Runtime

All services run as Docker containers orchestrated by Docker Compose. This provides:

 * Isolation between services
 * Easy updates (⁠`docker compose pull && docker compose up -d`) Reproducible deployments(
   entire stack defined in one ⁠`docker-compose.yml` file)
 * Shared networking between containers (they talk to each other by service name)

### Traefik — The Reverse Proxy

Traefik sits in front of all services and routes incoming HTTPS traffic to the correct
container based on the subdomain. It automatically handles SSL certificates via 
Let’s Encrypt, so all your services are accessible securely over HTTPS from the 
internet.

For example:

 * ⁠`jellyfin.yourdomain.com `→ Jellyfin container
 * `jellyseerr.yourdomain.com` → Jellyseerr container ⁠
 * `radarr.yourdomain.com` → Radarr container

### Moonfin — The Mobile Client

Moonfin is a premium iOS app that connects to your Jellyfin server and Jellyseerr
simultaneously, providing a unified Netflix-like mobile experience. You can browse
your library, stream directly, and request new content — all from one app.

## Architecture & Structure

### High-Level Overview

    ```EnlighterJSRAW
    ┌─────────────────────────────────────────────────────────────┐
    │                          INTERNET                           │
    └────────────────────────┬────────────────────────────────────┘
                              │ HTTPS (443)
                              ▼
    ┌─────────────────────────────────────────────────────────────┐
    │                    YOUR DOMAIN DNS                          │
    │   jellyfin.you.me / jellyseerr.you.me / radarr.you.me       │
    └────────────────────────┬────────────────────────────────────┘
                              │
                              ▼
    ┌─────────────────────────────────────────────────────────────┐
    │                  HOME ROUTER / BOX                          │
    │         Port forwarding 80 + 443 → LXC IP                   │
    └────────────────────────┬────────────────────────────────────┘
                              │
                              ▼
    ┌─────────────────────────────────────────────────────────────┐
    │                  PROXMOX HOST                               │
    │  ┌──────────────────────────────────────────────────────┐   │
    │  │        LXC Container "media-server" (Docker host)    │   │
    │  │  ┌──────────────────────────────────────────────────┐│   │
    │  │  │  Docker Network (bridge, static IPs)             ││   │
    │  │  │                                                  ││   │
    │  │  │  ┌──────────┐  routes to  ┌────────────────┐     ││   │
    │  │  │  │ Traefik  │ ───────────►│ Jellyfin       │     ││   │
    │  │  │  │ :80/443  │             │ Jellyseerr     │     ││   │
    │  │  │  │ (proxy)  │ ───────────►│ Radarr/Sonarr  │     ││   │
    │  │  │  │          │             │ Prowlarr       │     ││   │
    │  │  │  │          │ ───────────►│ qBittorrent    │     ││   │
    │  │  │  │          │             │ Bazarr         │     ││   │
    │  │  │  │          │ ───────────►│ Homepage       │     ││   │
    │  │  │  │          │             │ Jellystat + DB │     ││   │
    │  │  │  │          │ ───────────►│ Maintainerr    │     ││   │
    │  │  │  └──────────┘             └────────────────┘     ││   │
    │  │  └──────────────────────────────────────────────────┘│   │
    │  │                                                      │   │
    │  │        NFS mount from Synology NAS                    │   │
    │  │        /mnt/media (single unified mount)              │   │
    │  └────────────────────────┬─────────────────────────────┘   │
    └───────────────────────────┼─────────────────────────────────┘
                                │ NFS
                                ▼
    ┌─────────────────────────────────────────────────────────────┐
    │                      SYNOLOGY NAS                           │
    │  /volume1/Media (single share)                             │
    │    ├── downloads  ← qBittorrent writes here                 │
    │    ├── movies     ← Radarr imports here                     │
    │    └── series     ← Sonarr imports here                     │
    └─────────────────────────────────────────────────────────────┘
    ```

### Data Flow: Requesting a New Movie

    ```EnlighterJSRAW
    User (Moonfin / Jellyseerr web)
       │  1. Search + Request "The Crow"
       ▼
    Jellyseerr
       │  2. Sends request to Radarr API
       ▼
    Radarr
       │  3. Queries Prowlarr for torrent sources
       ▼
    Prowlarr
       │  4. Returns torrent results from indexers
       ▼
    Radarr
       │  5. Picks best quality match, sends to qBittorrent
       ▼
    qBittorrent
       │  6. Downloads torrent to /data/downloads
       │  7. Radarr detects completion, hardlinks file to /data/movies/...
       ▼
    Jellyfin
       │  8. Library scan detects new file, fetches metadata
       │     Bazarr grabs subtitles automatically
       ▼
    User's TV / Phone — Film appears in library, ready to watch
    ```

### Network Communication Map

    ```EnlighterJSRAW
    jellyseerr    ──────────────────────► radarr:7878
    jellyseerr    ──────────────────────► sonarr:8989
    jellyseerr    ──────────────────────► jellyfin:8096

    radarr        ──────────────────────► prowlarr:9696
    sonarr        ──────────────────────► prowlarr:9696
    radarr        ──────────────────────► qbittorrent:8080
    sonarr        ──────────────────────► qbittorrent:8080

    bazarr        ──────────────────────► sonarr:8989 (SignalR)
    bazarr        ──────────────────────► radarr:7878 (SignalR)

    homepage      ──────────────────────► ALL services (read-only stats)
    jellystat     ──────────────────────► jellyfin:8096
    jellystat     ──────────────────────► jellystat-db:5432

    jellyfin plugin (Moonfin) ──────────► jellyseerr:5055

    All services  ──────────────────────► /mnt/media (shared NFS volume)
    ```

## Prerequisites

Before starting, you will need :

| **Requirement** | **Details** | 
| Proxmox VE host | Or a Linux VM/bare-metal server, 4GB+ RAM | 
| LXC container | Ubuntu 22.04 template, unprivileged, 6GB+ RAM recommended | 
| Docker + Docker Compose v2.x | Installed inside the LXC | 
| A Synology NAS (optional) | Or any external storage / local disk | 
| A domain name | Purchased from OVH, Namecheap, Cloudflare, etc. | 
| A public IP | From your ISP (static preferred) or use DDNS | 
| Port forwarding | Open 80 and 443 on your router to the LXC |

## Step-by-Step Setup

### Step 1 — Create the LXC Container on Proxmox

From the Proxmox web UI, create a new unprivileged LXC container:

 * Template: Ubuntu 22.04
 * RAM: **6 GB minimum** (Jellyfin transcoding + all *arr apps + Docker overhead
   adds up fast — 4GB is too tight and can cause OOM kills)
 * CPU cores: 4+
 * Enable **nesting=1** in the container’s options (required for Docker to run inside
   LXC)
   `pct set <CTID> -features nesting=1`

### Step 2 – Prepare the Container

    ```EnlighterJSRAW
    # Update the system
    sudo apt update && sudo apt upgrade -y

    # Install Docker
    curl -fsSL https://get.docker.com | sh
    sudo usermod -aG docker $USER
    newgrp docker

    # Install Docker Compose plugin
    sudo apt install docker-compose-plugin -y
    ```

### Step 3 — Mount the Synology NAS

If you use a Synology NAS as storage, mount it via NFS:

    ```EnlighterJSRAW
    # Install NFS client
    sudo apt install nfs-common -y

    # Create mount point (single unified mount)
    sudo mkdir -p /mnt/media

    # Mount entire media share (replace NAS_IP with your Synology IP)
    sudo mount -t nfs NAS_IP:/volume1/Media /mnt/media
    ```

To make mounts persistent across reboots, add to ⁠/etc/fstab:

    ```EnlighterJSRAW
    NAS_IP:/volume1/Media   /mnt/media   nfs   defaults,_netdev   0   0
    ```

### Step 4 — DNS Configuration

In your domain registrar’s DNS panel, create the following **A records** pointing
to your public IP:

| **Subdomain** | **Type** | **Value** | 
| ⁠jellyfin | A | ⁠YOUR_PUBLIC_IP | 
| ⁠jellyseerr | A | ⁠YOUR_PUBLIC_IP | 
| ⁠radarr | A | ⁠YOUR_PUBLIC_IP | 
| ⁠sonarr | A | ⁠YOUR_PUBLIC_IP | 
| ⁠prowlarr | A | ⁠YOUR_PUBLIC_IP | 
| ⁠qbit | A | ⁠YOUR_PUBLIC_IP | 
| ⁠traefik | A | ⁠YOUR_PUBLIC_IP | 
| homepage | A | ⁠YOUR_PUBLIC_IP |

### Step 4 — Create the Docker Compose Stack (with static IPs)

Key improvement over earlier versions of this stack: assign a static IP to every
container on the Docker bridge network. This prevents a subtle but very real class
of bugs where a service (particularly dashboards like Homepage) caches a container’s
resolved IP internally and keeps hammering a stale address after that container 
gets recreated with a new IP — resulting in confusing ⁠`ETIMEDOUT` / ⁠`ECONNREFUSED`
errors that look like network failures but aren’t.

    ```EnlighterJSRAW
    x-logging: &logging
      driver: json-file
      options:
        max-size: "10m"
        max-file: "3"

    services:
      traefik:
        image: traefik:v3.0
        container_name: traefik
        restart: unless-stopped
        networks:
          medianet:
            ipv4_address: 172.18.0.2
        ports:
          - "80:80"
          - "443:443"
        volumes:
          - /var/run/docker.sock:/var/run/docker.sock:ro
          - ./config/traefik/traefik.yml:/etc/traefik/traefik.yml:ro
          - ./config/traefik/acme.json:/acme.json
        labels:
          - "traefik.enable=true"
          - "traefik.http.routers.traefik.rule=Host(traefik.yourdomain.com)"
          - "traefik.http.routers.traefik.tls.certresolver=letsencrypt"
          - "traefik.http.routers.traefik.service=api@internal"

      jellyfin:
        image: lscr.io/linuxserver/jellyfin:latest
        container_name: jellyfin
        environment:
          - PUID=1024
          - PGID=100
          - TZ=Europe/Paris
        volumes:
          - ./config/jellyfin:/config
          - /mnt/media:/data:ro
        networks:
          medianet:
            ipv4_address: 172.18.0.15
        restart: unless-stopped
        logging: *logging
        mem_limit: 2g
        memswap_limit: 2g
        labels:
          - "traefik.enable=true"
          - "traefik.http.routers.jellyfin.rule=Host(jellyfin.yourdomain.com)"
          - "traefik.http.routers.jellyfin.tls.certresolver=letsencrypt"
          - "traefik.http.services.jellyfin.loadbalancer.server.port=8096"

      jellyseerr:
        image: fallenbagel/jellyseerr:latest
        container_name: jellyseerr
        volumes:
          - ./config/jellyseerr:/app/config
        networks:
          medianet:
            ipv4_address: 172.18.0.16
        restart: unless-stopped
        logging: *logging
        mem_limit: 384m
        labels:
          - "traefik.enable=true"
          - "traefik.http.routers.jellyseerr.rule=Host(jellyseerr.yourdomain.com)"
          - "traefik.http.routers.jellyseerr.tls.certresolver=letsencrypt"
          - "traefik.http.services.jellyseerr.loadbalancer.server.port=5055"

      radarr:
        image: lscr.io/linuxserver/radarr:latest
        container_name: radarr
        environment:
          - PUID=1024
          - PGID=100
          - TZ=Europe/Paris
        volumes:
          - ./config/radarr:/config
          - /mnt/media:/data
        networks:
          medianet:
            ipv4_address: 172.18.0.11
        restart: unless-stopped
        logging: *logging
        mem_limit: 512m
        labels:
          - "traefik.enable=true"
          - "traefik.http.routers.radarr.rule=Host(radarr.yourdomain.com)"
          - "traefik.http.routers.radarr.tls.certresolver=letsencrypt"
          - "traefik.http.services.radarr.loadbalancer.server.port=7878"

      sonarr:
        image: lscr.io/linuxserver/sonarr:latest
        container_name: sonarr
        environment:
          - PUID=1024
          - PGID=100
          - TZ=Europe/Paris
        volumes:
          - ./config/sonarr:/config
          - /mnt/media:/data
        networks:
          medianet:
            ipv4_address: 172.18.0.12
        restart: unless-stopped
        logging: *logging
        mem_limit: 512m
        labels:
          - "traefik.enable=true"
          - "traefik.http.routers.sonarr.rule=Host(sonarr.yourdomain.com)"
          - "traefik.http.routers.sonarr.tls.certresolver=letsencrypt"
          - "traefik.http.services.sonarr.loadbalancer.server.port=8989"

      prowlarr:
        image: lscr.io/linuxserver/prowlarr:latest
        container_name: prowlarr
        environment:
          - PUID=1024
          - PGID=100
          - TZ=Europe/Paris
        volumes:
          - ./config/prowlarr:/config
        networks:
          medianet:
            ipv4_address: 172.18.0.10
        restart: unless-stopped
        logging: *logging
        mem_limit: 256m
        labels:
          - "traefik.enable=true"
          - "traefik.http.routers.prowlarr.rule=Host(prowlarr.yourdomain.com)"
          - "traefik.http.routers.prowlarr.tls.certresolver=letsencrypt"
          - "traefik.http.services.prowlarr.loadbalancer.server.port=9696"

      qbittorrent:
        image: lscr.io/linuxserver/qbittorrent:latest
        container_name: qbittorrent
        environment:
          - PUID=1024
          - PGID=100
          - TZ=Europe/Paris
          - WEBUI_PORT=8080
        volumes:
          - ./config/qbittorrent:/config
          - /mnt/media:/data
        networks:
          medianet:
            ipv4_address: 172.18.0.13
        restart: unless-stopped
        logging: *logging
        mem_limit: 1g
        labels:
          - "traefik.enable=true"
          - "traefik.http.routers.qbit.rule=Host(qbit.yourdomain.com)"
          - "traefik.http.routers.qbit.tls.certresolver=letsencrypt"
          - "traefik.http.services.qbit.loadbalancer.server.port=8080"

      bazarr:
        image: lscr.io/linuxserver/bazarr:latest
        container_name: bazarr
        environment:
          - PUID=1024
          - PGID=100
          - TZ=Europe/Paris
        volumes:
          - ./config/bazarr:/config
          - /mnt/media:/data
        networks:
          medianet:
            ipv4_address: 172.18.0.14
        restart: unless-stopped
        logging: *logging
        mem_limit: 512m

      jellystat-db:
        image: postgres:15.2
        container_name: jellystat-db
        environment:
          - POSTGRES_DB=jfstat
          - POSTGRES_USER=jellystat
          - POSTGRES_PASSWORD=changeme
        volumes:
          - ./config/jellystat/db:/var/lib/postgresql/data
        networks:
          medianet:
            ipv4_address: 172.18.0.17
        restart: unless-stopped
        logging: *logging
        mem_limit: 256m

      jellystat:
        image: cyfershepard/jellystat:latest
        container_name: jellystat
        environment:
          - POSTGRES_USER=jellystat
          - POSTGRES_PASSWORD=changeme
          - POSTGRES_IP=jellystat-db
          - POSTGRES_PORT=5432
          - JWT_SECRET=use_a_long_random_string_here
          - TZ=Europe/Paris
        depends_on:
          - jellystat-db
        networks:
          medianet:
            ipv4_address: 172.18.0.18
        restart: unless-stopped
        logging: *logging
        mem_limit: 256m

      maintainerr:
        image: ghcr.io/maintainerr/maintainerr:latest
        container_name: maintainerr
        user: "1024:100"
        environment:
          - TZ=Europe/Paris
        volumes:
          - ./config/maintainerr:/opt/data
          - /mnt/media:/data
        networks:
          medianet:
            ipv4_address: 172.18.0.21
        restart: unless-stopped
        logging: *logging
        mem_limit: 256m

      homepage:
        image: ghcr.io/gethomepage/homepage:latest
        container_name: homepage
        environment:
          - TZ=Europe/Paris
          - PUID=1024
          - PGID=100
        volumes:
          - ./config/homepage:/app/config
          - /var/run/docker.sock:/var/run/docker.sock:ro
        networks:
          medianet:
            ipv4_address: 172.18.0.20
        restart: unless-stopped
        logging: *logging
        mem_limit: 192m
        labels:
          - "traefik.enable=true"
          - "traefik.http.routers.homepage.rule=Host(homepage.yourdomain.com)"
          - "traefik.http.routers.homepage.tls.certresolver=letsencrypt"
          - "traefik.http.services.homepage.loadbalancer.server.port=3000"

    networks:
      medianet:
        driver: bridge
        ipam:
          config:
            - subnet: 172.18.0.0/16
              gateway: 172.18.0.1
    ```

### Step 6 — Traefik Configuration

Create ⁠`config/traefik/traefik.yml`:

    ```EnlighterJSRAW
    api:
      dashboard: true

    entryPoints:
      web:
        address: ":80"
        http:
          redirections:
            entryPoint:
              to: websecure
              scheme: https
      websecure:
        address: ":443"

    certificatesResolvers:
      letsencrypt:
        acme:
          email: you@example.com
          storage: /acme.json
          httpChallenge:
            entryPoint: web

    providers:
      docker:
        exposedByDefault: false
        network: medianet
    ```

### Step 7 — Launch the Stack

    ```EnlighterJSRAW
    cd ~/mediaserver
    docker compose up -d
    docker compose ps
    ```

### Step 8 — Initial Configuration

**Jellyfin** (⁠`jellyfin.yourdomain.com`)

 1. Complete the setup wizard
 2. Add movie library → path ⁠`/data/movies` Add TV library → path ⁠`/data/series`
 3. Install plugins: Open Subtitles, TMDb, TMDb Box Set

**Prowlarr** (⁠`prowlarr.yourdomain.com`)

 1. Add indexers (public/private trackers)
 2. Settings → Apps → Add Radarr (URL: ⁠`http://radarr:7878`, API key from Radarr)
 3. Settings → Apps → Add Sonarr (URL: ⁠`http://sonarr:8989`, API key from Sonarr)

**Radarr / Sonarr**

 1. Settings → Download Clients → Add qBittorrent (Host: ⁠`qbittorrent`, Port: ⁠`8080`)
 2. Settings → Root Folders → Add ⁠`/movies` (or ⁠`/tv`) Settings → Quality Profiles
    → configure your preferred quality

**Jellyseerr** (`⁠jellyseerr.yourdomain.com`)

 1. Connect to Jellyfin: URL ⁠`http://jellyfin:8096`
 2. Connect to Radarr and Sonarr with their API keys Configure default quality profiles

**Bazarr**

 1. Settings → Sonarr / Radarr → add connection with API keys
 2. Settings → Languages → choose your subtitle languages Settings → Providers → enable
    OpenSubtitles

**Homepage**

 1. Edit ⁠`config/services.yaml` to add each widget with its URL and API key
 2. Edit ⁠`config/settings.yaml` to set ⁠`HOMEPAGE_ALLOWED_HOSTS` to your access `URL`—
    otherwise Homepage will silently refuse connections from anything but localhost

**Jellystat**

 1. On first launch, create an admin account
 2. Settings → Add Jellyfin server URL + API key Wait for the initial sync to populate
    historical data

### Step 9 — Apply the Netflix Theme to Jellyfin

In Jellyfin → Dashboard → General → Custom CSS Code:

    ```EnlighterJSRAW
    @import url("https://cdn.jsdelivr.net/gh/prayag17/JellyFlix@latest/default.css");
    @import url("https://cdn.jsdelivr.net/gh/prayag17/JellyFlix@latest/addons/Logo.css");
    ```

### Step 10 — Configure Moonfin on iOS

 1. Download **Moonfin** from the App Store
 2. Add server: ⁠https://jellyfin.yourdomain.com
 3. Sign in with your Jellyfin credentials
 4. Settings → Seerr → Enable Seerr → Sign in with Jellyfin account

## Security Considerations

This stack is exposed to the internet, so security matters:

 * Strong passwords for all services
 * Never expose qBittorrent, Radarr, Sonarr, or Prowlarr publicly — use Traefik 
   middlewares with BasicAuth or IP whitelisting
 * Only Jellyfin and Jellyseerr should be publicly accessible
 * Fail2ban can be configured to block brute force attempts
 * Let’s Encrypt SSL is handled automatically by Traefik

To restrict access to the *arr tools and Homepage to your local network only, add
this Traefik middleware:

    ```EnlighterJSRAW
    # In your docker-compose labels for radarr, sonarr, prowlarr, qbittorrent:
    - "traefik.http.routers.radarr.middlewares=local-only"
    - "traefik.http.middlewares.local-only.ipwhitelist.sourcerange=192.168.1.0/24"
    ```

## Maintaining the Stack

### Update all containers

    ```EnlighterJSRAW
    cd ~/mediaserver
    docker compose pull
    docker compose up -d
    ```

### View logs

    ```EnlighterJSRAW
    docker compose logs -f jellyfin
    docker compose logs -f radarr
    ```

### Backup

The only thing you need to back up are the ⁠config/ folders. Your media files live
on the NAS and are safe. A simple cron job can archive configs :

    ```EnlighterJSRAW
    # Daily backup of configs
    0 3 * * * tar -czf /backup/mediaserver-config-$(date +%F).tar.gz ~/mediaserver/config/
    ```

> ⚠️ **Lesson learned:** if you also run a **filesystem-level backup job** (e.g.
> Proxmox’s built-in backup, or a snapshot-based backup) on the LXC container itself,
> schedule it _outside_ peak usage hours and expect it to briefly freeze I/O on 
> the container during the snapshot phase — this can cause transient database corruption
> on services with SQLite databases (Radarr, Sonarr, Prowlarr) if the container 
> crashes or is forcibly stopped mid-backup. Always run ⁠fsck on the LXC’s underlying
> volume after any unclean shutdown.

## Troubleshooting: Real Issues and Fixes

Running this stack for a while inevitably surfaces a handful of recurring issues.
Here are the ones most worth knowing about, since none of them are obvious from 
the official documentation.

**“Homepage shows API Error on some widgets, but the service works fine when I open
it directly”**

**Symptom:** ⁠docker compose logs homepage shows ⁠ETIMEDOUT or ⁠ECONNREFUSED errors
pointing to an IP address that doesn’t match the container’s actual current IP.

**Cause:** Docker assigns dynamic IPs to containers by default. Node.js-based services
like Homepage can internally cache resolved DNS lookups and keep using stale IPs
after a container is recreated with a new one — especially after several rounds 
of ⁠docker compose down/up.

**Fix:** Assign **static IPs** to every service in your ⁠docker-compose.yml (as 
shown in the compose file above). Once an IP never changes, this class of bug disappears
permanently — even if the DNS cache issue persists internally, it will always resolve
to the correct, unchanging address.

**“One or two services intermittently show 503 or ECONNREFUSED, but a manual curl/
wget to them returns 200 OK instantly”**

**Symptom:** Errors appear only occasionally in the logs; manual testing shows the
service is healthy.

**Cause:** Usually a transient resource issue — the container briefly restarts (
silent crash-restart) or is momentarily unavailable (e.g. Jellyfin mid-library-scan
or transcoding under load) at the exact moment Homepage polls it.

**Diagnosis:**

    ```EnlighterJSRAW
    docker inspect <container> --format='{{.State.OOMKilled}} - Restart Count: {{.RestartCount}}'
    ```

If ⁠`OOMKilled`: true or a high restart count shows up, your ⁠mem_limit is too tight
for that service.

**Fix:** Increase the container’s memory limit, particularly for Jellyfin (transcoding
is memory-hungry) and any container handling large libraries.

**“The whole Docker network seems frozen — containers can’t reach each other, even
by IP”**

**Symptom:** All inter-container communication fails simultaneously, ⁠nf_conntrack
counters look fine, ⁠iptables rules look normal, restarting Docker (⁠systemctl restart
docker) inside the container doesn’t help.

**Cause:** The LXC container’s network namespace itself gets into a bad state — 
often after repeated Docker daemon or ⁠docker compose down/up cycles overnight.

**Fix:** Restart the **entire LXC container** from the Proxmox host (not from inside
it):

pct stop <CTID>

pct start <CTID>

This gives the container a genuinely fresh network stack, which a simple Docker 
restart cannot achieve since the underlying namespace issue lives outside of Docker’s
control.

**“A container’s SQLite database is corrupted after an unclean shutdown”**

**Symptom:** Radarr/Sonarr/Prowlarr fails to start, or the web UI throws generic
500 errors, right after a backup job or a forced restart.

**Fix:**

    ```EnlighterJSRAW
    pct enter <CTID>

    fsck -y /dev/mapper/<volume-of-affected-mountpoint>
    ```

If the corruption is inside the app’s own SQLite database rather than the filesystem,
most *arr apps ship a repair/vacuum option accessible from ⁠System → Backup in their
web UI, or you can restore from the last known-good daily config backup.

## Going Further

Once your base stack is running, here are popular extensions:

| **Tool** | **Purpose** | 
| **Bazarr** | Automatic subtitle download for existing library | 
| **Jellystats** | Beautiful statistics dashboard for Jellyfin | 
| **Homepage** | Unified live dashboard for the entire stack | 
| **Maintainerr** | Automatic cleanup of unwatched content to reclaim storage | 
| **Notifiarr** | Centralized notification hub for all *arr apps | 
| **Recyclarr** | Sync quality profiles from the community TRaSH Guides | 
| **Watchtower** | Automatic Docker image updates | 
| **Cloudflare Tunnel** | No port forwarding needed, traffic through Cloudflare |

## Conclusion

What we have built is a fully self-hosted, automated, and private streaming platform.
From a single ⁠docker compose up -d command, you get a Netflix-like media server
that:

 * Organizes and serves your entire movie and TV collection
 * Automatically downloads new content on request in the best available quality
 * Handles subtitles without any manual intervention
 * Is accessible from anywhere in the world via a secure HTTPS domain
 * Gives you a real-time operational dashboard of the entire stack’s health
 * Looks stunning on any device thanks to the JellyFlix theme and Moonfin on iOS
 * Runs on a resilient, snapshot-friendly LXC container rather than a heavier full
   VM

The total cost? A Linux VM (can even run on a spare PC or a €5/month VPS for the
apps), a NAS or external hard drive for storage, and a domain name (~€10/year). 
No monthly subscription. No content disappearing. No geo-restrictions. **Your media,
your rules.**

---

    ```wp-block-preformatted
    Stack versions used in this guide: Jellyfin 10.11, Jellyseerr 2.x, Radarr 5.x, Sonarr 4.x, Prowlarr 1.x, Traefik 3.x, Docker Compose v2. Architecture: Unified /data mount, single NFS share from Synology, hardlink-capable imports.
    ```

**Update (September 2026):** The complete Docker Compose and mount architecture 
have been revised to use a single unified /mnt/media → /data bind mount. For migration
guidance from fragmented paths, see [Auditing and Consolidating a Self-Hosted Media Stack](https://blog.jouskaio.me/auditing-and-consolidating-a-self-hosted-media-stack-hardlinks-storage-cleanup-and-safe-migration/)—
includes real audit results with 150 GiB reclaimed and hardlink preservation validated.

## Homebridge: Bridging the Smart Home Gap in the Age of IoT

- Link: https://blog.jouskaio.me/homebridge-bridging-the-smart-home-gap-in-the-age-of-iot/
- Published: 2026-08-20T21:30:04+02:00
- Author: Manon Salsou

## **Why Homebridge Was Created**

When Apple launched HomeKit in 2014 alongside iOS 8, it introduced a powerful vision:
a secure, unified framework letting iPhones, iPads, and Siri control smart home 
accessories. The problem was adoption. HomeKit required manufacturers to implement
Apple’s proprietary Home Accessory Protocol (HAP) and, in the early years, embed
specific Apple-certified chips into their hardware. Most smart home brands—especially
budget and mid-range ones—simply didn’t bother.

This left a huge gap between what people already owned (Wemo switches, Nest thermostats,
generic Wi-Fi plugs, IP cameras) and what Siri could actually control. Developer**
Nick Farina** stepped in, building on reverse-engineering work done by **Khaos Tian**
in the HAP-NodeJS project, and created **Homebridge**: a lightweight, open-source
Node.js server that emulates the HomeKit Accessory Protocol on your local network.
In effect, Homebridge acts as a translator—it pretends to be a legitimate HomeKit
device to Apple’s ecosystem while communicating with the real device using whatever
API, cloud service, or local protocol the manufacturer actually provides .

The tagline on the project’s own GitHub page sums up the philosophy well: “HomeKit
support for the impatient.” It was built for tinkerers who didn’t want to wait for
official support—or who owned devices that would likely never get it .

## **Core Features**

Homebridge is free, non-commercial, and entirely community-driven—it has no formal
affiliation with Apple, Google, Amazon, or the Connectivity Standards Alliance .

 * **Plugin ecosystem**: Over 2,000 plugins, published on NPM and tagged as ⁠homebridge-
   plugin, provide bridges to virtually every major smart home brand—Ring, Nest,
   TP-Link Kasa, Philips Hue/deCONZ, Belkin Wemo, SwitchBot, UniFi Protect, Roomba,
   Lutron Caseta, Tado, and many more .
 * **Native Siri and Apple ecosystem integration**: Once bridged, accessories appear
   directly in the Apple Home app (or third-party apps like Eve for HomeKit) and
   respond to natural voice commands (“Turn off the lights downstairs,” “Set the
   temperature to 20 degrees”) . **Automations and remote access**: With a home 
   hub (Apple TV, HomePod, or iPad), Homebridge-bridged devices support the same
   automations and secure remote control as native HomeKit accessories.
 * **Homebridge UI**: A web dashboard for installing/configuring plugins, monitoring
   the server, managing accessories, creating isolated “child bridges” per plugin(
   to prevent one buggy plugin from crashing everything), and backing up/restoring
   the whole setup .
 * **Matter support (v2 and beyond)**: This is the big architectural shift. As of
   Homebridge v2, plugins can expose their devices not just via HomeKit but directly
   over the **Matter** protocol, opt-in per bridge, so the same bridged accessory
   becomes visible to Apple Home, Google Home, Amazon Alexa, and Samsung SmartThings
   simultaneously—without needing separate integrations for each .
 * **Broad device-type coverage**: lights, switches, outlets, motion/contact/temperature/
   humidity/leak/smoke/air-quality sensors, locks, thermostats, fans, window coverings,
   robot vacuums, and water valves.

## Interfaces UI – from NPM package

![](https://blog.jouskaio.me/wp-content/uploads/2026/08/homebridge-config-ui-x-accessories.
png)

Accessories page

![](https://blog.jouskaio.me/wp-content/uploads/2026/08/homebridge-config-ui-x-darkmode-
plugins.png)

Plugins page

![](https://blog.jouskaio.me/wp-content/uploads/2026/08/homebridge-config-ui-x-status.
png)

Status page

![](https://blog.jouskaio.me/wp-content/uploads/2026/08/homebridge-config-ui-x-config.
png)

Configuration page

![](https://blog.jouskaio.me/wp-content/uploads/2026/08/homebridge-config-ui-x-logs.
png)

Logs page

**Accessories** **page : **This shows you the Homebridge accessories for all the
Homebridge instances on your network. You can use this to control accessories from
a web browser and works well on mobile devices which allows users to control Homebridge
from non-Apple devices.

**Plugings page : **This shows you the currently installed plugins and allows you
to install, configure, update, and remove plugins.

**Status Screen** : This shows an overview of your Homebridge system. The dashboard
is widget-based and completely customisable with a number of themes available.

**Configuration page** : The configuration screen allows you to modify your Homebridge`
config.json`. The built-in editor automatically syntax-checks your JSON and makes
a backup of your config every time you make a change.

**Log page** **: **This shows you the Homebridge rolling log. This is helpful for
troubleshooting.

## **Why It Matters Even More in the IoT Era**

Homebridge’s relevance hasn’t faded as the smart home market matured—if anything,
it has grown, for a specific reason: **the industry is now consolidating around 
subscriptions and platform lock-in, and Homebridge is one of the few tools that 
pushes back against that.**

The IoT boom of the last decade multiplied the number of connected devices in homes
dramatically, but it also multiplied fragmentation and business-model friction:

 * **Apple’s shift toward subscription-flavored smart home services.** Apple has
   increasingly tied advanced Home features to services layers (HomeKit Secure Video
   storage requires iCloud+, for instance), and its 2026 architecture overhaul retires
   the original 2014-era HomeKit framework entirely (support ends February 10, 2026)
   in favor of a new hub-centric architecture built around Apple TV/HomePod as always-
   on controllers, designed specifically to support full Matter compatibility . 
   This is part of a broader industry trend where hardware companies increasingly
   monetize the _software layer_—cloud storage, AI features, advanced automations—
   as recurring revenue rather than one-time purchases.
 * **Matter’s rise as the “universal language.”** Since Project CHIP launched in
   2019 (backed by Apple, Google, Amazon, and the Zigbee Alliance), Matter has evolved
   rapidly: 1.0 in October 2022, expanding through 1.2 (major appliances), 1.3 and
   1.4 (energy management, EV chargers, solar/heat pumps), 1.5 (cameras, closures)
   in late 2025, and 1.6 in June 2026 (NFC commissioning, Joint Fabric, thermostat
   automation suggestions) . The promise: buy any Matter-certified device and it
   works across ecosystems without cloud relays.
 * **Homebridge sits exactly at the intersection of these two trends.** It lets 
   users keep using cheap, older, or niche devices that never got official Matter
   or HomeKit certification, while now also _exposing_ those same devices to Matter—
   meaning a $10 no-name smart plug bought years ago can, via a Homebridge plugin,
   become visible to Apple Home, Google Home, and Alexa all at once, sidestepping
   both certification costs and vendor subscription paywalls .

In short: as IoT devices multiply and big platforms increasingly try to monetize
the ecosystem layer around them, Homebridge remains a grassroots equalizer—open-
source glue that keeps consumer choice and interoperability alive rather than ceding
it to whichever company controls the hub.

## **Where Homebridge Is Headed**

Homebridge’s own trajectory mirrors the industry’s shift from “single-ecosystem 
bridging” to “protocol-agnostic bridging.” The addition of native Matter output 
in Homebridge v2 is the clearest signal: rather than being purely a HomeKit-compatibility
tool, Homebridge is repositioning itself as a **universal accessory bridge**—one
config, one plugin, output to any Matter-capable controller (Apple, Google, Amazon,
SmartThings, Home Assistant), plus legacy HomeKit for anyone still on older setups.

Several forces will likely shape its future role:

 * **Matter’s maturation reduces some of Homebridge’s original purpose** (new devices
   increasingly ship “Works with Matter” natively), but it doesn’t eliminate it.
   Millions of pre-Matter devices—Wemo plugs, older Hue bulbs, proprietary cameras,
   legacy sensors—will never receive firmware updates, and Homebridge’s 2,000+ community
   plugins remain the only realistic path to bring them into modern ecosystems .
 * **The “long tail” problem persists.** Matter certification has real costs and
   complexity; niche, regional, or budget manufacturers will keep skipping it, just
   as many skipped HomeKit certification a decade ago. Homebridge’s community-plugin
   model is faster and cheaper to extend than any official certification pipeline,
   so it will likely keep being the first place support appears for oddball hardware.**
   A more prominent role as neutral infrastructure.** With Homebridge no longer 
   tied exclusively to Apple’s ecosystem, it’s positioned to become the plugin/translation
   layer of choice for anyone who wants a self-hosted, privacy-respecting, subscription-
   free alternative to platform clouds—appealing to the same audience gravitating
   toward tools like Home Assistant. **Risk of relevance decline for “core” categories.**
   As lighting, locks, and thermostats standardize under Matter 1.x natively, Homebridge’s
   value proposition may consolidate around cameras, appliances, irrigation, and
   other categories where certification lags (Matter cameras only arrived in version
   1.5, late 2025) .

Ultimately, Homebridge’s future isn’t about competing with Matter—it’s about being
the community-run safety net underneath it: the layer that keeps old, cheap, and
orphaned devices useful long after their manufacturers or original ecosystems have
moved on.

## Homebridge contributions

Here, you can see my latest contribution for Homebridge.. a few more will probably
arrived with times.

 TypeScript

 Apache-2.0

 [ ⌊Jouskaio/homebridge-midea-platform⌉ ](https://github.com/Jouskaio/homebridge-midea-platform)

 [ homebridge-midea-platform ](https://github.com/Jouskaio/homebridge-midea-platform)

 By [ Jouskaio ](https://github.com/Jouskaio)

 0

 0

 0

 Homebridge plugin for Midea devices

 Last updated: 16 August 2026

 [ View on GitHub ](https://github.com/Jouskaio/homebridge-midea-platform)

## How to Add Tenda Beli Wi‑Fi Plugs to Homebridge

- Link: https://blog.jouskaio.me/how-to-add-tenda-beli-wi-fi-plugs-to-homebridge/
- Published: 2026-08-20T20:12:54+02:00
- Author: Manon Salsou

If you own Tenda Beli Wi‑Fi smart plugs and want to control them with Homebridge,
this guide explains setup. It covers the local protocol and how to write a working
configuration for a homebridge plug wifi tenda beli.

This method does **not** require a Tuya developer account, a Local Key, or any cloud
linking. It relies entirely on the plug’s **local HTTP API**, discovered through
reverse engineering by the community.

## **Why This Works**

Tenda Beli plugs are white-label devices built on Tuya’s hardware/firmware platform.
They are rebranded under Tenda’s own app, Beli. The homebridge plug wifi tenda beli
does not expose a standard Tuya Local Key. This is unlike generic Tuya devices. 
Likewise, the Beli app offers no way to link accounts. No QR code scanning for account
linking, unlike the Tuya Smart / Smart Life apps.

Fortunately, once the plug is connected to your Wi-Fi network, it runs a small, **
unencrypted local web server on port 5000**. This server accepts simple HTTP requests
to turn the plug on/off and to check its current status — no cloud, no authentication,
no Local Key required.

This was documented by the open-source project [nohous/tenda-beli](https://github.com/nohous/tenda-beli),
which reverse-engineered the plug’s provisioning and control protocol.

## **Glossary of Terms Used in This Guide**

| **Term** | **Meaning** | 
| **Homebridge** | An open-source server that emulates the HomeKit API, letting non-HomeKit-compatible smart devices (like Tenda plugs) appear as HomeKit accessories in Apple’s Home app. | 
| **Plugin** | A Homebridge add-on (a Node.js package) that adds support for a specific type of device or protocol. | 
| **Accessory** | A single smart device exposed to HomeKit (e.g., one smart plug). | 
| **Child bridge** | An isolated process Homebridge can run for a plugin, so that a crash in one plugin doesn’t affect the whole system. | 
| **Local Key** | A secret key normally required to control Tuya devices locally over the LAN. Not needed here, since we bypass Tuya’s protocol entirely. | 
| **DHCP reservation** | A router setting that always assigns the same local IP address to a specific device (identified by its MAC address), preventing the IP from changing after a reboot. | 
| **curl** | A command-line tool used to send HTTP requests, useful here for testing the plug’s local API before configuring Homebridge. | 
| **JSON** | A lightweight text format (JavaScript Object Notation) used to structure data — both in Homebridge’s configuration file and in the plug’s API requests/responses. | 
| **config.json** | Homebridge’s main configuration file, listing all bridges, platforms, and accessories. | 
| **statusUrl / onUrl / offUrl** | Configuration fields used by the ⁠homebridge-http-switch plugin to define which HTTP endpoints control and report the state of the switch. | 
| **statusPattern** | A text pattern used to detect whether the plug is “on” by searching for a specific string in the response returned by ⁠statusUrl. |

## How to achieve it

### **Step 1 — Find Your Plug’s Local IP Address**

First, you need to know the local IP address of your Tenda plug on your home network.

 * Log into your router’s admin panel and look at the list of connected devices.

If you do not have access to you router to see all IP on your LAN, you can still
uses `nmap` or use Fing application.

Look for a device with a hostname like ⁠sp3.home or a name resembling “Tenda.” Note
its **IP address** (e.g., ⁠192.168.1.124) and its **MAC address**.

**Important:** Set up a **DHCP reservation** for this device in your router settings,
so it always keeps the same IP address. Homebridge relies on a fixed IP to communicate
reliably with the plug.

### **Step 2 — Test the Plug’s Local HTTP API**

The Tenda Beli plug listens on **port 5000** and responds to plain, unencrypted 
HTTP requests — no login, no token, no HTTPS.

**Turn the plug ON**

Open a terminal (on Mac, Linux, or on your Homebridge server itself) and run:

    ```EnlighterJSRAW
    curl -X POST "http://192.168.1.94:5000/setSta" \

      -H "Content-Type: application/json" \

      -d '{"status":1}'
    ```

Expected response : `{"resp_code":0,"status":1}`

**Turn the plug OFF**

    ```EnlighterJSRAW
    curl -X POST "http://192.168.1.94:5000/setSta" \

      -H "Content-Type: application/json" \

      -d '{"status":0}'
    ```

Expected response: `{"resp_code":0,"status":0}`

**Check the plug’s current status**

    ```EnlighterJSRAW
    curl -X POST "http://192.168.1.94:5000/getSta"
    ```

Expected response: `{"resp_code":0,"data":{"status":1}}`

_Note : unlike a typical REST API, the status-check endpoint (⁠`/getSta`) uses **
POST**, not GET — this is a quirk of the plug’s firmware._

If these commands work and change your plug’s power state, you’re ready to configure
Homebridge.

### **Step 3 — Install the **⁠homebridge-http-switch** Plugin**

This plugin lets Homebridge control any device through simple HTTP requests, which
is exactly what we need here.

From the Homebridge Config UI X web interface :

 1. Go to the **Plugins** tab.

Search for ⁠homebridge-http-switch. Click **Install**.

Or via the command line: `sudo npm install -g homebridge-http-switch`

### **Step 4 — Configure the Accessory**

This is the most important (and trickiest) part. You must edit Homebridge’s **raw
JSON configuration** directly — **do not** use the plugin’s graphical settings form
in Config UI X, because it may strip out fields it doesn’t recognize (like ⁠statusUrl),
causing the plugin to fail on restart.

In Config UI X, look for a **raw JSON editor** option (usually an icon like ⁠{ }),
not the individual plugin settings panel.

Add this block inside the ⁠”accessories” array of your ⁠configuration.json:

    ```EnlighterJSRAW
    {

        "accessory": "HTTP-SWITCH",

        "name": "Library Plug",

        "switchType": "stateful",

        "onUrl": {

            "url": "http://192.168.1.124:5000/setSta",

            "method": "POST",

            "body": "{\"status\":1}",

            "headers": {

                "Content-Type": "application/json"

            }

        },

        "offUrl": {

            "url": "http://192.168.1.94:5000/setSta",

            "method": "POST",

            "body": "{\"status\":0}",

            "headers": {

                "Content-Type": "application/json"

            }

        },

        "statusUrl": {

            "url": "http://192.168.1.94:5000/getSta",

            "method": "POST"

        },

        "statusPattern": "\"status\":1"

    }
    ```

#### **Explanation of Each Field**

 * `⁠"accessory": "HTTP-SWITCH" `: tells Homebridge to use the ⁠homebridge-http-
   switch plugin for this device.
 * `"name" `: the name that will appear in the Apple Home app. ⁠
 * `"switchType": "stateful"` : means the switch remembers its ON/OFF state (as 
   opposed to ⁠”stateless”, used for momentary buttons). ⁠
 * “`onUrl" / ⁠"offUrl" `: the HTTP requests sent when you toggle the switch in 
   HomeKit. Each includes:
    - `⁠"url" `: the plug’s local endpoint.
    - `"method"` : the HTTP method used (⁠POST in this case). ⁠
    - `"body" `: the JSON payload sent to the plug (⁠{“status”:1} for on, ⁠{“status”:
      0} for off).
    - ⁠`"headers"` : tells the plug’s server to expect JSON-formatted data.
    - `"statusUrl" `: the endpoint Homebridge polls to check the plug’s actual current
      state, keeping HomeKit’s display in sync even if the plug was toggled another
      way (e.g., manually or via the Beli app).
    -  ⁠`"statusPattern"` : the text string Homebridge looks for in the response
      of ⁠statusUrl to determine if the plug is ON. Since the plug returns `⁠{"resp_code":
      0,"data":{"status":1}} `when on, we search for the substring ⁠”status”:1.

### **Step 5 — Restart Homebridge (Fully, Not Just the Plugin)**

After saving your ⁠configuration.json, restart the **entire Homebridge service**,
not just the individual child bridge, to make sure the new configuration is fully
reloaded.

Via Config UI X:

 * Go to **Homebridge Settings** and click **Restart Homebridge**.

Via SSH: `sudo hb-service restart`

or, depending on your installation : `sudo systemctl restart homebridge`

### **Step 6 — Verify in the Logs**

Check the Homebridge logs to confirm the accessory loaded correctly. You should 
see:

    ```EnlighterJSRAW
    [Library Plug] Initializing HTTP-SWITCH accessory...

    [Library Plug] Switch successfully configured...
    ```

If instead you see : `Property 'statusUrl' is required when using switchType 'stateful'`

This means the ⁠statusUrl field was stripped from the configuration — usually because
the plugin’s graphical settings panel was opened and saved after your manual edit,
overwriting it. **Always re-edit through the raw JSON editor**, and avoid opening
the plugin’s visual settings form afterward.

### **Step 7 — Pair with Apple Home**

Once the accessory loads successfully, Homebridge will display a **setup code** 
in the logs (or in Config UI X):

    ```EnlighterJSRAW
    Setup Payload:

    X-HM://0023RQRLWDF8V

    Enter this code with your HomeKit app on your iOS device to pair with Homebridge:

    ┌────────────┐

    │ 181-58-916 │

    └────────────┘
    ```

Open the **Home app** on your iPhone/iPad, tap **Add Accessory**, scan the QR code(
or manually enter the 8-digit code), and your Tenda plug will appear as a standard
HomeKit switch.

![](https://blog.jouskaio.me/wp-content/uploads/2026/08/IoT-demo-beli-1.gif)

### **Troubleshooting Tips**

| **Symptom** | **Likely Cause** | **Fix** | 
| ⁠curl returns “Connection refused” | Your computer isn’t on the same 2.4 GHz Wi-Fi network as the plug | Double-check your Wi-Fi connection; Tenda plugs only support 2.4 GHz | 
| Accessory disappears after every restart | The plugin’s graphical settings form is overwriting ⁠statusUrl | Only edit ⁠config.json via the raw JSON editor | 
| Plug’s IP address keeps changing | No DHCP reservation configured | Set a static DHCP lease for the plug’s MAC address in your router | 
| Status doesn’t update in the Home app | ⁠statusPattern doesn’t match the plug’s actual JSON response | Run ⁠curl -X POST http://<IP>:5000/getSta again and adjust the pattern to match the exact returned text |

## **Summary**

Using the plug’s local, unauthenticated HTTP API—homebridge plug wifi tenda beli—
allows direct integration. This avoids the Tuya cloud or the Beli app and provides
local, fast control through Apple HomeKit. No Tuya Local Key or account linking 
is needed.

## July Book Dump

- Link: https://blog.jouskaio.me/july-book-dump/
- Published: 2026-08-09T19:35:01+02:00
- Author: Manon Salsou

Another month, another little stack of books 📚

Here’s a look at what I read this month — the stories that kept me turning pages,
the ones that surprised me, and a few that might just stay with me for a while.

Let’s take a look at this month’s reads. ✨

 [](https://blog.jouskaio.me/book/yellowface/)

##  󠀁[Yellowface](https://blog.jouskaio.me/book/yellowface/)󠁿 󠀁[#](https://blog.jouskaio.me/july-book-dump/#book-220)󠁿

 **By** Rebecca F. Kuang

  Finished

 Psychological thriller Satire

 ⚠ Violence ⚠ Abuse

### Summary

Authors June Hayward and Athena Liu were supposed to be twin rising stars: same 
year at Yale, same debut year in publishing. But Athena’s a cross-genre literary
darling, and June didn’t even get a paperback release. Nobody wants stories about
basic white girls, June thinks. So when June witnesses Athena’s death in a freak
accident, she acts on impulse: she steals Athena’s just-finished masterpiece, an
experimental novel about the unsung contributions of Chinese laborers to the British
and French war efforts during World War I. So what if June edits Athena’s novel 
and sends it to her agent as her own work? So what if she lets her new publisher
rebrand her as Juniper Song–complete with an ambiguously ethnic author photo? Doesn’t
this piece of history deserve to be told, whoever the teller? That’s what June claims,
and the New York Times bestseller list seems to agree. But June can’t get away from
Athena’s shadow, and emerging evidence threatens to bring June’s (stolen) success
down around her. As June races to protect her secret, she discovers exactly how 
far she will go to keep what she thinks she deserves. With its totally immersive
first-person voice, Yellowface takes on questions of diversity, racism, and cultural
appropriation not only in the publishing industry but the persistent erasure of 
Asian-American voices and history by Western white society. R. F. Kuang’s novel 
is timely, razor-sharp, and eminently readable.

The best way to hide a lie is in plain sight.

 * **Publisher:** Pile à Lire
 * **Published:** 9 August 2026
 * **Pages:** 336
 * **ISBN:** 978-0008532772

 [Buy this book](https://www.amazon.fr/Yellowface-Rebecca-F-Kuang/dp/000853277X)
[View on Pagebound](https://pagebound.co/books/390398a4-d83e-4987-84fc-bd032f35d233?view=about)

### My Review

 ⭐ 4 / 5

 **Started:** 20 July 2026 **Finished:** 1 August 2026

3/5

Enjoyment

4/5

Quality

5/5

Characters

4/5

Plot

Insufferable is the word I would use to describe the main character of this story,
June Hayward.

Rebecca F. Kuang, whose tragic series The Poppy War I had heard so much about, was
an amazing discovery through her novel Yellowface. Unlike many books where the main
character is portrayed as either a kind-hearted hero or a sarcastic antihero you
can’t help but grow attached to, Rebecca F. Kuang chose to make hers as insufferable
as possible.

Between racism, jealousy, betrayal, bad faith, pathetic behavior, and a complete
lack of self-awareness, you might expect June to eventually learn from her mistakes—
but not at all. That’s actually what makes this book so fascinating: every time 
you think she’s hit rock bottom, she somehow manages to dig herself even deeper.
What’s even more surprising is the confidence with which she keeps making one terrible
decision after another, even though most of her problems are entirely self-inflicted.

Overall, this was a huge favorite of mine. I’ll definitely be reading more of Rebecca
F. Kuang’s books.

 [↑ Back to list](https://blog.jouskaio.me/july-book-dump/#book-dump-toc)

 [](https://blog.jouskaio.me/book/nachtland/)

##  󠀁[Nachtland](https://blog.jouskaio.me/book/nachtland/)󠁿 󠀁[#](https://blog.jouskaio.me/july-book-dump/#book-201)󠁿

### translated by Maja Zade

 **By** Marius von Mayenburg

  Finished

 Satire Theater

### Summary

The people who buy paintings like this aren’t just driven by aesthetic desire. People
who buy paintings like this want a story. A story that catapults them into the orbit
of the Führer.

As Nicola and Philipp are clearing out their late father’s house, they find an old
painting stashed in the attic: a quaint watercolour of a church on a pale summer
day, signed ‘A. Hitler’.

Nicola wants to sell it. Philipp wants to keep it. Philipp’s wife Judith wants to
burn it.

A jagged satire from one of Germany’s foremost playwrights, Nachtland opened at 
the Young Vic Theatre, London, in February 2024.

There's no statement without the person making the statement, there's no art without
the artist.

 * **Publisher:** Faber & Faber
 * **Published:** 7 March 2024
 * **Pages:** 96
 * **ISBN:** 0571390382

 [Buy this book](https://www.amazon.fr/Nachtland-Marius-von-Mayenburg/dp/0571390382)
[View on Pagebound](https://pagebound.co/books/4e8403bf-5418-4f21-81b6-31c0a35bdb24)

### My Review

 ⭐ 3.5 / 5

 **Started:** 1 August 2026 **Finished:** 6 August 2026

3/5

Enjoyment

4/5

Quality

3/5

Characters

4/5

Plot

Nachtland, by Marius von Mayenburg, is a play that deliberately embraces provocation
as its dramatic engine. It exposes the worst in its characters to explore a burning
question: can we truly separate the artist from their work?

Through figures that are by turns opportunistic, moralizing, or simply overwhelmed
by their own contradictions, Mayenburg crafts a human gallery oscillating between
the grotesque and the disturbingly plausible.

The play multiplies its angles of attack: ethical debates, vulgar provocations, 
and parallels so heavy-handed they sometimes verge on irrelevance. Yet despite these
excesses, it manages to evoke a wide range of emotions. We laugh, we cringe, we 
feel outraged, but we also – sometimes reluctantly – understand the characters? 
reasoning, however twisted or exaggerated it may be. It is precisely in this tension
between rejection and comprehension that the play finds its strength.

Reading Nachtland, it is impossible not to think of contemporary debates surrounding
the separation between an artwork and its creator. The controversies involving Roman
Polanski during his Oscar recognition, or the public accusations surrounding Gérard
Depardieu, resonate strongly with the questions raised by the pLay. Nachtland seems
to converse with these cases, not to judge them, but to reveal how deeply they divide
us and expose our own moral contradictions.

On another level, the play reminded me of Le Prénom by Matthieu Delaporte, a French
comedy that sparked intense discussion upon its release. Both works share a similar
device: a family gathering that spirals out of control around a charged symbobvo
here a painting, there a name But while Le Prénom relies on 6-humor and social discomfort,
Nachtland pushes provocation much furtherluv ziotisq confronting the audience with
its own ehical boundarie.

Ultimately, Nachtland is disturbing, sometimes excessive, but indeniably stimulating
play. It forces us to face questions we often prefer to avoid, and or that may well
be its greatest achievement.

 [↑ Back to list](https://blog.jouskaio.me/july-book-dump/#book-dump-toc)

 [](https://blog.jouskaio.me/book/scythe/)

##  󠀁[Scythe](https://blog.jouskaio.me/book/scythe/)󠁿 󠀁[#](https://blog.jouskaio.me/july-book-dump/#book-272)󠁿

 **By** Neal Shusterman

  DNF

 Dystopian Fantasy Romance Science Fiction

 ⚠ Violence ⚠ Death ⚠ Drug Use ⚠ Abuse

### Summary

Thou shalt kill.

A world with no hunger, no disease, no war, no misery.

Humanity has conquered all those things, and has even conquered death. Now scythes
are the only ones who can end life-and they are commanded to do so, in order to 
keep the size of the population under control.

Citra and Rowan are chosen to apprentice to a scythe—a role that neither wants. 
These teens must master the “art” of taking life, knowing that the consequence of
failure could mean losing their own.

The greatest achievement of the human race was not conquering death. It was ending
government.

 * **Publisher:** Walker
 * **Published:** 10 August 2026
 * **Pages:** 441
 * **ISBN:** 9781442472426

 [Buy this book](https://www.walkerbooks.com.au/book/9781529512434/) [View on Pagebound](https://pagebound.co/books/397cb3b3-41ad-4df1-991c-9d003ff5d557)

### My Review

 ⭐ 2 / 5

 **Started:** 10 August 2026 **Finished:** 24 August 2026

1/5

Enjoyment

2/5

Quality

1/5

Characters

3/5

Plot

I unfortunately abandoned my reading of **Scythe**, despite some genuine qualities.

On paper, the world-building is very appealing: a society where death has been conquered,
where “Scythes” must decide who has to die in order to regulate the population. 
The theme is original and raises some great philosophical questions, even though,
in the portion I read, this potential remained fairly underexploited.

The novel does, however, deserve credit for offering main characters whose age is
finally consistent with their characterization. Here we avoid the recurring YA cliché
of the 13-year-old teenager who, with no particular background, already possesses
the maturity and expertise of a forty-year-old man. In the same vein, the romance,
while present, remains understated and never takes precedence over the plot. This
is rare enough to be worth pointing out, since this kind of subplot is usually poorly
handled in the genre, with romances that end up overtaking the story to the point
of erasing the world-building, and characters who fall in love for no reason other
than their looks. Here, it stays in its proper place, which suits me well since 
I don’t particularly enjoy this type of subplot when it becomes too overwhelming.

The novel’s real problem is its pacing. The plot takes an interminable amount of
time to get going: I understand the narrative logic — the characters are in training,
and it’s normal for that phase to take time — but almost nothing happens for far
too long. On top of that, the characters lack depth, especially the main protagonists,
which makes them bland and hard to connect with despite their central importance
to the story.

In the end, **Scythe** remains an enjoyable, undemanding YA read, carried by an 
appealing world at first glance. But between the slow start and the underdeveloped
characters, I couldn’t find the motivation to see it through to the end.

 [↑ Back to list](https://blog.jouskaio.me/july-book-dump/#book-dump-toc)

## Deploy Flask API with Gunicorn and Apache

- Link: https://blog.jouskaio.me/deploy-flask-api-with-gunicorn-and-apache/
- Published: 2024-05-22T14:01:19+02:00
- Author: Manon Salsou

Deploying a Flask API is a common task for developers aiming to bring their web 
applications to life. While Flask itself is a powerful micro web framework, deploying
it efficiently requires a reliable and scalable setup. This article will guide you
through the process of deploying a Flask API using Gunicorn and Apache2. By leveraging
Gunicorn, a Python WSGI HTTP Server for UNIX, and the robust Apache2 web server,
you can ensure your Flask API performs well under heavy loads and remains secure.
Let’s dive into the steps to get your Flask application up and running in a production
environment.

# Installation

Firstly please install your application somewhere in your server, for good practice
it is recommanded to user this following path `/var/www/html/you_project`

In this folder, check if you have a `.venv` folder which is the virtual environment
to run python locally. If you do not have this environment, please create it by 
executing this command : `python3 -m venv .venv`. Then access to this virtual environment
with `source .venv/bin/avtivate` then do not forget to `pip install` all your packages
in your projects.

Make sure to install these packages :

 * `gunicorn`
 * `Flask`

Once done, please execute the command to deploy the flask API with gunicorn `gunicorn-
b 0.0.0.0:5000 main:app`. It should return you something like that :

    ```EnlighterJSRAW
    (.venv) ubuntu@ip-10-0-3-23:~/_Projets/API_Hub/API-Webhook gunicorn -b 0.0.0.0:5002 main:app
    [2024-05-22 11:44:31 +0200] [7898] [INFO] Starting gunicorn 21.2.0
    [2024-05-22 11:44:31 +0200] [7898] [INFO] Listening at: http://0.0.0.0:5002 (7898)
    [2024-05-22 11:44:31 +0200] [7898] [INFO] Using worker: sync
    [2024-05-22 11:44:31 +0200] [7901] [INFO] Booting worker with pid: 7901
    [2024-05-22 11:44:45 +0200] [7898] [INFO] Handling signal: int
    [2024-05-22 11:44:45 +0200] [7901] [INFO] Worker exiting (pid: 7901)
    [2024-05-22 11:44:45 +0200] [7898] [INFO] Shutting down: Master
    ```

`-b 0.0.0.0:500` specify that the application will run on 0.0.0.0 on the port 5000.
Of course, you can change 5000 to another port.

`main:app` implies that you will run your files main.py from the gunicorn files 
app.wsgi.

To see what `app.wsgi` should look like, see below

    ```EnlighterJSRAW
    #app.wsgi
    import sys
    sys.path.insert(0,'/home/ubuntu/_Projets/API_Hub/API-Webhook/') # Load your project

    from main import app as application # Execute your Flask app named "app"
    ```

If it’s working, then you are ready to deploy your application.

# Creation of a service

First of all, we will make sure that our flask API will work at anytime in the background(
so without blocking our server by being use on the main thread).

For that we will create a new service

    ```EnlighterJSRAW
    # webhooks-apihub.service
    [Unit]
    Description=Gunicorn instance to serve flask API
    After=network.target

    [Service]
    User=ubuntu
    Group=ubuntu
    WorkingDirectory=/home/ubuntu/_Projets/API_Hub/API-Webhook/
    ExecStart=/home/ubuntu/_Projets/API_Hub/API-Webhook/.venv/bin/gunicorn -b 0.0.0.0:5000 main:app
    Restart=always

    [Install]
    WantedBy=multi-user.target
    ```

In this command, we can see that we execute the gunicorn package in our virtual 
environment previously created with `/home/ubuntu/_Projets/API_Hub/API-WebhookEfento/.
venv/bin/gunicorn gunicorn`

Once everything is done, we will enable this service and start it

    ```EnlighterJSRAW
    # Reload all units
    sudo systemctl daemon-reload
    # Activate the service
    sudo systemctl enable webhooks-apihub.service
    # Start the service
    sudo systemctl start webhooks-apihub.service
    # Check the status
    sudo systemctl status webhooks-apihub.service
    ```

# Apache

Now, that our application is deployed, we will manage it to be accessible from the
outside with Apache2.

For that, create a new `.conf` in `/etc/apache2/sites-available/`

    ```EnlighterJSRAW
    <VirtualHost *:80>
        ServerName www.domain.com
        ServerAdmin you@mail.com
        DocumentRoot /home/ubuntu/_Projets/API_Hub/API-Webhook/

        ErrorLog ${APACHE_LOG_DIR}/apihub-api.log
        CustomLog ${APACHE_LOG_DIR}/apihub.log combined

        ProxyPass               / http://0.0.0.0:5000/
        ProxyPassReverse        / http://0.0.0.0:5000/
        ProxyRequests           Off

        <Directory /home/ubuntu/_Projets/API_Hub/API-Webhook>
            WSGIProcessGroup app
            WSGIApplicationGroup %{GLOBAL}
            Order deny,allow
            Require all granted
        </Directory>
    </VirtualHost>
    ```

This will create an HTTP link (http://www.domain.com), if you want to create an 
HTTPS link, make sure to use `<VirtualHost *:443>` which is the port used for secure
HTTP connections and add your certificate and keys like that or use [`certbot`](https://certbot.eff.org/)

    ```EnlighterJSRAW
    <VirtualHost *:443>
            ServerName www.domain.com
            ServerAdmin you@mail.com
            DocumentRoot /home/ubuntu/_Projets/API_Hub/API-Webhook/

            ErrorLog ${APACHE_LOG_DIR}/apihub-api.log
            CustomLog ${APACHE_LOG_DIR}/apihub.log combined

            ProxyPass               / http://0.0.0.0:5000/
            ProxyPassReverse        / http://0.0.0.0:5000/
            ProxyRequests           Off

            SSLEngine On
            SSLCertificateFile your/path/fullchain.crt
            SSLCertificateKeyFile /your/path/privkey.pem
            Include /etc/letsencrypt/options-ssl-apache.conf
    </VirtualHost>
    ```

Once done, like our service, we will enable our site and then activate it.

    ```EnlighterJSRAW
    # Check the syntaxe of each conf file
    sudo apachectl -t
    # Enable the conf
    sudo a2ensite webhooks-apihub.service.conf
    # Restart apache2 to take in consideration changes without stopping the service
    sudo systemctl reload apache2
    # Check the status
    sudo systemctl reload apache2
    ```

Now, if the status is correct, you should see something on www.domain.com… then 
congratulation, you succeed to deploy your first application flask ! On the future,
if you want to add new flask API, make sure to user another port gunicorn than 5000.

## Welcome aboard

- Link: https://blog.jouskaio.me/welcome/
- Published: 2024-05-17T15:08:34+02:00
- Author: Manon Salsou

Hi, I’m Manon Salsou (Jouskaio), a software engineer based in Paris, and this blog
is my technical notebook in public.

I build and run self-hosted systems, then document what worked, what broke, and 
how I fixed it. If you like practical infrastructure posts, clean architecture decisions,
and real-world troubleshooting, you’re in the right place.

## What you’ll find here

 * **Self-hosting in production:** media stack operations, storage migrations, hardlink-
   safe paths, and reliability patterns.
 * **Monitoring and observability:** VictoriaMetrics, Grafana, Loki, Alloy, alerting
   pipelines, and onboarding new nodes.
 * **Systems and automation:** Proxmox, Debian VMs, LXC containers, Docker Compose,
   scripts, and repeatable maintenance workflows.
 * **Embedded-to-cloud integration:** collecting telemetry from constrained devices
   through MQTT, serial, or HTTP bridges.

## How I write

Every article is grounded in hands-on implementation. I focus on decisions, trade-
offs, and reproducible steps rather than theory alone. You’ll usually get architecture
context, command-level details, and post-change verification so you can adapt the
approach to your own setup.

## What this blog is for

This space is for engineers, self-hosters, and curious builders who want reliable
systems without losing the fun of experimentation. My goal is simple: publish useful,
honest technical notes that save you time and help you avoid avoidable mistakes.

## Start here

 * Media stack cleanup and mount-path consolidation
 * Monitoring stack deployment and scaling strategy
 * Operational guides for safe migrations and rollback planning

Thanks for reading, and welcome aboard.
