VPN-routed download clients: qBittorrent and SABnzbd behind Gluetun with Docker Compose.
Last updated: Oct 2026
This stack runs your download clients behind a VPN. Gluetun opens the VPN tunnel and owns the network, and qBittorrent (torrents) and SABnzbd (Usenet) join it with network_mode: service:gluetun. If the VPN drops, the clients lose internet access instead of leaking your real IP.
Why you'd want it: A kill switch by design, automatic port forwarding for better torrent speeds, and one place to manage VPN settings.
| Item | Value |
|---|---|
| Stack folder | /opt/stacks/arr_download |
| Category | Downloads |
| Main image | lscr.io/linuxserver/qbittorrent:latest |
| Services | 3 |
| Web UI | qBittorrent: http://<SERVER_IP>:8090, SABnzbd: http://<SERVER_IP>:8084 |
| Official docs | https://github.com/qdm12/gluetun-wiki |
.env/.secret files from Docker Folders and Environment/dev/net/tun available on the hostFolder layout:
/opt/stacks/
├── _shared/
│ ├── .env # shared, non-secret settings
│ └── .secret # passwords, tokens and keys (chmod 600, never commit)
└── arr_download/
└── compose.yaml
/opt/appdata/arr_download/ # persistent application data
/opt/stacks/arr_download/compose.yaml:
networks:
media-network:
name: media-network
driver: bridge
services:
gluetun:
image: qmcgaw/gluetun:v3
container_name: gluetun
cap_add:
- NET_ADMIN
devices:
- /dev/net/tun:/dev/net/tun
environment:
- TZ=${TZ:?err}
- UPDATER_PERIOD=24h
- VPN_SERVICE_PROVIDER=protonvpn
- VPN_TYPE=${VPN_TYPE:?err}
- BLOCK_MALICIOUS=off
- OPENVPN_USER=${OPENVPN_USERNAME:?err}+pmp
- OPENVPN_PASSWORD=${OPENVPN_PASSWORD:?err}
- OPENVPN_CIPHERS=AES-256-GCM
- WIREGUARD_PRIVATE_KEY=${WIREGUARD_PRIVATE_KEY:?err}
- PORT_FORWARD_ONLY=on
- VPN_PORT_FORWARDING=on
- VPN_PORT_FORWARDING_UP_COMMAND=/bin/sh -c 'wget -O- --retry-connrefused --post-data "json={\"listen_port\":{{PORTS}}}" http://127.0.0.1:8090/api/v2/app/setPreferences 2>&1'
- SERVER_COUNTRIES=${SERVER_COUNTRIES:?err}
- SHADOWSOCKS=on
- FIREWALL_OUTBOUND_SUBNETS=<LAN_SUBNET> # <-- Your actual LAN subnet
# Optional: Use Docker DNS for inter-container hostname resolution
#- DNS_ADDRESS=127.0.0.11
logging:
driver: json-file
options:
max-file: ${DOCKERLOGGING_MAXFILE:?err} # Max number of log files
max-size: ${DOCKERLOGGING_MAXSIZE:?err} # Max file size
volumes:
- ${DOCKERFOLDER:?err}/gluetun/config:/gluetun
ports:
- 6881:6881/tcp # Transmission/Deluge: Torrent Port TCP
- 6881:6881/udp # Transmission/Deluge: Torrent Port UDP
- 8084:8080 # WebUI Portal: SABnzbd
- 8090:8090/tcp # WebUI Portal: qBittorrent
#- 8112:8112 # WebUI Portal: Deluge
#- 9091:9091 # Transmission web UI
#- 9090:9090 # SABnzbd: API port
#- 51413:51413 # Transmission peer port (TCP)
#- 51413:51413/udp # Transmission peer port (UDP)
#- 58846:58846 # Deluge RPC
- 8888:8888/tcp # Gluetun Local Network HTTP proxy
- 8388:8388/tcp # Gluetun Local Network Shadowsocks
- 8388:8388/udp # Gluetun Local Network Shadowsocks
networks:
- media-network
restart: unless-stopped
sysctls:
- net.ipv6.conf.all.disable_ipv6=1
qbittorrent:
image: lscr.io/linuxserver/qbittorrent:latest
container_name: qbittorrent
depends_on:
gluetun:
condition: service_healthy
environment:
- PUID=${PUID:?err} # User ID
- PGID=${PGID:?err} # Group ID
- TZ=${TZ:?err} # TimeZone
- UMASK=${UMASK:?err} # File Umask
- WEBUI_PORT=8090
logging:
driver: json-file
options:
max-file: ${DOCKERLOGGING_MAXFILE:?err} # Max number of log files
max-size: ${DOCKERLOGGING_MAXSIZE:?err} # Max file size
volumes:
- ${DOCKERFOLDER:?err}/qbittorrent/config:/config
- ${TORRENTDOWNLOAD:?err}:/data/torrents
- ${MEDIAFOLDER:?err}:/data/media
network_mode: "service:gluetun"
# Optional: Use extra_hosts for specific local services if needed
# extra_hosts:
# - "my-nas:<SERVER_IP>"
# ... other app settings
restart: unless-stopped
deploy:
resources:
limits:
cpus: '2.0'
memory: 2000M
sabnzbd:
image: lscr.io/linuxserver/sabnzbd:latest
container_name: sabnzbd
depends_on:
gluetun:
condition: service_healthy
environment:
- PUID=${PUID:?err} # User ID
- PGID=${PGID:?err} # Group ID
- TZ=${TZ:?err} # TimeZone
- UMASK=${UMASK:?err} # File Umask
- DOCKER_MODS=ghcr.io/gilbn/theme.park:sabnzbd
- TP_THEME=${TP_THEME:?err}
logging:
driver: json-file
options:
max-file: ${DOCKERLOGGING_MAXFILE:?err} # Max number of log files
max-size: ${DOCKERLOGGING_MAXSIZE:?err} # Max file size
volumes:
- ${DOCKERFOLDER:?err}/sabnzbd:/config
- ${USENETDOWNLOAD:?err}:/data/usenet
- ${MEDIAFOLDER:?err}:/data/media
network_mode: "service:gluetun"
# Optional: Use extra_hosts for specific local services if needed
# extra_hosts:
# - "my-nas:<SERVER_IP>"
# ... other app settings
restart: unless-stopped
deploy:
resources:
limits:
cpus: '2.0'
memory: 2000M
Compose reads these from the shared files with --env-file. Replace every <PLACEHOLDER> with your own value.
/opt/stacks/_shared/.env:
PUID=1000
PGID=1000
TZ=<TZ>
VPN_TYPE=wireguard
SERVER_COUNTRIES=<COUNTRY_1>,<COUNTRY_2>
DOCKERLOGGING_MAXFILE=10
DOCKERLOGGING_MAXSIZE=200k
DOCKERFOLDER=/opt/appdata
UMASK=002
TORRENTDOWNLOAD=/srv/data/torrents
MEDIAFOLDER=/srv/media
TP_THEME=organizr
USENETDOWNLOAD=/srv/data/usenet
/opt/stacks/_shared/.secret:
OPENVPN_USERNAME=<VPN_USERNAME>
OPENVPN_PASSWORD=<VPN_PASSWORD>
WIREGUARD_PRIVATE_KEY=<WIREGUARD_PRIVATE_KEY>
| Variable | File | Purpose | Example |
|---|---|---|---|
PUID |
.env |
User ID the container runs as (see Docker Folders) | 1000 |
PGID |
.env |
Group ID the container runs as | 1000 |
TZ |
.env |
Time zone in tz database format, e.g. America/New_York | <TZ> |
VPN_TYPE |
.env |
Gluetun VPN protocol: openvpn or wireguard | wireguard |
OPENVPN_USERNAME |
.secret |
VPN provider OpenVPN username | <VPN_USERNAME> |
OPENVPN_PASSWORD |
.secret |
VPN provider OpenVPN password | <VPN_PASSWORD> |
WIREGUARD_PRIVATE_KEY |
.secret |
VPN provider WireGuard private key | <WIREGUARD_PRIVATE_KEY> |
SERVER_COUNTRIES |
.env |
Comma-separated VPN server countries | <COUNTRY_1>,<COUNTRY_2> |
DOCKERLOGGING_MAXFILE |
.env |
Number of rotated container log files to keep | 10 |
DOCKERLOGGING_MAXSIZE |
.env |
Maximum size of each container log file | 200k |
DOCKERFOLDER |
.env |
Base folder for persistent app data | /opt/appdata |
UMASK |
.env |
File creation mask for new files | 002 |
TORRENTDOWNLOAD |
.env |
Torrent download folder | /srv/data/torrents |
MEDIAFOLDER |
.env |
Root of your media library (movies, tv, music, books) | /srv/media |
TP_THEME |
.env |
theme.park theme name for SABnzbd | organizr |
USENETDOWNLOAD |
.env |
Usenet download folder | /srv/data/usenet |
| Host port | Container port | Protocol | Service | Purpose |
|---|---|---|---|---|
| 6881 | 6881 | tcp | gluetun |
Transmission/Deluge: Torrent Port TCP |
| 6881 | 6881 | udp | gluetun |
Transmission/Deluge: Torrent Port UDP |
| 8084 | 8080 | tcp | gluetun |
WebUI Portal: SABnzbd |
| 8090 | 8090 | tcp | gluetun |
WebUI Portal: qBittorrent |
| 8888 | 8888 | tcp | gluetun |
Gluetun Local Network HTTP proxy |
| 8388 | 8388 | tcp | gluetun |
Gluetun Local Network Shadowsocks |
| 8388 | 8388 | udp | gluetun |
Gluetun Local Network Shadowsocks |
Apps with
network_mode: service:<gluetun>share the VPN container's network, so their ports are published on the Gluetun service.
| Host path | Container path | Service | Purpose |
|---|---|---|---|
${DOCKERFOLDER}/gluetun/config |
/gluetun |
gluetun |
Application configuration |
${DOCKERFOLDER}/qbittorrent/config |
/config |
qbittorrent |
Application configuration |
${TORRENTDOWNLOAD} |
/data/torrents |
qbittorrent |
Downloads |
${MEDIAFOLDER} |
/data/media |
qbittorrent |
Media library |
${DOCKERFOLDER}/sabnzbd |
/config |
sabnzbd |
Application configuration |
${USENETDOWNLOAD} |
/data/usenet |
sabnzbd |
Downloads |
${MEDIAFOLDER} |
/data/media |
sabnzbd |
Media library |
Recommended limits, already set in the compose file under deploy.resources. Raise them if the app is starved, or lower them on small hosts.
| Service | CPU limit | Memory limit | Memory reservation | GPU |
|---|---|---|---|---|
qbittorrent |
2.0 | 2000M | - | - |
sabnzbd |
2.0 | 2000M | - | - |
sudo mkdir -p /opt/stacks/arr_download /opt/appdata/arr_download
sudo chown -R $USER: /opt/stacks/arr_download
/opt/stacks/_shared/.env and /opt/stacks/_shared/.secret (chmod 600 .secret)./opt/stacks/arr_download/compose.yaml.cd /opt/stacks/arr_download
docker compose --env-file ../_shared/.env --env-file ../_shared/.secret config --quiet
docker compose --env-file ../_shared/.env --env-file ../_shared/.secret up -d
docker compose logs -f.docker logs gluetun.docker exec gluetun wget -qO- https://ipinfo.io/ip. It must not be your home IP.docker logs qbittorrent. Change it right away./data/torrents and /data/usenet.Tip: create a small wrapper such as
alias dc='docker compose --env-file /opt/stacks/_shared/.env --env-file /opt/stacks/_shared/.secret'so every stack uses the same env files.
cd /opt/stacks/arr_download
docker compose --env-file ../_shared/.env --env-file ../_shared/.secret pull
docker compose --env-file ../_shared/.env --env-file ../_shared/.secret up -d
docker image prune -f
Some images here are pinned to a version. Read the upstream release notes before changing the tag.
Back up /opt/appdata/arr_download before major upgrades. Watchtower can automate updates for stateless apps.
docker compose ps and docker compose logs --tail=100.FIREWALL_OUTBOUND_SUBNETS to your LAN subnet, for example 192.168.1.0/24.VPN_PORT_FORWARDING_UP_COMMAND pushes the forwarded port to the qBittorrent API, so the URL must use the qBittorrent Web UI port (8090 here). In qBittorrent, enable Bypass authentication for clients on localhost.+pmp suffix on the OpenVPN user enables port forwarding.:?err): run the config command above to see which variable is not set in your env files.Educational example. Review every path, port and credential before you deploy, and never publish your .secret file.