A lightweight Docker container that keeps Gluetun VPN port forwarding and qBittorrent in sync automatically.
When Gluetun port forwarding is enabled, NAT-PMP renewal can fail while the VPN tunnel stays up. Docker still sees the container as healthy, but qBittorrent loses its reachable listen port and torrent connectivity degrades.
Common workarounds such as VPN_PORT_FORWARDING_UP_COMMAND, Gluetun healthchecks, autoheal containers, or cron scripts each cover only part of the failure mode and can introduce race conditions or container restart problems.
Related Gluetun issues:
- qdm12/gluetun#1891 — port forwarding stops, stuck in restart loop
- qdm12/gluetun#1749 — NAT-PMP does not recover after internal VPN restart
- qdm12/gluetun#1882 — port connection lost after 10-15 minutes
- qdm12/gluetun#2679 — port forwarding connection refused
- qdm12/gluetun#3196 — port changing every minute with WireGuard
- qdm12/gluetun#3079 — timeout on port forwarding with WireGuard and OpenVPN
- qdm12/gluetun#2528 — WireGuard port forwarding fails while OpenVPN works
Every cycle, the watchdog:
- Reads Gluetun's forwarded port from the HTTP control API.
- Recovers missing ports by restarting the VPN through Gluetun's API, then restarting containers only as a last resort.
- Authenticates to qBittorrent using either API-key auth or legacy username/password auth.
- Updates qBittorrent's
listen_portwhen it differs from Gluetun's forwarded port. - Logs periodic heartbeats so you know it is alive.
Example logs:
2026-03-19 09:29:04 WARNING: Port mismatch: gluetun=47830, qbt=34987. Fixing...
2026-03-19 09:29:07 Port synced successfully: 47830
2026-03-19 09:34:14 OK: gluetun=47830, qbt=47830
2026-03-19 15:01:45 WARNING: No forwarded port from gluetun. Attempting VPN restart...
2026-03-19 15:02:03 New port after recovery: 52194
docker run --rm qmcgaw/gluetun genkeyConfigure Gluetun's control server with that key:
environment:
- HTTP_CONTROL_SERVER_AUTH_DEFAULT_ROLE={"auth":"apikey","apikey":"${GLUETUN_API_KEY}"}For qBittorrent 5.2.0 / WebAPI 2.14.1+, API-key auth is preferred:
- Open qBittorrent WebUI.
- Go to Preferences → WebUI → API Key.
- Generate an API key.
- Set
QBT_API_KEYin.env.
If you have not generated an API key yet, or you are using an older qBittorrent version, leave QBT_API_KEY empty and use QBT_USER / QBT_PASS instead.
cp .env.example .envExample:
GLUETUN_API_KEY=YourGeneratedGluetunKeyHere
# Preferred for qBittorrent 5.2.0+
QBT_API_KEY=qbt_yourGeneratedQbitApiKeyHere
QBT_AUTH_MODE=auto
# Legacy fallback / first-start recovery
QBT_USER=admin
QBT_PASS=yourpasswordUse the published GHCR image:
gluetun-qbt-watchdog:
image: ghcr.io/brunoorsolon/gluetun-qbt-watchdog:latest
container_name: gluetun-qbt-watchdog
restart: unless-stopped
networks:
- vpn-net
depends_on:
- gluetun
- qbittorrent
volumes:
- /var/run/docker.sock:/var/run/docker.sock
environment:
- GLUETUN_CONTAINER_NAME=${GLUETUN_CONTAINER_NAME:-gluetun}
- GLUETUN_API=http://${GLUETUN_CONTAINER_NAME:-gluetun}:${GLUETUN_API_PORT:-8000}
- GLUETUN_API_KEY=${GLUETUN_API_KEY}
- QBT_CONTAINER_NAME=${QBT_CONTAINER_NAME:-qbittorrent}
- QBT_API=http://${GLUETUN_CONTAINER_NAME:-gluetun}:${QBT_WEBUI_PORT:-8080}
- QBT_API_KEY=${QBT_API_KEY:-}
- QBT_AUTH_MODE=${QBT_AUTH_MODE:-auto}
- QBT_USER=${QBT_USER:-admin}
- QBT_PASS=${QBT_PASS:-}
- CHECK_INTERVAL=${CHECK_INTERVAL:-60}
- HEARTBEAT_CYCLE_FREQUENCY=${HEARTBEAT_CYCLE_FREQUENCY:-10}
- MAX_RESTART_WAIT=${MAX_RESTART_WAIT:-120}
- ADDITIONAL_RESTART=${ADDITIONAL_RESTART:-}A complete example is available in docker-compose.yml.example.
docker pull ghcr.io/brunoorsolon/gluetun-qbt-watchdog:latestYou can also pin a release tag once releases are published:
image: ghcr.io/brunoorsolon/gluetun-qbt-watchdog:1.0.0| Variable | Default | Description |
|---|---|---|
GLUETUN_CONTAINER_NAME |
gluetun |
Docker container name for Gluetun. |
GLUETUN_API |
http://gluetun:8000 |
Gluetun HTTP control API URL. |
GLUETUN_API_KEY |
empty | API key generated by docker run --rm qmcgaw/gluetun genkey. |
QBT_CONTAINER_NAME |
qbittorrent |
Docker container name for qBittorrent. |
QBT_API |
http://gluetun:8080 |
qBittorrent WebAPI URL. Use the Gluetun service when qBittorrent shares Gluetun's network namespace. |
QBT_API_KEY |
empty | qBittorrent 5.2.0+ API key. Preferred when available. |
QBT_AUTH_MODE |
auto |
auto, apikey, or password. |
QBT_USER |
admin |
qBittorrent username for legacy password auth. |
QBT_PASS |
empty | qBittorrent password for legacy password auth and first-start recovery. |
CHECK_INTERVAL |
60 |
Seconds between sync cycles. |
HEARTBEAT_CYCLE_FREQUENCY |
10 |
Log an OK heartbeat every N successful cycles. |
MAX_RESTART_WAIT |
120 |
Seconds to wait for a forwarded port after full restart. |
ADDITIONAL_RESTART |
empty | Optional space-separated container names to restart after Gluetun/qBittorrent. |
QBT_AUTH_MODE=auto is the recommended default:
- If
QBT_API_KEYis set, the watchdog uses qBittorrent API-key auth withAuthorization: Bearer <QBT_API_KEY>. - If
QBT_API_KEYis empty, the watchdog falls back toQBT_USER/QBT_PASScookie login.
Other modes:
QBT_AUTH_MODE=apikeyrequiresQBT_API_KEYand never calls qBittorrent auth endpoints.QBT_AUTH_MODE=passwordalways usesQBT_USER/QBT_PASS.
A configured but rejected API key is treated as a configuration error and does not silently fall back to the password. Leave QBT_API_KEY empty if you intentionally want password auth.
Password auth supports both legacy qBittorrent login success (200 with Ok.) and qBittorrent 5.2.0 login success (204 No Content).
In password mode, if the watchdog cannot log in with QBT_PASS, it tries to recover automatically:
- Read the temporary password from qBittorrent container logs.
- Log in with that temporary password.
- Update qBittorrent's WebUI password to
QBT_PASS. - Retry normal login and continue the sync loop.
This is useful on first startup with linuxserver/qbittorrent, where qBittorrent may generate a temporary password before you set your own.
Password recovery is not used in API-key mode. If you are setting up a new qBittorrent 5.2.0 instance, you can start with password mode, generate an API key in the WebUI, then switch back to QBT_AUTH_MODE=auto with QBT_API_KEY set.
When Gluetun has no forwarded port:
- Restart the VPN through Gluetun's HTTP API.
- Wait briefly and check the forwarded port again.
- If still missing, restart Gluetun and qBittorrent containers through Docker.
- Restart any containers listed in
ADDITIONAL_RESTART. - Give up after
MAX_RESTART_WAITand retry next cycle.
┌─────────────────────────────────────────────────────┐
│ vpn-net (Docker bridge network) │
│ │
│ ┌───────────┐ ┌──────────────┐ ┌────────────┐ │
│ │ gluetun │ │ qbittorrent │ │ watchdog │ │
│ │ :8000 API │ │ :8080 WebUI │ │ │ │
│ │ :8080 ←── ┼───┤ (shares net) │ │ curl → API │ │
│ └───────────┘ └──────────────┘ └────────────┘ │
│ │ network_mode: │
│ │ service:gluetun │
└───────┼─────────────────────────────────────────────┘
│
VPN tunnel → ProtonVPN → NAT-PMP port forwarding
qBittorrent usually uses network_mode: "service:gluetun", so it shares Gluetun's network namespace. The watchdog is on vpn-net independently and talks to both services through Gluetun's exposed ports/DNS name.
The watchdog mounts /var/run/docker.sock so it can:
- restart containers as a last-resort recovery path
- read qBittorrent logs for legacy password recovery
Docker socket access allows container control from inside the watchdog container. Only run this in an environment where you trust the image and Compose configuration.
If the watchdog is working for your stack, you generally do not need:
VPN_PORT_FORWARDING_UP_COMMAND/VPN_PORT_FORWARDING_DOWN_COMMANDon Gluetun- Docker healthchecks on Gluetun / qBittorrent solely for port-forward recovery
willfarrell/autohealor similar auto-restart containers for this issue- cron jobs that check port files or restart containers
The recommended deployment uses GHCR. To build locally instead:
gluetun-qbt-watchdog:
build: ./gluetun-qbt-watchdogThe build context contains:
gluetun-qbt-watchdog/
├── Dockerfile
└── gluetun-qbt-watchdog.sh
The GitHub Actions workflow publishes to:
ghcr.io/brunoorsolon/gluetun-qbt-watchdog
Publishing behavior:
- Push to
main: publisheslatestand the branch tag. - Push
v*.*.*: publishes semver tags such as1.2.3and1.2. - Manual dispatch: publishes tags derived from the selected ref.
After the first GHCR publish, make the package public in GitHub package settings if unauthenticated pulls should work.
- Gluetun with
VPN_PORT_FORWARDING=onand HTTP control server enabled. - qBittorrent with WebUI/WebAPI enabled.
- Docker socket access for restart fallback and legacy password recovery.
- Shared Docker network where the watchdog can reach Gluetun and qBittorrent WebAPI.
- Gluetun
latest(v3.40+) - ProtonVPN WireGuard port forwarding
- qBittorrent 5.2.0 / WebAPI 2.14.1+ API-key auth
- qBittorrent legacy username/password auth
- qBittorrent linuxserver image
- TrueNAS SCALE + Docker
- Alpine 3.20 base image
The script is written for qBittorrent, but the pattern works for any torrent client with a web API. You would need to modify the qBittorrent auth, port read, and port write helpers.
MIT — do whatever you want with it.