Gluetun
A VPN client in a container — route other containers through WireGuard or OpenVPN.
The stack
Generated output
services:
gluetun:
image: docker.io/qmcgaw/gluetun:v3.41.1@sha256:1a5bf4b4820a879cdf8d93d7ef0d2d963af56670c9ebff8981860b6804ebc8ab
restart: unless-stopped
environment:
VPN_SERVICE_PROVIDER: ${VPN_SERVICE_PROVIDER}
VPN_TYPE: wireguard
WIREGUARD_PRIVATE_KEY: ${WIREGUARD_PRIVATE_KEY}
WIREGUARD_ADDRESSES: ${WIREGUARD_ADDRESSES}
SERVER_COUNTRIES: ${SERVER_COUNTRIES}
TZ: ${TZ}
volumes:
- gluetun-data:/gluetun
cap_add:
- NET_ADMIN
devices:
- /dev/net/tun:/dev/net/tun
volumes:
gluetun-data: {}
# Caddyfile for Gluetun
# Replace gluetun.local with your real domain. Caddy issues TLS automatically.
gluetun.local {
encode zstd gzip
reverse_proxy gluetun:8000
}
# Traefik labels for Gluetun
# Merge into the gluetun service in docker-compose.yml.
# Assumes an external Traefik network named "proxy" and a
# certresolver named "letsencrypt". Replace gluetun.local.
services:
gluetun:
labels:
traefik.enable: "true"
traefik.http.routers.gluetun.rule: Host(`gluetun.local`)
traefik.http.routers.gluetun.entrypoints: websecure
traefik.http.routers.gluetun.tls: "true"
traefik.http.routers.gluetun.tls.certresolver: letsencrypt
traefik.http.services.gluetun.loadbalancer.server.port: "8000"
networks:
- proxy
- default
networks:
proxy:
external: true
Replace gluetun.local with your domain. Generate secrets below.
Environment
.env with generated secrets
Secrets are generated in your browser via crypto.getRandomValues. Nothing is sent anywhere.
Server-rendered .env template
# The VPN provider Gluetun should connect to. Must be one of the names Gluetun supports, for example mullvad, protonvpn, ivpn, airvpn, or nordvpn. VPN_SERVICE_PROVIDER=mullvad # The WireGuard client private key issued by your VPN provider. This is not generated here — paste the value from the provider WireGuard configuration. Treat it as a credential. WIREGUARD_PRIVATE_KEY= # The client address your provider assigns in its WireGuard configuration, in CIDR form. Not a secret, but specific to your account. WIREGUARD_ADDRESSES=10.64.0.2/32 # Optional. Restrict server selection to one or more countries (comma-separated). Leave it empty to let Gluetun choose. Provider support varies; some use cities or hostnames instead. SERVER_COUNTRIES=Switzerland # Timezone for log timestamps (IANA tz name). TZ=UTC
About
What is Gluetun?
Gluetun is a VPN client that runs as a container. Instead of configuring a VPN on the host, you point Gluetun at a supported provider — Mullvad, ProtonVPN, IVPN, AirVPN, NordVPN, and dozens more — over WireGuard or OpenVPN, and then route other containers through it. This is the standard way to guarantee that a download client, a scraper, or any service only ever reaches the internet through the tunnel: in their own compose definition those containers set `network_mode: "service:gluetun"` and share the Gluetun network namespace, so if the VPN drops their traffic stops with it — a built-in kill switch. Gluetun needs two things the kernel does not grant ordinary containers: the `NET_ADMIN` capability, to create and manage the tunnel interface, and the `/dev/net/tun` device. Both are present in this compose; without them the container cannot bring up a VPN and exits on boot. One thing to be clear about: Gluetun is not a web application and has no dashboard. It is not meant to sit behind a reverse proxy, so the generated Caddyfile and Traefik tabs on this page do not apply to it. There is no public hostname pointing at Gluetun, and the `gluetun.local` value above is only a placeholder this template requires. Gluetun does run a small control server on port 8000 for health and status queries, but that is an internal management API, not a public endpoint, and this stack does not publish it. The credentials below come from your VPN provider, not from any generator.
Requirements
Before you start
- An active subscription with a VPN provider Gluetun supports (Mullvad, ProtonVPN, IVPN, AirVPN, NordVPN, and many more), plus the WireGuard key and address that provider assigns you.
- The `NET_ADMIN` capability for the container — set via `cap_add` in this compose.
- The `/dev/net/tun` device present on the host and passed into the container — also handled in this compose.
- Docker 24+ and Docker Compose v2.
Deploy
How to deploy
- Generate a WireGuard configuration in your VPN provider dashboard, then copy the private key and the assigned client address.
- In `.env`, set `VPN_SERVICE_PROVIDER`, paste `WIREGUARD_PRIVATE_KEY` and `WIREGUARD_ADDRESSES`, and optionally pin `SERVER_COUNTRIES`. Do not use the "Generate secrets" button for the key — it must be the real value from your provider.
- Start the stack: `docker compose up -d`, then watch `docker compose logs -f gluetun` for the line confirming the tunnel is up and reporting the new public IP.
- Route another container through the tunnel by adding `network_mode: "service:gluetun"` to it in the same compose project, removing its own `networks`, and publishing its ports on the gluetun service instead.
- Confirm the exit IP from inside the container: `docker compose exec gluetun wget -qO- https://ipinfo.io/ip` should print the VPN address, not your home address.
Errors
Common errors & fixes
Gluetun exits immediately with "TUN device is not available" or "operation not permitted".
The container is missing `NET_ADMIN` or `/dev/net/tun`. Both are required and are in this compose. If the host kernel has no tun module loaded, run `sudo modprobe tun` and retry.
The VPN authenticates in the logs, but a container routed through Gluetun has no internet.
A container using `network_mode: "service:gluetun"` must not also declare its own `networks`; it inherits the Gluetun stack. Publish the ports of that container on the gluetun service, not on the routed container itself, because only Gluetun owns the shared network namespace.
Authentication fails or the tunnel never establishes after pasting the key.
The wrong value was likely used. `WIREGUARD_PRIVATE_KEY` is your client private key from the provider, and `WIREGUARD_ADDRESSES` is the client address it assigned, such as `10.64.0.2/32` — not the server public key or endpoint. Regenerate the configuration in the provider dashboard and copy both values exactly.
After a reboot, routed containers start before the VPN is ready and fail or leak.
Make the routed containers wait on Gluetun health: add `depends_on` with a `condition: service_healthy` on gluetun, which has a built-in healthcheck that only passes once the tunnel is up.
Limitations
Honest limitations
- Gluetun is not a web service and has no UI, so the Caddyfile and Traefik output generated on this page do not apply. There is nothing to reverse-proxy, and `gluetun.local` is only a placeholder this template requires.
- The control server on port 8000 is an internal status and management API, not a public endpoint. This stack does not publish it; expose it to localhost only if you need it.
- You need a paid subscription with a supported provider. Gluetun is only the client; it does not provide VPN service itself.
- The WireGuard key and address come from your provider and cannot be generated here, so the "Generate secrets" button does not apply to them.
- Routing other containers through Gluetun ties their network to its lifecycle: when Gluetun restarts, their connectivity drops briefly — which is also exactly what makes it a kill switch.
FAQ
Frequently asked
Why can the secrets button not generate WIREGUARD_PRIVATE_KEY?+
Because it is not a random secret you mint. It is the private key your VPN provider issued for your account, paired with a public key on their server, so a random value simply fails to authenticate. Copy the real key from the provider WireGuard configuration and leave the generator unused for this field.
How do I send another container traffic through the VPN?+
In the same compose project, give that container `network_mode: "service:gluetun"` and remove its own `ports` and `networks`. It then shares the Gluetun network stack, and you publish its ports on the gluetun service instead. If the tunnel drops, that container loses internet too — the built-in kill switch.
Does Gluetun belong behind the reverse proxy like the other stacks?+
No. Gluetun has no web UI and is not meant to be proxied, so ignore the Caddyfile and Traefik tabs for this stack. The only listening service is the control server on port 8000, which is an internal API you would expose to localhost at most.
WireGuard or OpenVPN?+
Prefer WireGuard where the provider supports it: it is faster, simpler, and has a smaller attack surface. This stack is configured for WireGuard via `VPN_TYPE=wireguard`; switch to OpenVPN by changing that value and supplying the OpenVPN credentials the provider documents.
How do I confirm the VPN works and is not leaking?+
Check `docker compose logs gluetun` for the public-IP line at startup, or query the control server with `docker compose exec gluetun wget -qO- http://127.0.0.1:8000/v1/publicip/ip`. It should report the VPN address. Gluetun also restarts itself through its built-in healthcheck if the tunnel goes down.
Related