thenug.net

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

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.


← back to blog