What's WireGuard-Easy?
wg-easy wraps WireGuard in a web UI. Key generation, client configs, peer management — all through a browser instead of editing config files by hand. I've been running it on a Hetzner VPS for about 14 months now with 11 peers connected (phones, laptops, a couple of home servers) and the thing just works. Haven't touched the config since initial setup.
One gotcha right away: WG_HOST must be set to the server's public IP or domain. I wasted 20 minutes the first time because I set it to the Docker bridge IP. The generated client configs pointed at a private address and obviously nothing connected. Use your actual public IP or a DNS name that resolves to it.
The UI is basic — it generates QR codes for mobile and download links for desktop configs. That's all it does and that's all it needs to do. I've tried fancier WireGuard managers (Firezone, Subspace) and they all add complexity without adding value. wg-easy does the boring thing well:
- Web interface for peer management
- One-click client creation
- QR codes for mobile device provisioning
- Per-client enable/disable toggle
- Connection status and transfer stats
Prerequisites
Nothing exotic. If you've got Docker running on a Linux box, you're 90% there.
- Linux server — VPS or home server, doesn't matter. A $5/month VPS handles dozens of peers without breaking a sweat.
- Docker and Docker Compose installed (if you need help with this, see the Docker guide)
- Port 51820/UDP open in firewall — and your cloud provider's security group if applicable. This is the one people forget.
- Public IP or dynamic DNS
Quick Deployment
The docker-compose.yml is the entire deployment. No init scripts, no PKI setup, no certificate generation. That's why this exists — WireGuard's raw setup is elegant but managing peers manually is tedious. Here's the file:
version: "3.8"
services:
wg-easy:
image: ghcr.io/wg-easy/wg-easy
container_name: wg-easy
environment:
- WG_HOST=your-server-ip-or-domain
- PASSWORD=your-admin-password
- WG_PORT=51820
- WG_DEFAULT_DNS=1.1.1.1
- WG_ALLOWED_IPS=0.0.0.0/0
volumes:
- ./wg-easy:/etc/wireguard
ports:
- "51820:51820/udp"
- "51821:51821/tcp"
cap_add:
- NET_ADMIN
- SYS_MODULE
sysctls:
- net.ipv4.ip_forward=1
- net.ipv4.conf.all.src_valid_mark=1
restart: unless-stopped
Start the container:
docker compose up -d
Configuration Options
Most of these you set once and never look at again. The ones that matter:
- WG_HOST: Your server's public IP or domain. Required. Get this wrong and nothing works — already mentioned it but it's worth repeating.
- PASSWORD: Web interface admin password. Pick something decent, this is your VPN management console.
- WG_PORT: WireGuard listen port (default 51820). No reason to change it unless you're running multiple instances.
- WG_DEFAULT_DNS: DNS server for clients. This is the one you'll actually change. Set it to your Pi-hole or AdGuard Home IP if you run one. The default uses Cloudflare's 1.1.1.1 which is fine but kind of defeats the point if you have your own DNS server sitting right there.
- WG_ALLOWED_IPS: What traffic goes through VPN.
0.0.0.0/0means everything. More on this in the split tunneling section. - WG_MTU: Custom MTU. Leave it alone unless you're seeing fragmentation issues on weird networks.
Access Web Interface
Open http://YOUR_SERVER_IP:51821 in a browser. Log in with the PASSWORD from your compose file. The interface is deliberately minimal — a list of peers, a button to add more, and that's basically it. Don't expect a dashboard with fancy graphs. It does its job.
Add Clients
Adding a new device takes about 10 seconds. Seriously.
- Click "New Client"
- Give it a name — I use descriptive ones like "anurag-pixel8" or "dad-iphone" so I can tell them apart later
- Click Create
That's it. Each client entry gives you a QR code for mobile, a downloadable .conf file for desktop, and an enable/disable toggle. I've added my parents' phones this way — zero chance I'm walking them through a manual WireGuard config with private keys and endpoint addresses.
Mobile Setup
Phone setup is the smoothest part of the whole experience.
- Install the WireGuard app from the App Store or Play Store
- Tap "+" then "Create from QR code"
- Point your camera at the QR code in the wg-easy interface
- Toggle on. You're connected.
The QR code thing is genuinely clever — the entire client config (keys, endpoint, DNS, allowed IPs) is encoded in that one image. No file transfers, no copy-pasting.
Desktop Setup
Slightly more manual than mobile but still under a minute.
- Download the .conf file from wg-easy
- Install WireGuard client (available for Windows, macOS, Linux)
- Import tunnel from file
- Activate
Split Tunneling
By default, WG_ALLOWED_IPS=0.0.0.0/0 routes everything through the VPN. All your traffic, all the time. That's what you want if you're on public wifi at a coffee shop.
But if you just want to reach your home network — access a NAS, hit an internal service, use your Pi-hole — then route only those subnets:
WG_ALLOWED_IPS=10.0.0.0/8,192.168.0.0/16
Everything else goes out your normal internet connection. Way less latency for everyday browsing, and you're not bottlenecked by your VPS upload speed for Netflix.
Security Hardening
The VPN tunnel itself is solid — WireGuard's cryptography is not the concern here. The weak point is the web UI. It's a password-protected admin panel sitting on port 51821 and by default it's plain HTTP. You should fix that.
Put Web UI Behind Reverse Proxy
Don't leave port 51821 exposed directly. Put it behind Nginx (or Caddy, or whatever you already run) with TLS:
server {
listen 443 ssl;
server_name wg.yourdomain.com;
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;
location / {
proxy_pass http://localhost:51821;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
After configuring the proxy, remove the 51821 port mapping from the compose file so it's only accessible through Nginx.
Restrict Web UI Access
Even better than a reverse proxy alone — restrict access by source IP. If you only ever manage this from home, allow your home IP and block everything else. I use a UFW rule that limits 51821 to my home IP and my phone's VPN IP. Overkill? Maybe. But the admin panel is the keys to the kingdom.
Monitoring
The web UI shows you per-client stats — who's connected, how much data they've pushed, and when the last handshake was. Green dot means handshake within 2 minutes (i.e., the client is active). That's about it for monitoring. No fancy time-series graphs or alerting built in.
If you want actual metrics, you'd need to pull WireGuard stats via wg show inside the container and push them to Prometheus or something. I haven't bothered. For 11 peers on a personal VPN, glancing at the web UI once a week is plenty.
Backup
The state lives in one directory — the ./wg-easy volume mount. All peer configs, private keys, everything. I rsync it to my NAS nightly with a cron job. Dead simple.
tar -czf wg-easy-backup.tar.gz ./wg-easy
If the VPS dies, spin up a new one, restore the directory, docker compose up -d, and every client reconnects automatically without changing anything on their end. Tested this when Hetzner had scheduled maintenance last October — took about 4 minutes from new VPS to all peers reconnected. The private keys stay the same so clients don't even notice it's a different server.
Updates
Two commands. That's the whole update process:
docker compose pull
docker compose up -d
Your volume data stays intact — the container is stateless, all the peer configs live in the mounted directory. I got caught off guard once when a major version bump (v7 to v8 last summer) changed the password hashing. Had to reset the admin password via an environment variable. Took 5 minutes to figure out but it was annoying. Check the release notes before pulling if you want to avoid surprises.
Troubleshooting
Two scenarios cover 95% of the problems people hit.
Client Cannot Connect
Almost always a networking issue, not a WireGuard issue.
- Port 51820/UDP — is it open? Check both the host firewall (iptables/ufw) and your cloud provider's security group. Hetzner, AWS, DigitalOcean all have their own firewall layer that's separate from the OS.
- WG_HOST — does it actually resolve to the right IP?
dig +short your-domain.comshould return your server's public IP. - Container logs:
docker logs wg-easy— usually tells you exactly what's wrong.
Connected But No Internet
This one's more subtle. The VPN tunnel works but traffic doesn't route properly.
- The sysctl values (
ip_forwardandsrc_valid_mark) need to be applied. They're in the compose file but some hosting providers override them. - NET_ADMIN and SYS_MODULE capabilities — make sure you didn't accidentally strip these from the compose file.
- Try switching WG_DEFAULT_DNS to 8.8.8.8. If that fixes it, your original DNS server was the problem, not the VPN.
TL;DR
One Docker container. Web UI for managing peers. UDP 51820 must be open.
Set WG_HOST to a public IP or domain — not a private address, not localhost, not the Docker bridge IP.
QR codes handle mobile provisioning. No file transfers, no manual config editing.
Back up the volume directory and you can rebuild the whole thing in under 5 minutes.
Done.
💬 Comments