qBittorrent
Runs inside the gluetun VPN tunnel (network_mode: service:gluetun), so it publishes no
ports of its own — the Web UI is reachable on gluetun’s 21287.
Config lives on a bind mount at /volume1/docker-ssd/qbittorrent/appdata, mapped to
/config. It is not a Docker named volume, so it survives docker compose down -v.
It is captured nightly by the stack’s backup job (../backup/), which stops the
container for the copy; the manual procedure below remains the right tool for a
deliberate pre-upgrade snapshot.
The gluetun namespace-orphaning failure
Section titled “The gluetun namespace-orphaning failure”qbittorrent and rustatio both run with network_mode: service:gluetun. Docker binds
a shared network namespace at container start and never re-attaches it. When gluetun
restarts, it gets a brand-new namespace and both dependants are left in the old,
destroyed one — still running, still reporting healthy, but reachable by nothing.
Observed on 2026-08-13:
01:06:48 qbittorrent + rustatio start, join gluetun's namespace01:06:56 gluetun dies: "getting OpenVPN version: signal: killed"01:06:58 gluetun restarts (RestartCount=1) -> NEW namespace dependants: RestartCount=0, still on the dead oneSymptom: curl http://192.168.1.29:21478/ and :21287/ both return nothing (curl
exit 56, connection reset). The host publishes the ports and gluetun holds the mappings,
so docker-proxy forwards into gluetun’s current namespace and finds no listener.
Meanwhile rustatio logs listening on 0.0.0.0:21478 — true, just in a namespace nothing
routes to.
Three mitigations are in place:
HEALTH_VPN_DURATION_INITIAL=30son gluetun (../gluetun/gluetun.yaml). The 6s default is shorter than a cold tunnel takes, so gluetun would kill itself during startup. This addresses the trigger.- Healthchecks that can actually see the fault, plus the
deunhealth.restart.on.unhealthy=truelabel and thedeunhealthwatchdog (../../host/deunhealth.yaml). Each probe hits gluetun’s control server on127.0.0.1:8000, which exists only in gluetun’s live namespace — so an orphaned container fails it. A localhost-only probe cannot: rustatio’s built-in check passed throughout the outage above, which is why nothing alerted. service:gluetunrather thancontainer:gluetun, withdepends_on: condition: service_healthy. Compose-native, drops the hardcoded container name, and stops dependants starting before the tunnel is up.
What this does not cover
Section titled “What this does not cover”deunhealth recovers the case above — gluetun restarting while keeping its container
ID. It cannot recover gluetun being recreated (new ID): the dependants’ stored
container:<old-id> reference is then dead and they fail to start at all, exiting
128. That is what left rustatio Exited (128) for eight days. Recovery there is
docker compose up -d (which recreates the dependants too), not a restart.
This fragility is inherent to sharing a namespace — every remedy is detect-and-restart, not prevention. Eliminating it entirely would mean running gluetun and its dependants as one unit (a Podman pod, or one container under a supervisor).
Version pinning
Section titled “Version pinning”This image is pinned to an exact LinuxServer build rather than :latest:
harbor.toolsera.lan/lscr_proxy_cache/linuxserver/qbittorrent:5.2.3_v2.0.14-ls471The tag encodes three things — qBittorrent 5.2.3, libtorrent v2.0.14, and the
LinuxServer build ls471. Pin it deliberately: qBittorrent rewrites qBittorrent.conf
into the new format on first start after an upgrade, and older builds will not read a
config a newer build has migrated. Upgrades are effectively one-way, which is why
this file does not track :latest.
Back up appdata before any version bump
Section titled “Back up appdata before any version bump”The last bump (5.2.0_v2.0.12-ls454 → 5.2.3_v2.0.14-ls471) was taken with this backup
in place:
/volume2/docker-apps-backup/qbittorrent/qbittorrent-appdata-<timestamp>.tar.gz/volume2/docker-apps-backup/qbittorrent/qbittorrent-appdata-<timestamp>.tar.gz.sha256The archive lives on /volume2 (pool2) while the source is on /volume1 (pool1), so it
sits on independent storage, and it is outside every compose project directory. This
destination changed when appdata moved off /home: writing to
/volume1/docker-ssd/backups/ would now put the archive on the same pool as the data it
protects.
Taking one
Section titled “Taking one”TS=$(date +%Y%m%d-%H%M%S)DEST=/volume2/docker-apps-backup/qbittorrent/qbittorrent-appdata-$TS.tar.gzmkdir -p "$(dirname "$DEST")"tar --exclude=appdata/qBittorrent/ipc-socket \ -czf "$DEST" -C /volume1/docker-ssd/qbittorrent appdatasha256sum "$DEST" | tee "$DEST.sha256"ipc-socket is a Unix socket — tar cannot meaningfully archive one and qBittorrent
recreates it at startup. Everything else is included, so the archive’s file count should
equal find … -type f | wc -l exactly. That equality is the check worth running:
tar -tzf "$DEST" | grep -v '/$' | wc -lfind /volume1/docker-ssd/qbittorrent/appdata -type f | wc -lgzip -t "$DEST" && echo GZIP_OKHot vs cold
Section titled “Hot vs cold”The command above is a hot backup — it runs while the container is up. Config files
are consistent enough for a rollback safety net, but a running client can leave
BT_backup fastresume state mid-write, so a hot archive is not guaranteed to restore
torrent progress cleanly.
For a guaranteed-consistent copy, stop the container first:
docker stop qbittorrent# ... run the tar above ...bws run --access-token "$BWS_ACCESS_TOKEN" -- docker compose up -d qbittorrentNote docker stop on a live host is a deliberate, user-initiated step.
Restoring
Section titled “Restoring”docker stop qbittorrenttar -xzf /volume2/docker-apps-backup/qbittorrent/qbittorrent-appdata-<timestamp>.tar.gz \ -C /volume1/docker-ssd/qbittorrentbws run --access-token "$BWS_ACCESS_TOKEN" -- docker compose up -d qbittorrentThe tarball’s root entry is appdata/, so it extracts over
/volume1/docker-ssd/qbittorrent and lands back in place. Archives taken before the
move to /volume1 have the same appdata/ root and restore the same way — only the
-C target changed. Restoring a config means also reverting the image: tag in
qbittorrent.yaml to the build that wrote it — see the one-way-upgrade note above.