What wg-easy Does
WireGuard itself is just a kernel interface plus a small config file per peer. Running it by hand means generating key pairs, picking a tunnel address for every device and copying configs around (that manual route is covered in WireGuard Cloud Deployment). wg-easy automates that bookkeeping behind a browser UI. The project README lists, among other things:
- Create, edit, enable, disable and delete clients
- A QR code per client for mobile apps, and a downloadable
.conffile for desktops - Connection status and per-client transfer charts
- Client expiry dates and one-time download links
- Two-factor authentication and optional OIDC sign-in for the admin panel
- Prometheus metrics and per-client firewall filtering (needs iptables on the host)
The trade-off is that you now run a web application with full control over your VPN. Most of this guide's security advice is about that admin panel, not about WireGuard.
v15 Is a Rewrite: Read This If You Used v14
Older wg-easy guides (including the earlier version of this page) configure everything with environment variables such as WG_HOST and a PASSWORD variable. Those belong to v14. According to the official v14 to v15 migration guide:
- v15 is a complete rewrite and doesn't use the v14 environment variables. Most settings moved into the Admin Panel in the web UI.
- If you reach the web UI over plain HTTP, you must set
INSECUREtotrue. - armv6 and armv7 hosts can't move to v15. Supported architectures are x86_64 and arm64.
- To migrate, download the
wg0.jsonbackup from v14 first, then upload it in the v15 setup wizard ("Do you have an existing setup?").
The image tags matter too. The project's Getting Started page says the latest tag still points to v14 and should be avoided, and recommends pinning the major version tag 15, which gets new features and fixes without breaking changes.
Prerequisites
- A Linux host you control (a VPS or a machine at home) on x86_64 or arm64.
- Docker Engine with the Compose plugin. If you don't have it yet, follow the Docker Installation Guide.
- A public IP address or a DNS name that points to it. If your home connection has a changing IP, use a dynamic DNS name. If you're behind carrier-grade NAT, you can't accept inbound connections at all; a mesh VPN such as Tailscale works around that.
- UDP port 51820 reachable from the internet: open it in the host firewall and in your cloud provider's firewall or security group, or forward it on your home router.
- For the HTTPS setup below, a domain name for the admin panel and a reverse proxy. The example uses Nginx; the Nginx Web Server Setup guide covers installing it and getting a certificate.
Deploy with Docker Compose
The official Basic Installation tutorial keeps the files in /etc/docker/containers/wg-easy and downloads the project's compose file:
sudo mkdir -p /etc/docker/containers/wg-easy
sudo curl -o /etc/docker/containers/wg-easy/docker-compose.yml https://raw.githubusercontent.com/wg-easy/wg-easy/master/docker-compose.yml
cd /etc/docker/containers/wg-easy
At the time of writing that file looks like this (comments trimmed). Review it before you start the container rather than trusting a copy on any third-party page, including this one:
volumes:
etc_wireguard:
services:
wg-easy:
#environment:
# - PORT=51821
# - HOST=0.0.0.0
# - INSECURE=false
image: ghcr.io/wg-easy/wg-easy:15
container_name: wg-easy
networks:
wg:
ipv4_address: 10.42.42.42
ipv6_address: fdcc:ad94:bacf:61a3::2a
volumes:
- etc_wireguard:/etc/wireguard
- /lib/modules:/lib/modules:ro
ports:
- "51820:51820/udp"
- "51821:51821/tcp"
restart: unless-stopped
cap_add:
- NET_ADMIN
- SYS_MODULE
sysctls:
- net.ipv4.ip_forward=1
- net.ipv4.conf.all.src_valid_mark=1
- net.ipv6.conf.all.disable_ipv6=0
- net.ipv6.conf.all.forwarding=1
- net.ipv6.conf.default.forwarding=1
networks:
wg:
driver: bridge
enable_ipv6: true
ipam:
driver: default
config:
- subnet: 10.42.42.0/24
- subnet: fdcc:ad94:bacf:61a3::/64
Versions: this page was written against wg-easy v15. The newest release on the wg-easy releases page when it was updated was v15.5.0. The compose file uses the major tag 15, so docker compose pull picks up 15.x releases. If you'd rather control every update, pin an exact tag such as ghcr.io/wg-easy/wg-easy:15.5.0 and change it after reading the release notes. There is no top-level version: key; the Compose specification treats it as obsolete.
What the pieces do:
- 51820/udp is the WireGuard port your clients connect to. 51821/tcp is the web UI.
- etc_wireguard is a named volume mounted at
/etc/wireguard. It holds all of wg-easy's state, including the server and client keys. Back it up (see below). - NET_ADMIN and SYS_MODULE let the container create the WireGuard interface and load the kernel module. The read-only
/lib/modulesmount serves the same purpose. - The sysctls turn on IP forwarding inside the container so client traffic can be routed onward.
Optional environment variables
The optional configuration page documents only a handful of variables:
PORT(default51821): TCP port of the web UI.HOST(default0.0.0.0): address the web UI binds to inside the container.INSECURE(defaultfalse): allow the web UI over plain HTTP. Leave it off if you use a reverse proxy with TLS.DISABLE_IPV6andDISABLE_VERSION_CHECK(both defaultfalse).
For automated deployments there are also INIT_* variables (INIT_ENABLED, INIT_USERNAME, INIT_PASSWORD, INIT_HOST, INIT_PORT, INIT_DNS and others) described on the unattended setup page. They're only read on the very first start, some must be set together, and the docs recommend removing them afterwards so the password isn't left in your compose file.
Pick How You'll Reach the Admin Panel
Since v15 the session cookie is marked Secure unless INSECURE is true. Browsers don't send such cookies over plain HTTP, so if you open http://SERVER_IP:51821 with the default settings, the login page tells you that you can't log in over an insecure connection. You have two options.
Option A (recommended): HTTPS through a reverse proxy
The wg-easy docs provide Caddy and Traefik examples. If Nginx already runs on the host, publish the UI port on the loopback address only, so nothing outside the host can reach it directly. In docker-compose.yml change the UI port line to:
- "127.0.0.1:51821:51821/tcp"
Then add a server block with TLS (certificate paths are placeholders; the Nginx guide shows how to get one with Certbot):
server {
listen 443 ssl;
server_name wg.example.com;
ssl_certificate /etc/letsencrypt/live/wg.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/wg.example.com/privkey.pem;
location / {
proxy_pass http://127.0.0.1:51821;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
Run sudo nginx -t before reloading Nginx. Leave INSECURE unset: the browser talks HTTPS to Nginx, so the secure cookie works.
Option B: plain HTTP on a trusted network only
The docs' No Reverse Proxy page marks this as insecure and not recommended. If the server sits on your home LAN and the UI is never reachable from outside, uncomment the environment: block and set INSECURE=true. Then make sure port 51821/tcp is closed to the internet in every firewall in front of the host.
Start wg-easy and Run the Setup Wizard
cd /etc/docker/containers/wg-easy
sudo docker compose up -d
sudo docker compose logs -f wg-easy
Open the admin panel (your HTTPS name, or http://LAN_IP:51821 with Option B). On first visit the setup wizard asks you to:
- Create the admin account. Use a long password; the docs note that setup doesn't check password complexity.
- Say whether you have an existing setup. Choose yes only when you're restoring a v14
wg0.jsonbackup. - Enter the host and port clients will connect to. The host must be your public IP or a DNS name that resolves to it. A private address such as the Docker bridge IP or a LAN IP behind NAT produces client configs that can't connect from outside.
The Getting Started page warns to use docker compose up and down, not start and stop; otherwise the container isn't properly destroyed and can start in an inconsistent state.
Configure Defaults in the Admin Panel
Settings that v14 took from environment variables now live under Admin Panel:
- Config: the public host and port written into client configs, plus the global Allowed IPs, DNS, MTU and persistent keepalive for clients. Changing host or port invalidates existing client configs, so settle these first.
- Interface: the server-side listen port, the tunnel address range (CIDR), the host network device traffic is forwarded through, and the per-client firewall switch.
- General: session timeout and the metrics endpoints.
- Hooks: PreUp/PostUp/PreDown/PostDown commands.
If you run your own DNS filter such as Pi-hole or AdGuard Home, set its address as the client DNS here so VPN clients get filtering too. The DNS server must be reachable through the tunnel.
Add Clients
- On the Clients page, click New and give the client a name that identifies the device.
- Phones: install the official WireGuard app, choose to add a tunnel from a QR code, and scan the client's QR code in wg-easy.
- Desktops: install the WireGuard client from wireguard.com/install, download the client's
.conffile and import it as a tunnel.
The QR code and the .conf file contain the client's private key. Treat them like a password: don't paste them into chat apps or leave them in a downloads folder.
Full Tunnel or Split Tunnel
A client's Allowed IPs decide which destinations it routes through the VPN. 0.0.0.0/0, ::/0 sends everything through the server (a full tunnel), which is what you want on untrusted Wi-Fi. If you only need to reach a home network, list just those ranges, for example 192.168.1.0/24, and everything else uses the device's normal connection. Set the default under Admin Panel, Config, or override it per client.
Allowed IPs only control routing on the client, and a user can edit them locally. The client guide points to the per-client firewall (Admin Panel, Interface) if you need to enforce which destinations a client may reach on the server side. The admin panel guide marks that feature as experimental, so test the rules before relying on them.
Harden the Admin Panel
WireGuard's protocol isn't the weak point here; the admin panel is, because anyone who logs in can create working VPN credentials.
- Use HTTPS (Option A) and keep 51821/tcp off the public internet.
- Turn on two-factor authentication for the admin account.
- If you only manage the server from known places, restrict the proxy further, for example with Nginx
allow/denyrules for your home IP and the VPN's own address range, or by only exposing the admin hostname inside the VPN. - Remove any
INIT_*variables after the first start.
Monitoring
The Clients page shows which clients are connected and their transfer charts. For history and alerts, wg-easy can expose Prometheus metrics; enable them and set a bearer password under Admin Panel, General, then scrape the endpoint from Prometheus (see Prometheus Grafana Monitoring).
Back Up and Restore
Everything lives in the etc_wireguard volume. Compose prefixes volume names with the project name (the directory name by default), so check the real name first:
sudo docker volume ls | grep etc_wireguard
sudo docker volume inspect --format '{{ .Mountpoint }}' wg-easy_etc_wireguard
For a consistent copy, stop the stack, archive the volume's directory, and start it again:
cd /etc/docker/containers/wg-easy
sudo docker compose down
BACKUP="$HOME/wg-easy-backup-$(date +%F).tar.gz"
install -m 600 /dev/null "$BACKUP"
sudo tar -czf "$BACKUP" -C "$(sudo docker volume inspect --format '{{ .Mountpoint }}' wg-easy_etc_wireguard)" .
sudo docker compose up -d
The archive contains private keys, so store it encrypted or on a machine you trust. To restore on a new host, create the stack once so the volume exists, run docker compose down, extract the archive into the volume's mountpoint, and start it again. As long as the server keys and the public host name stay the same, existing client configs keep working; if the public IP changed, update the DNS record (or the host in Admin Panel, Config, which requires re-issuing client configs).
Updates
The basic installation guide updates with:
cd /etc/docker/containers/wg-easy
sudo docker compose pull
sudo docker compose up -d
With the 15 tag this stays within v15. Read the release notes before pulling, and take a backup first. A future major version will need a deliberate tag change, just like the v14 to v15 move.
Troubleshooting
Clients never connect (no handshake)
- Port not reachable. Check UDP 51820 in the host firewall (
sudo ufw statusorsudo nft list ruleset), in your cloud provider's firewall or security group, and on any home router port forward. Cloud firewalls are separate from the OS firewall. - Wrong host in client configs. Open a client's
.confand look at theEndpointline. It must be your public IP or a name that resolves to it;dig +short wg.example.comshould return that IP. Fix the host under Admin Panel, Config, then re-issue the client config. - Container problems. Run
sudo docker compose logs wg-easy. The wg-easy FAQ covers errors such asCannot find device "wg0"(load the module withsudo modprobe wireguard) and missing iptablesnatorfiltertables (loadiptable_nat,ip6table_natand friends).
Connected, but no internet or no LAN access
- Confirm the sysctls and the
NET_ADMIN/SYS_MODULEcapabilities are still in the compose file; without forwarding, the handshake works but traffic stops at the server. - Check the Device under Admin Panel, Interface. It must be the network interface that leads to the internet inside the container.
- If you attached the container to a second Docker network (for a reverse proxy), the FAQ explains that Docker can swap
eth0andeth1on restart. Setinterface_nameandgw_priorityin the compose file as shown there. - Test DNS separately: if
ping 1.1.1.1works through the tunnel but names don't resolve, the client DNS server is wrong or unreachable through the tunnel.
Can't log in to the web UI
"You can't log in with an insecure connection" means you're on plain HTTP without INSECURE=true. Use the HTTPS proxy, or (on a trusted LAN only) set the variable and recreate the container with docker compose up -d.
Summary
- Use the
ghcr.io/wg-easy/wg-easy:15tag, notlatest, and configure host, DNS and Allowed IPs in the Admin Panel, not with v14 variables. - Open UDP 51820 everywhere it can be blocked; keep the 51821 admin panel behind HTTPS.
- Back up the
etc_wireguardvolume; it holds every key.



