Full example: Clone the working files at github.com/KingPin/sumguy-examples/networking/wg-easy-wireguard-normal-humans
Your WireGuard server is up. You configured it by hand, you understand every line of wg0.conf, you can recite the PostUp iptables rules from memory. Good for you. Genuinely. Now your partner wants VPN access on their phone. Your sibling visiting for the holidays wants it too. Your parents heard “the VPN thing” and now they’re asking.
Suddenly your clean hand-crafted config is a support ticket waiting to happen.
That’s not a WireGuard problem — WireGuard is excellent. That’s an onboarding problem. wg-easy solves exactly that.
What wg-easy Actually Is
wg-easy is a Docker container that wraps a WireGuard server with a web UI. You get:
- A browser dashboard to create, disable, and delete clients
- Per-client QR codes (for phones) and
.conffile downloads (for desktops) - Live traffic stats per client
- Per-client allowed IPs and optional expiration dates
- v15+ multi-server support — one dashboard managing multiple WireGuard interfaces
The underlying WireGuard setup is identical to what you’d do by hand. wg-easy just handles the key generation, peer config wrangling, and wg commands so you don’t have to SSH in every time someone buys a new laptop.
Honest take: if you’re a single-user power-user who lives in wg-quick and SSH is second nature, you don’t need this. But if you’re running a VPN for a household, a small team, or a homelab where you want a clean admin interface — wg-easy is a good tool.
The Compose Stack
Here’s the full setup. No hand-waving.
services: wg-easy: image: ghcr.io/wg-easy/wg-easy:14 container_name: wg-easy restart: unless-stopped cap_add: - NET_ADMIN - SYS_MODULE sysctls: - net.ipv4.ip_forward=1 - net.ipv4.conf.all.src_valid_mark=1 ports: - "51820:51820/udp" # WireGuard - "51821:51821/tcp" # Web UI volumes: - wg-easy-data:/etc/wireguard environment: - LANG=en - WG_HOST=vpn.yourdomain.com # Your public IP or hostname - PASSWORD_HASH=${WG_PASSWORD_HASH} # bcrypt hash, not plaintext - WG_PORT=51820 - WG_DEFAULT_ADDRESS=10.8.0.x - WG_DEFAULT_DNS=1.1.1.1 - WG_ALLOWED_IPS=0.0.0.0/0 # Full tunnel (or restrict per client) - WG_PERSISTENT_KEEPALIVE=25 - UI_TRAFFIC_STATS=true - UI_CHART_TYPE=1
volumes: wg-easy-data:A few things worth calling out:
cap_add: NET_ADMIN and SYS_MODULE — WireGuard needs kernel-level network access. These capabilities are required, not optional. Don’t strip them thinking you’re being security-conscious; the container won’t function.
sysctls — IP forwarding has to be enabled or traffic routing breaks entirely. The src_valid_mark sysctl is required for WireGuard’s routing to work correctly on most setups. Set these in the Compose file, not on the host — keeps the config self-contained.
PASSWORD_HASH — Never put a plaintext password in your Compose file. Generate a bcrypt hash:
docker run --rm -it ghcr.io/wg-easy/wg-easy:14 wgpw 'YourPasswordHere'Copy the output into a .env file as WG_PASSWORD_HASH. The $ signs in bcrypt hashes need escaping or quoting — store it in .env and let Compose handle it.
WG_HOST — This has to be your actual public-facing hostname or IP. This is what gets embedded in every client config. If you put localhost here your clients will try to connect to themselves. Ask me how I know.
Reverse Proxy with Caddy
You probably don’t want the admin UI exposed on a raw port with no TLS. Here’s a minimal Caddy config to put it behind HTTPS with basic auth as an extra layer:
vpn-admin.yourdomain.com { basicauth { admin $2a$14$... # same bcrypt hash } reverse_proxy wg-easy:51821}Add Caddy to the same Compose network as wg-easy:
services: wg-easy: # ... same as above ... networks: - proxy ports: - "51820:51820/udp" # Remove 51821 port mapping — Caddy handles it internally
caddy: image: caddy:2 container_name: caddy restart: unless-stopped ports: - "80:80" - "443:443" volumes: - ./Caddyfile:/etc/caddy/Caddyfile - caddy-data:/data - caddy-config:/config networks: - proxy
networks: proxy:
volumes: caddy-data: caddy-config: wg-easy-data:Once this is running, drop the 51821:51821 port mapping from wg-easy. Caddy terminates TLS, authenticates, and proxies to the container internally. The admin panel is never exposed on a raw port.
Hardening: The Short Version
Strong password, bcrypt hashed. Already covered. Don’t skip it.
Restrict UI access by network. If your admin dashboard is only for you, add an IP allowlist in Caddy or firewall the port to your home IP only. The WireGuard port (51820/udp) stays public — that’s intentional, it’s how clients connect. The admin port does not need to be.
Firewall the host. wg-easy handles the WireGuard config, but your host firewall is still your responsibility. Open UDP 51820, HTTPS 443, and nothing else you don’t need.
Keep the image pinned to a major version tag. The latest tag on any container is a surprise waiting to happen. Pin to 14 or whatever the current stable is and update intentionally.
Per-client allowed IPs. By default, WG_ALLOWED_IPS=0.0.0.0/0 routes all client traffic through your VPN (full tunnel). If a client only needs access to your home network — say, reaching a NAS — set their allowed IPs to 10.8.0.0/24,192.168.1.0/24 instead. You can do this per-client from the UI.
v15+ Multi-Server Mode
wg-easy v15 added multi-server support: one instance can manage multiple WireGuard interfaces. This is useful if you want separate VPN pools — say, one for family traffic and one for your server admin access — without running separate containers.
Configuration-wise, you define multiple interfaces in the Compose environment. The UI shows a dropdown to switch between them. It’s a relatively new feature; the single-server setup above covers 95% of use cases. But it’s worth knowing if you find yourself wanting to segment clients.
How Onboarding a Client Actually Works
This is the part that makes wg-easy worth it.
- Open the web UI at
https://vpn-admin.yourdomain.com - Click “Add Client”, type a name (“Dad’s iPhone”)
- A QR code appears immediately
- Hand your phone to dad, open WireGuard app, scan code
- Done
No SSH. No key generation. No copying .conf files via email (please don’t do that). The QR code embeds the full client config — keys, server endpoint, allowed IPs, DNS. The WireGuard mobile apps read it natively.
For desktop clients, the same dialog has a “Download .conf” button. Import it into the WireGuard app on Windows, macOS, or Linux. Same deal.
When someone loses their device or you want to revoke access, click the delete button in the UI. Their keys are gone from the server instantly.
wg-easy vs. the Alternatives
Bare WireGuard with scripts. This is fine if you’re the only user and you’re comfortable in the terminal. It’s not fine when you’re the support line for five family members who’ve never touched a config file.
NetBird. More of a mesh networking tool than a simple VPN server. Excellent for team setups with peer-to-peer traffic, access policies, and SSO. Significantly more infrastructure to run self-hosted. If you just want a VPN server for home use, it’s overkill in the same way a Kubernetes cluster is overkill for a blog.
Tailscale. Great product. The control plane is Tailscale’s — you don’t own it. If Tailscale goes down, your VPN goes down. Headscale is the self-hosted control plane alternative, but now you’re maintaining Headscale too. For a simple home VPN, wg-easy gets you 90% of the Tailscale experience with full control and zero dependency on a third-party SaaS.
Wireguard-ui (alternative Docker UIs). There are a few others: ngoduykhanh/wireguard-ui, vx3r/wg-gen-web. They work. wg-easy has the most active maintenance and the cleanest UX right now.
When wg-easy Is Overkill
Honestly? If you’re a single user who’s comfortable with wg-quick and you’ve already got your config dialed in — don’t bother. You’re not the target audience.
wg-easy adds a container, a web service, and a dependency to manage. For a solo power-user, that’s net complexity, not simplification. Your flat wg0.conf file is already auditable, version-controllable, and perfectly fine.
The tool earns its keep when you’re managing multiple clients for people who will never touch a terminal. The moment you’re the WireGuard IT department for your household, the web UI pays for itself in time saved.
Quick Start
# 1. Create the project directorymkdir wg-easy && cd wg-easy
# 2. Generate your password hashdocker run --rm -it ghcr.io/wg-easy/wg-easy:14 wgpw 'YourPasswordHere'
# 3. Create .env with the hashecho 'WG_PASSWORD_HASH=$2a$14$...' > .env
# 4. Drop in the docker-compose.yml (above), update WG_HOST
# 5. Start itdocker compose up -d
# 6. Open http://yourserver:51821 and add your first clientPoint your phone at the QR code. Connect. Your 2 AM “why is dad’s phone not connecting” call just got a lot shorter.