How to Route a Container Through a VPN with Gluetun (Privacy for One App, Not the Whole Server)

One container, through the VPN; everything else untouched. Gluetun: a kill switch, a provider, a network to join.

Sometimes one app should go out through a commercial VPN — a scraper or changedetection watch (tutorial 135) a site keeps blocking, an RSS fetcher, a download client, anything whose traffic you'd rather not tie to your home IP — while the rest of the server (Pi-hole, backups, Jellyfin) stays on the normal connection. Gluetun is a container that connects to your VPN provider; any other container you attach to it sends all its traffic through that tunnel, with a kill switch so nothing leaks if the VPN drops.

Quick reference

TaskWhat
Containerqmcgaw/gluetun with NET_ADMIN and /dev/net/tun
ProviderVPN_SERVICE_PROVIDER (mullvad, protonvpn, airvpn, nordvpn, surfshark, custom…)
ProtocolVPN_TYPE: wireguard (fast) or openvpn
Attach an appnetwork_mode: "service:gluetun" on the app
Reach the app's UIpublish its port on gluetun, not the app
Checkdocker compose exec gluetun wget -qO- https://ipinfo.io

1. Get your VPN details

From your provider's account page, generate a WireGuard configuration (Mullvad, Proton, AirVPN, Surfshark, NordVPN and most others offer one). You need the private key, the address, and for some providers a preshared key. Gluetun's wiki lists exact variables per provider.

2. The compose file

mkdir -p ~/docker/vpn && cd ~/docker/vpn
nano compose.yaml

services:
gluetun:
image: qmcgaw/gluetun:latest
container_name: gluetun
cap_add:
- NET_ADMIN
devices:
- /dev/net/tun:/dev/net/tun
ports:
- "127.0.0.1:5001:5000" # ports of ATTACHED apps are published here
environment:
VPN_SERVICE_PROVIDER: mullvad
VPN_TYPE: wireguard
WIREGUARD_PRIVATE_KEY: ${WG_PRIVATE_KEY}
WIREGUARD_ADDRESSES: 10.64.0.2/32
SERVER_COUNTRIES: Netherlands
TZ: Europe/London
FIREWALL_OUTBOUND_SUBNETS: 192.168.1.0/24
HEALTH_VPN_DURATION_INITIAL: 30s
restart: unless-stopped

changedetection:
image: ghcr.io/dgtlmoon/changedetection.io:latest
container_name: changedetection-vpn
network_mode: "service:gluetun"
volumes:
- ./cd-data:/datastore
depends_on:
gluetun:
condition: service_healthy
restart: unless-stopped

The attached app has no ports: and no networks: — it shares Gluetun's network entirely. Its web port is published on the Gluetun service instead. FIREWALL_OUTBOUND_SUBNETS lets the attached app still talk to your LAN (for ntfy at home, say); without it, only the tunnel is allowed.

docker compose up -d
docker compose logs gluetun | tail

"Initialization sequence completed" / "healthy" means the tunnel is up.

3. Prove it

docker compose exec gluetun wget -qO- https://ipinfo.io
docker compose exec changedetection-vpn wget -qO- https://ipinfo.io

Both should show the VPN's address and country, not yours. Then on the host, curl ifconfig.me still shows your home IP — the rest of the server is unaffected.

4. The kill switch

Gluetun's firewall allows traffic only through the tunnel. If the VPN drops, attached containers lose the internet rather than leaking; Gluetun reconnects and they resume. docker compose exec gluetun wget -qO- https://ipinfo.io during a reconnect simply fails, which is the behaviour you want.

5. More than one app

Any number of services can use network_mode: "service:gluetun". Each one's port is published on Gluetun's ports: list. They see each other on localhost (they share a network namespace), so an app and its own database both attached can use localhost:5432.

6. Extras

Troubleshooting

Attached container can't start: "network_mode: service:gluetun" error. Gluetun must be in the same compose file (or use container:gluetun), and it must be healthy first — the depends_on condition handles ordering.

UI port doesn't respond. You published it on the app, not on Gluetun. Move it.

Attached app can't reach ntfy/Pi-hole on the LAN. FIREWALL_OUTBOUND_SUBNETS with your LAN range.

DNS inside attached containers fails. Gluetun uses the provider's DNS over the tunnel by default (DOT: on); if the provider blocks it, set DNS_ADDRESS to 1.1.1.1.

Changed provider details, still the old server. Recreate: docker compose up -d --force-recreate gluetun.

Wrapping up

One Gluetun container with the provider's WireGuard details, network_mode: "service:gluetun" on the apps that need privacy, their ports published on Gluetun. Those apps leave through the VPN with a kill switch; Pi-hole, backups and everything else carry on exactly as before.

Call us