container network namespacing with gluetun
running a torrent client behind a VPN in Docker is simple in principle but awkward in practice. the cleanest approach is network namespace sharing — one container owns the VPN connection, another shares it entirely.
the naive approach and why it's bad
the obvious way to VPN a container is to run openvpn or wireguard on the host, then route container traffic through it. it works but it's fragile — VPN dies, traffic leaks to the real IP. you'd need kill switch rules at the host firewall level, which is extra configuration that has to be maintained.
another common approach is running the VPN client inside the same container as the app. this means building custom images or using community ones that bundle both. when either component needs updating you're updating the whole image.
gluetun
Gluetun is a VPN client container that exposes its network to other containers. it handles the kill switch internally — if the VPN tunnel drops, the network interface goes down and no traffic passes.
it supports most providers (Mullvad, NordVPN, ExpressVPN, etc.) over WireGuard or OpenVPN.
network_mode: container
Docker lets a container share another container's network namespace entirely with network_mode: "container:<name>". the second container has no network stack of its own — it uses the first container's interfaces, IP, and routing tables.
services:
gluetun:
image: qmcgaw/gluetun
cap_add:
- NET_ADMIN
environment:
- VPN_SERVICE_PROVIDER=mullvad
- VPN_TYPE=wireguard
- WIREGUARD_PRIVATE_KEY=...
- SERVER_COUNTRIES=USA
ports:
- "8080:8080" # qBittorrent web UI exposed here, on gluetun's network
qbittorrent:
image: lscr.io/linuxserver/qbittorrent
network_mode: "container:gluetun"
# no ports here — they're declared on gluetun above
qBittorrent has no ports declared on its own service — they're declared on gluetun, because that's the network stack both containers share. from the outside, port 8080 on the host reaches gluetun, which reaches qBittorrent through the shared namespace.
what this means in practice
- if gluetun isn't running, qBittorrent has no network at all — it can't even start properly
- if the VPN tunnel drops, gluetun's kill switch removes the network interface — qBittorrent immediately loses connectivity
- no leak is possible because there's no fallback route
verification is simple:
docker exec qbittorrent curl https://am.i.mullvad.net/json
# mullvad_exit_ip: true
stack ordering
since qBittorrent depends on gluetun's network namespace, gluetun must be up before qBittorrent starts. if gluetun restarts (e.g. config change, container update), qBittorrent loses its network namespace and needs to be restarted too. the namespace is tied to the container ID, not just the container name.
in practice: restart gluetun → restart the app stack. it's a known quirk of this approach.
one gotcha with server names
Gluetun is picky about country/server name formatting. SERVER_COUNTRIES=USA works. SERVER_COUNTRIES=United States does not — it silently fails to connect. check the Gluetun docs for your provider's exact accepted values.
eli5
imagine a bodyguard (Gluetun) who has a private tunnel out of town (VPN). a delivery truck (qBittorrent) is only allowed to use the bodyguard's tunnel — it has no other way out. they share the same ID to the outside world, so all the truck's trips look like they came from the bodyguard's location. if the tunnel ever closes, the truck stops completely — it can't sneak out the regular road because it literally doesn't have one. this is the key difference from other approaches: there's no fallback route that could expose the real location. the truck either goes through the tunnel or doesn't go at all.