Skip to main content

Route qBittorrent Traffic Through Gluetun with AirVPN or ProtonVPN

·970 words·5 mins·
Photo by Yuki Ho on Unsplash

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:

  1. Run one Gluetun container per VPN profile.
  2. Attach application containers to Gluetun with network_mode: "service:gluetun".
  3. Expose ports on the Gluetun service, not on the application container.
  4. 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-stopped

The 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-stopped

AirVPN-specific notes:

  • VPN_SERVICE_PROVIDER=airvpn selects the provider profile.
  • VPN_TYPE=wireguard tells Gluetun to use WireGuard instead of OpenVPN.
  • WIREGUARD_PRIVATE_KEY, WIREGUARD_PRESHARED_KEY, and WIREGUARD_ADDRESSES come from AirVPN’s client area or config generator.
  • SERVER_COUNTRIES=Netherlands is 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-stopped

ProtonVPN-specific notes:

  • VPN_SERVICE_PROVIDER=protonvpn selects the ProtonVPN profile.
  • WIREGUARD_PRIVATE_KEY is the private key from a generated ProtonVPN WireGuard configuration.
  • SERVER_COUNTRIES=Netherlands keeps the exit node selection readable and reproducible.
  • VPN_PORT_FORWARDING=on enables 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=8080

This 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:

  1. /dev/net/tun is mounted into the container.
  2. NET_ADMIN is present in cap_add.
  3. The provider credentials are correct and match the selected VPN type.
  4. The VPN server filter is not too narrow.
  5. 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.

References
#