services: gluetun: image: qmcgaw/gluetun:latest container_name: gluetun cap_add: - NET_ADMIN devices: - /dev/net/tun:/dev/net/tun environment: - VPN_SERVICE_PROVIDER=protonvpn - VPN_TYPE=openvpn - OPENVPN_USER=${OPENVPN_USER} - OPENVPN_PASSWORD=${OPENVPN_PASSWORD} - FIREWALL_OUTBOUND_SUBNETS=192.168.88.0/24 - FIREWALL_INPUT_PORTS=8080 - TZ=America/Chicago ports: - 8080:8080 - 6881:6881 - 6881:6881/udp volumes: - /srv/docker/gluetun:/gluetun restart: unless-stopped healthcheck: test: ["CMD", "sh", "-c", "wget -qO- https://ipinfo.io/ip || exit 1"] interval: 60s timeout: 20s retries: 3 start_period: 30s labels: - "com.centurylinklabs.watchtower.monitor-only=true" qbittorrent: image: lscr.io/linuxserver/qbittorrent:latest container_name: qbittorrent network_mode: "service:gluetun" depends_on: gluetun: condition: service_healthy environment: - PUID=1000 - PGID=1000 - TZ=America/Chicago - WEBUI_PORT=8080 volumes: - /srv/docker/qbittorrent/config:/config - /mnt/media:/mnt/media restart: unless-stopped labels: - "com.centurylinklabs.watchtower.monitor-only=true" # NOTE (2026-08-07): Replaces the native qbittorrent-nox systemd service on the # jellyfin host, which was bound to a desktop ProtonVPN app's tunnel interface # (proton0). When that GUI-managed tunnel dropped, there was no way to reconnect # without an interactive desktop session, silently killing all downloads. # # qbittorrent now runs with network_mode: service:gluetun, so it shares gluetun's # network namespace entirely. If the VPN drops, qbittorrent has zero network # access (fails closed, never falls back to the real IP) until gluetun # auto-reconnects on its own -- no GUI or manual intervention needed. # # OPENVPN_USER / OPENVPN_PASSWORD are the ProtonVPN OpenVPN/IKEv2 credentials # (see API Codes.md), NOT the regular Proton account login. Set them in a .env # file alongside this compose file (not committed) or export them before # `docker compose up -d`. # # qbittorrent's media volume is mounted at /mnt/media (matching the original # native install), NOT /data, so it lines up with Sonarr/Radarr's existing # Remote Path Mapping (host 192.168.88.10: /mnt/media/ -> /data/) and the # pre-existing categories (tv-sonarr, movies-radarr) needed zero path rewriting # when migrating resume data. # # NOTE (2026-09-03): Added com.centurylinklabs.watchtower.monitor-only=true to # both services. A Watchtower instance was newly deployed on the jellyfin host # on 2026-09-01 (previously only siklos had one), and its first successful # gluetun update on 2026-09-02 broke qbittorrent: Watchtower recreates the # dependent container (qbittorrent) BEFORE recreating the linked target # (gluetun), so qbittorrent got wired to gluetun's old, about-to-be-removed # container ID and lost all network access until manually fixed. monitor-only # keeps the nightly email notification (so Joe still hears about new images) # but stops Watchtower from auto-restarting this pair. Update manually with # `cd /srv/docker/qbittorrent && docker compose pull && docker compose up -d` # (compose handles the gluetun-then-qbittorrent order correctly; watchtower # does not).