Gluetun is one of the cleanest ways to force Docker traffic through a VPN tunnel. In this post, the routed workload is qBittorrent, but the pattern is the same for other containers: Gluetun acts as a lightweight gateway container with a built-in kill switch, DNS handling, and firewall rules, so the application that shares its network stack inherits the VPN path automatically.
In this post, we deploy Gluetun with two providers that are my personal go-to choices: AirVPN or ProtonVPN.
Why Gluetun#
For containerized workloads like qBittorrent, Gluetun solves a common problem: you want one service to egress through a VPN without touching the host routing table.
The practical benefits are straightforward:
- One container becomes the VPN gateway for qBittorrent.
- The built-in firewall blocks traffic if the tunnel drops.
- DNS is handled inside the VPN container instead of leaking through the host.
- You can switch providers by changing environment variables, not by rebuilding your app images.
Deployment Model#
The pattern is always the same:
- Run one Gluetun container per VPN profile.
- Attach application containers to Gluetun with
network_mode: "service:gluetun". - Expose ports on the Gluetun service, not on the application container.
- Verify the public IP from inside qBittorrent’s network namespace before trusting the setup.
One important constraint: a single Gluetun container routes to one VPN provider at a time. If you want AirVPN for one qBittorrent instance and ProtonVPN for another, run two separate stacks.
Base Compose Skeleton#
This is the shared shape I recommend for qBittorrent on both providers.
services:
gluetun:
image: qmcgaw/gluetun:latest
container_name: gluetun
cap_add:
- NET_ADMIN
devices:
- /dev/net/tun:/dev/net/tun
volumes:
- ./gluetun:/gluetun
environment:
- TZ=Europe/Paris
- FIREWALL_OUTBOUND_SUBNETS=192.168.1.0/24
ports:
- 8080:8080/tcp
restart: unless-stopped
qbittorrent:
image: lscr.io/linuxserver/qbittorrent:latest
network_mode: "service:gluetun"
depends_on:
- gluetun
environment:
- TZ=Europe/Paris
- WEBUI_PORT=8080
restart: unless-stoppedThe ports section belongs to Gluetun because qBittorrent shares its network namespace. If you publish the Web UI, publish it on the VPN container.
AirVPN Profile#
AirVPN works well when you want qBittorrent to use a VPN service with strong WireGuard support and port-forwarding-friendly workflows.
For a clean Gluetun setup, use WireGuard unless you have a specific reason to stay on OpenVPN.
services:
gluetun-airvpn:
image: qmcgaw/gluetun:latest
container_name: gluetun-airvpn
cap_add:
- NET_ADMIN
devices:
- /dev/net/tun:/dev/net/tun
volumes:
- ./gluetun-airvpn:/gluetun
environment:
- VPN_SERVICE_PROVIDER=airvpn
- VPN_TYPE=wireguard
- WIREGUARD_PRIVATE_KEY=${YOUR_AIRVPN_PRIVATE_KEY}
- WIREGUARD_PRESHARED_KEY=${YOUR_AIRVPN_PRESHARED_KEY}
- WIREGUARD_ADDRESSES=${YOUR_AIRVPN_ADDRESSES}
- SERVER_COUNTRIES=Netherlands
- TZ=Europe/Paris
- FIREWALL_OUTBOUND_SUBNETS=192.168.1.0/24
ports:
- 8080:8080/tcp
restart: unless-stoppedAirVPN-specific notes:
VPN_SERVICE_PROVIDER=airvpnselects the provider profile.VPN_TYPE=wireguardtells Gluetun to use WireGuard instead of OpenVPN.WIREGUARD_PRIVATE_KEY,WIREGUARD_PRESHARED_KEY, andWIREGUARD_ADDRESSEScome from AirVPN’s client area or config generator.SERVER_COUNTRIES=Netherlandsis a simple way to constrain exit nodes; you can also use regions, cities, names, or hostnames if needed.
If you need inbound access for a workload behind AirVPN, use the provider port-forwarding flow and set FIREWALL_VPN_INPUT_PORTS to the forwarded port.
See AirVPN provider setup for the full list of required keys and expected values.
ProtonVPN Profile#
ProtonVPN is also a good fit for qBittorrent, especially if you want a straightforward WireGuard tunnel with optional port forwarding.
services:
gluetun-protonvpn:
image: qmcgaw/gluetun:latest
container_name: gluetun-protonvpn
cap_add:
- NET_ADMIN
devices:
- /dev/net/tun:/dev/net/tun
volumes:
- ./gluetun-protonvpn:/gluetun
environment:
- FIREWALL_OUTBOUND_SUBNETS=192.168.1.0/24
- SERVER_COUNTRIES=Netherlands
- TZ=Europe/Paris
- VPN_PORT_FORWARDING_DOWN_COMMAND=/bin/sh -c 'wget -O- --retry-connrefused --post-data "json={\"listen_port\":0,\"current_network_interface\":\"lo"}" http://127.0.0.1:8080/api/v2/app/setPreferences 2>&1'
- VPN_PORT_FORWARDING_UP_COMMAND=/bin/sh -c 'wget -O- --retry-connrefused --post-data "json={\"listen_port\":{{PORT}},\"current_network_interface\":\"tun0\",\"random_port\":false,\"upnp\":false}" http://127.0.0.1:8080/api/v2/app/setPreferences 2>&1'
- VPN_PORT_FORWARDING=on
- VPN_SERVICE_PROVIDER=protonvpn
- VPN_TYPE=wireguard
- WIREGUARD_PRIVATE_KEY=${YOUR_PROTONVPN_PRIVATE_KEY}
ports:
- 8080:8080/tcp
restart: unless-stoppedProtonVPN-specific notes:
VPN_SERVICE_PROVIDER=protonvpnselects the ProtonVPN profile.WIREGUARD_PRIVATE_KEYis the private key from a generated ProtonVPN WireGuard configuration.SERVER_COUNTRIES=Netherlandskeeps the exit node selection readable and reproducible.VPN_PORT_FORWARDING=onenables ProtonVPN server-side port forwarding when your plan supports it.
The VPN_PORT_FORWARDING_UP_COMMAND and VPN_PORT_FORWARDING_DOWN_COMMAND hooks keep qBittorrent aligned with ProtonVPN’s dynamic forwarded port. When Gluetun receives a forwarded port, the UP hook updates qBittorrent’s listen_port to {{PORT}}, binds traffic to tun0, and disables random port behavior. If forwarding goes down, the DOWN hook resets qBittorrent to a safe fallback by dropping the listening port and switching the interface away from the tunnel endpoint, preventing stale inbound-port announcements.
If you prefer OpenVPN for ProtonVPN, replace the WireGuard settings with OPENVPN_USER and OPENVPN_PASSWORD.
See ProtonVPN provider setup for the full list of required keys and expected values.
Running qBittorrent Through Gluetun#
Once Gluetun is up, attach qBittorrent to it with the same network namespace.
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=wireguard
- WIREGUARD_PRIVATE_KEY=${YOUR_PROTONVPN_PRIVATE_KEY}
- SERVER_COUNTRIES=Netherlands
- TZ=Europe/Paris
ports:
- 8080:8080/tcp # expose the Web UI on the VPN container, not on qBittorrent for local access
qbittorrent:
image: lscr.io/linuxserver/qbittorrent:latest
network_mode: "service:gluetun"
depends_on:
- gluetun
environment:
- TZ=Europe/Paris
- WEBUI_PORT=8080This is the key idea: qBittorrent does not own its own outbound network path anymore. Gluetun does.
Validation Checklist#
After bringing the stack up, I validate in this order:
docker compose up -d
docker compose logs -f gluetun
docker exec -it gluetun sh -c 'wget -qO- https://ifconfig.me'
docker exec -it gluetun sh -c 'nslookup example.com'
docker exec -it qbittorrent sh -c 'wget -qO- https://ifconfig.me'What I want to see:
- Gluetun connects cleanly to the selected provider.
- The container reports the VPN exit IP, not the ISP IP.
- DNS resolution works from inside the tunnel.
- qBittorrent starts only after the VPN gateway is healthy.
- qBittorrent’s public IP matches the VPN exit IP, confirming that traffic is routed through Gluetun.
Troubleshooting#
If the tunnel does not come up, I check these first:
/dev/net/tunis mounted into the container.NET_ADMINis present incap_add.- The provider credentials are correct and match the selected VPN type.
- The VPN server filter is not too narrow.
- The routed app is not accidentally publishing ports on the host instead of on Gluetun.
If routing works but the app cannot reach LAN resources, add the appropriate subnet to FIREWALL_OUTBOUND_SUBNETS.
