This one bites a lot of people — depends_on with condition: service_healthy only waits for the service's own healthcheck, so gluetun needs a healthcheck that only reports healthy once the tunnel is actually passing traffic, not just the container running.
What works for me:
services:
gluetun:
image: qmcgaw/gluetun
cap_add: [NET_ADMIN]
devices: [/dev/net/tun:/dev/net/tun]
healthcheck:
test: ["CMD", "sh", "-c", "wget -qO- --spider https://ipinfo.io/ip || exit 1"]
interval: 15s
timeout: 10s
retries: 6
start_period: 60s
qbittorrent:
image: lscr.io/linuxserver/qbittorrent
network_mode: "service:gluetun"
depends_on:
gluetun:
condition: service_healthy
restart: unless-stopped
Key points:
- The HTTP check is the part that actually proves the tunnel passes traffic — a plain process check reports healthy while the VPN is still negotiating, which is exactly why qBt starts before the tunnel is ready.
start_periodmatters: gluetun can take 30-60s to connect on first boot, and without it the retries get consumed immediately.- No port mapping on qbittorrent; with
network_mode: service:gluetunits WebUI is reachable via gluetun's ports section (forward 8080 on gluetun instead). - Provider blocks the check URL? Swap in
ifconfig.meor your provider's status endpoint; gluetun also hasHEALTH_VPN_DURATION_INITIALfor native tuning. - Validate the chain:
docker exec qbittorrent curl -s ifconfig.meshould print the VPN exit IP, not your home IP.
Caveat: keep restart: unless-stopped on both services — a qBt restart while gluetun renegotiates can leave it in a dead network state until you docker compose restart qbittorrent.