goodall
Docker Compose media stack (Jellyfin, Radarr, Sonarr, Prowlarr, Bazarr, qBittorrent) fronted by Caddy, reachable only over Tailscale. qBittorrent's traffic is routed entirely through Gluetun (WireGuard to Mullvad) via network_mode: "service:gluetun".
Deployed on the NAS at /volume2/docker/goodall (host karan@100.121.200.111, tailnet name goodall.taildc6b0a.ts.net). This local directory is the source of truth for edits; ship a full deploy with:
./deploy.sh
This clears the project directory on the NAS (preserving config/ runtime state and .env), streams the repo over via tar+SSH (DSM's bundled rsync only supports its own daemon/module mode, not plain SSH+path, so this project uses tar instead), decrypts .env.enc and writes it directly to the NAS, then runs docker compose pull && up -d remotely.
Note: this NAS's Docker Manager GUI doesn't show goodall as a project since it was deployed via SSH/CLI rather than the GUI's own wizard — cosmetic only, the stack runs fine either way.
Secrets
All secrets and per-deployment config (VPN credentials, Gandi DNS token, LAN/Tailscale IPs, PUID/PGID/TZ, media paths, and the four app API keys) live encrypted in .env.enc, via sops. To view: sops -d --input-type dotenv --output-type dotenv .env.enc. To edit: sops --input-type dotenv --output-type dotenv .env.enc (opens $EDITOR on the decrypted content, re-encrypts on save). deploy.sh decrypts this on every run and writes it to the NAS directly — there's no plaintext .env to hand-maintain locally.
Updates
All images (including Gluetun) track :latest. Nothing auto-updates — re-run ./deploy.sh to pull and recreate everything. This is deliberate: gluetun and qbittorrent share a network namespace (network_mode: "service:gluetun"), so updates need to recreate both together, which a project-level docker compose pull && up -d does correctly and a per-container auto-updater would not.
Monitoring
Every service has a real HEALTHCHECK (not just "is the process alive" — e.g. qbittorrent's also curls an external IP through gluetun's tunnel, since its WebUI keeps responding even when the VPN is dead). autoheal watches Docker's health status and runs docker restart on anything reporting unhealthy. uptime-kuma (at status.karanj.com) is the dashboard — monitors aren't config-as-code, so after a fresh deploy log in and add a "Docker Container" monitor per service plus an HTTP monitor per public hostname.
Caveat: qbittorrent shares gluetun's network namespace (network_mode: "service:gluetun"), and autoheal restarts containers individually — it has no notion of "restart this one's dependents too." Both carry equivalent connectivity healthchecks so they should flip unhealthy and get restarted within a cycle or two of each other, but it's not perfectly atomic. If qbittorrent ever looks stuck after a gluetun restart, docker compose restart gluetun qbittorrent (together) is the manual fallback — that's what actually fixed the Aug 2026 VPN outage.
Gotchas
- Gluetun blocks inbound container-to-container traffic by default —
FIREWALL_INPUT_PORTS=8123indocker-compose.ymlis required, or qBittorrent's WebUI (proxied by Caddy fordl.karanj.com) returns a 502. - Caddy's Gandi DNS-01 challenge needs
propagation_timeout -1in thetls_gandisnippet (caddy/Caddyfile) — Gandi's own nameserver propagation is inconsistent across resolvers, so Caddy's own pre-check needs to be skipped and left to Let's Encrypt to validate directly.
