Run WireGuard and WireGuard UI behind Traefik
Contents
This guide deploys a small VPN gateway on a public Ubuntu virtual machine. Traefik is the only container that publishes host ports:
- TCP
80and443for HTTPS and Let’s Encrypt; - UDP
51820for WireGuard; - WireGuard UI and the Traefik dashboard are available only over HTTPS.
This setup does not require a separate router or port forwarding. The virtual
machine must have a public IP address, and its cloud firewall must allow
incoming TCP 80, TCP 443, and UDP 51820 traffic.
Architecture
| Component | Purpose | Published through |
|---|---|---|
| Traefik | TLS termination, HTTP and UDP routing | Host ports 80, 443, and 51820/udp |
| WireGuard | VPN server | Traefik UDP router |
| WireGuard UI | Peer and server configuration | Traefik HTTPS router |
| WireGuard reloader | Applies changes made in the UI | Docker socket |
| Whoami | Verifies DNS, TLS, and HTTP routing | Traefik HTTPS router |
Prerequisites
Create three DNS records pointing to the server’s public IP address:
| Record | Purpose |
|---|---|
vpn.example.com |
WireGuard endpoint and test page |
wg.example.com |
WireGuard UI |
traefik.example.com |
Traefik dashboard |
Install Docker Engine and the Compose plugin. On a fresh Ubuntu server, you can use Docker’s official convenience script:
curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh get-docker.sh
sudo usermod -aG docker "$USER"Log out of the SSH session and reconnect after adding your user to the Docker group.
Files and secrets
Create .env.example with the public hostnames and initial credentials:
TZ=Etc/UTC
PUID=1000
PGID=1000
# All three A/AAAA records must point to this server.
WG_HOST=vpn.example.com
WG_UI_HOST=wg.example.com
TRAEFIK_HOST=traefik.example.com
ACME_EMAIL=admin@example.com
# Change these values before the first start.
WGUI_USERNAME=admin
WGUI_PASSWORD=change-me
SESSION_SECRET=replace-with-openssl-rand-hex-32Create compose.yaml:
name: wireguard
services:
traefik:
image: traefik:v3.6
container_name: traefik
restart: unless-stopped
security_opt:
- no-new-privileges:true
networks:
- proxy
command:
- --providers.docker=true
- --providers.docker.exposedbydefault=false
- --providers.docker.network=proxy
- --api.dashboard=true
- --api.insecure=false
- --entrypoints.web.address=:80
- --entrypoints.web.http.redirections.entrypoint.to=websecure
- --entrypoints.web.http.redirections.entrypoint.scheme=https
- --entrypoints.websecure.address=:443
- --entrypoints.wireguard.address=:51820/udp
- --certificatesresolvers.le.acme.email=${ACME_EMAIL}
- --certificatesresolvers.le.acme.storage=/acme/acme.json
- --certificatesresolvers.le.acme.httpchallenge.entrypoint=web
ports:
- 80:80
- 443:443
- 51820:51820/udp
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./traefik/auth:/etc/traefik/auth:ro
- ./acme:/acme
labels:
- traefik.enable=true
- traefik.http.routers.dashboard.rule=Host(`${TRAEFIK_HOST}`) && (PathPrefix(`/api`) || PathPrefix(`/dashboard`))
- traefik.http.routers.dashboard.entrypoints=websecure
- traefik.http.routers.dashboard.tls=true
- traefik.http.routers.dashboard.tls.certresolver=le
- traefik.http.routers.dashboard.service=api@internal
- traefik.http.routers.dashboard.middlewares=dashboard-auth
- traefik.http.middlewares.dashboard-auth.basicauth.usersfile=/etc/traefik/auth/dashboard.htpasswd
wireguard:
image: lscr.io/linuxserver/wireguard:latest
container_name: wireguard
restart: unless-stopped
cap_add:
- NET_ADMIN
- SYS_MODULE
networks:
- proxy
environment:
- PUID=${PUID}
- PGID=${PGID}
- TZ=${TZ}
- SERVERURL=${WG_HOST}
- SERVERPORT=51820
volumes:
- ./config:/config
- /lib/modules:/lib/modules:ro
sysctls:
- net.ipv4.conf.all.src_valid_mark=1
- net.ipv4.ip_forward=1
labels:
- traefik.enable=true
- traefik.docker.network=proxy
- traefik.udp.routers.wireguard.entrypoints=wireguard
- traefik.udp.routers.wireguard.service=wireguard
- traefik.udp.services.wireguard.loadbalancer.server.port=51820
wireguard-ui:
image: ngoduykhanh/wireguard-ui:latest
container_name: wireguard-ui
restart: unless-stopped
depends_on:
- wireguard
networks:
- proxy
environment:
- SESSION_SECRET=${SESSION_SECRET}
- WGUI_USERNAME=${WGUI_USERNAME}
- WGUI_PASSWORD=${WGUI_PASSWORD}
- WGUI_MANAGE_START=false
- WGUI_MANAGE_RESTART=false
volumes:
- ./db:/app/db
- ./config:/etc/wireguard
labels:
- traefik.enable=true
- traefik.docker.network=proxy
- traefik.http.routers.wg-ui.rule=Host(`${WG_UI_HOST}`)
- traefik.http.routers.wg-ui.entrypoints=websecure
- traefik.http.routers.wg-ui.tls=true
- traefik.http.routers.wg-ui.tls.certresolver=le
- traefik.http.services.wg-ui.loadbalancer.server.port=5000
wireguard-reloader:
image: docker:29-cli
container_name: wireguard-reloader
restart: unless-stopped
depends_on:
- wireguard
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- ./config:/config:ro
command:
- sh
- -c
- |
set -eu
file=/config/wg_confs/wg0.conf
last=""
while true; do
if [ -f "$$file" ]; then
current="$$(sha256sum "$$file" | awk '{print $$1}')"
if [ -n "$$last" ] && [ "$$current" != "$$last" ]; then
echo "wg0.conf changed, restarting wireguard"
sleep 2
docker restart wireguard
fi
last="$$current"
fi
sleep 5
done
whoami:
image: traefik/whoami:v1.11
container_name: whoami
restart: unless-stopped
networks:
- proxy
labels:
- traefik.enable=true
- traefik.docker.network=proxy
- traefik.http.routers.whoami.rule=Host(`${WG_HOST}`)
- traefik.http.routers.whoami.entrypoints=websecure
- traefik.http.routers.whoami.tls=true
- traefik.http.routers.whoami.tls.certresolver=le
- traefik.http.services.whoami.loadbalancer.server.port=80
networks:
proxy:
name: proxyCopy .env.example to .env, then prepare the persistent data directories:
cp .env.example .env
mkdir -p acme config db traefik/auth
touch acme/acme.json
chmod 600 acme/acme.jsonEdit .env and replace the example domains, Let’s Encrypt email address,
WireGuard UI username, and password. Check that PUID and PGID match the
output of id -u and id -g. Generate the session secret with:
openssl rand -hex 32Create a separate Basic Auth password for the Traefik dashboard:
read -s DASHBOARD_PASSWORD
docker run --rm httpd:2.4-alpine \
htpasswd -nbB admin "$DASHBOARD_PASSWORD" > traefik/auth/dashboard.htpasswd
unset DASHBOARD_PASSWORDImportant: Do not commit
.env,acme/acme.json, theconfiganddbdirectories, ordashboard.htpasswd.
Starting the stack
Validate the rendered configuration and start the services:
docker compose config --quiet
docker compose pull
docker compose up -d
docker compose psFollow the container startup and certificate issuance logs with:
docker compose logs -f traefik wireguard wireguard-uiOnce the DNS records have propagated, check the test service:
curl -I https://vpn.example.com/The following pages should now be available:
https://wg.example.comfor WireGuard UI;https://traefik.example.com/dashboard/for the Traefik dashboard. The trailing slash in/dashboard/is required.
Configuring WireGuard UI
Open Global Settings and enter:
| Setting | Value |
|---|---|
| Endpoint Address | vpn.example.com |
| Listen Port | 51820 |
| Server Interface | 10.252.1.1/24 |
| Config File Path | /etc/wireguard/wg_confs/wg0.conf |
Add the hooks below so VPN clients can access the Internet through the server.
The container’s outbound interface is named eth0 in this example. You can
verify it after startup with docker exec wireguard ip route show default.
Post Up: iptables -A FORWARD -i %i -j ACCEPT; iptables -A FORWARD -o %i -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
Post Down: iptables -D FORWARD -i %i -j ACCEPT; iptables -D FORWARD -o %i -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADECreate a client with an address such as 10.252.1.2/32. Use the following
client settings to route all IPv4 traffic through the VPN:
Allowed IPs: 0.0.0.0/0
DNS: 1.1.1.1Save the configuration, then download the client configuration file or scan its
QR code. wireguard-reloader detects changes to wg0.conf and restarts the
WireGuard container. To apply changes manually, run:
docker compose restart wireguardDisplay the tunnel status and latest handshake time with:
docker exec wireguard wg showHow traffic flows
Traefik terminates TLS for both web interfaces. Its Docker provider discovers
the HTTP services from their labels. WireGuard uses a separate UDP entry point:
datagrams received on host port 51820 are forwarded to port 51820 in the
WireGuard container.
Traefik does not configure the VPN and is not a DNS server. WireGuard UI writes
wg0.conf, while the Linux kernel and iptables handle routing and NAT.
Updates, backups, and security
Update the images and recreate the containers with:
docker compose pull
docker compose up -dBack up .env, the config and db directories, acme/acme.json, and
traefik/auth/dashboard.htpasswd.
The wireguard-reloader service mounts /var/run/docker.sock, which effectively
grants it broad control over Docker. This is a convenient tradeoff for a small
personal server. In a stricter environment, use a Docker socket proxy or a
host-side systemd watcher instead.
For additional protection, restrict the administration domains by source IP, place them behind an identity-aware proxy, or make them accessible only through the VPN. The Traefik dashboard is protected by Basic Auth in this example, while WireGuard UI uses its own authentication.