1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
# 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=8123` in `docker-compose.yml` is required, or qBittorrent's WebUI (proxied by Caddy for `dl.karanj.com`) returns a 502.
- **Caddy's Gandi DNS-01 challenge needs `propagation_timeout -1`** in the `tls_gandi` snippet (`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.
|