SSD Nodes Learn Hosting plans →
How to do am Matt ConnorBy Matt Connor · Updated 2026-08-29

wg-easy Docker: WireGuard Web UI Setup Guide

Set up wg-easy with Docker Compose, ports, NET_ADMIN, required sysctls, and QR code onboarding. Version 15 moves settings out of environment variables.

Wetin you dey build

wg-easy na WireGuard wey get web interface, and e dey run as one Docker container. E dey manage WireGuard interface for you and add browser UI wey you fit use create clients. Every client wey you create go get config file and QR code, so phone fit join the VPN when you point camera to the screen.

The tunnel itself na normal WireGuard. Kernel module dey move the packets, so throughput dey the same like setup wey you write by hand. Wetin you gain na client lifecycle: you fit add, disable, and delete peers without editing config file over SSH. Wetin you lose na direct control of that config. Na this manual WireGuard setup for VPS dey cover.

You need KVM VPS wey get public IPv4 address, Docker Engine with Compose plugin, and root access. Container virtualisation wey share host kernel, like OpenVZ or LXC, usually no fit load WireGuard module. Because of this, container go fail to bring the interface up.

Version 15 don move the settings comot environment

Most guides wey you go find na for wg-easy 14, where you set WG_HOST to your server address and PASSWORD_HASH to bcrypt hash of the admin password, both as environment variables. Version 15 na complete rewrite. The official migration notes talk am plain: v15 no dey use the same environment variables wey v14 dey use, and most of dem don move go admin panel for web UI.

So WG_HOST and PASSWORD_HASH no dey do anything again. If you copy old compose file, the container go start, ignore those lines, then ask you to create admin account for browser. This no be bug. Na the new setup flow be this.

As of July 2026, the major tag wey you suppose pin na 15. Pin the major version instead of using latest, because major upgrade dey change on-disk config format and e no go roll back cleanly.

The compose file

Stack dey create a directory for, then write the official compose file inside am. Na upstream file be this, and e no get any change.

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

The contents dey look like this:

volumes:
  etc_wireguard:

services:
  wg-easy:
    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

etc_wireguard na named volume wey dey hold the server key and every client wey you create. Back up that volume, or rebuild go throw away all your peers. If you prefer make those files dey for host filesystem, replace am with bind mount. Before you do that, read the difference between bind mounts and named volumes, because permissions dey behave differently.

Why e need NET_ADMIN, SYS_MODULE and the sysctls

By default, container no get permission to touch network stack, and each of these lines dey remove one specific block.

NET_ADMIN dey allow container create the wg0 interface, assign address to am and write routes. Without am, container go start, then die while e dey bring the interface up, because ip link add wg0 type wireguard go return Operation not permitted.

SYS_MODULE together with the read-only /lib/modules mount dey allow container load WireGuard kernel module if host never load am before. The module dey inside host kernel, not inside the image. Na why the host directory must dey visible. For modern kernel, the module usually dey built in. You fit confirm am with sudo modprobe wireguard && echo ok for host.

net.ipv4.ip_forward=1 dey make kernel forward packets wey no address to the box itself. Without am, client go connect and handshake go succeed, but every packet to internet go drop. So ping 1.1.1.1 go timeout while VPN dey look connected.

net.ipv4.conf.all.src_valid_mark=1 na the one wey dey surprise people. WireGuard dey mark its own outgoing packets so dem no route back into the tunnel. Strict reverse path filtering dey see packet wey source address no match the expected route and drop am. This sysctl dey tell kernel to accept marked packets. Na this one dey stop full tunnel from breaking itself.

Start am and create the admin account

cd /etc/docker/containers/wg-easy
sudo docker compose up -d
sudo docker compose logs -f

Use docker compose up and docker compose down, no be start and stop. Upstream warn say start for a container wey dem create with different settings fit leave the network for inconsistent state. If you want the stack to come back after reboot, restart: unless-stopped don already cover am, and the boot behaviour of compose services explain wetin that policy promise and wetin e no promise.

The web UI dey listen for TCP 51821. The first time you visit am, e go show setup page where you go create the admin account and confirm the host address wey clients go use to reach the server. That host address go enter the Endpoint line for every client config, so e must be the public IP or DNS name of the VPS. If e wrong, the QR code wey you give phone go point to somewhere wey e no fit reach, and the handshake no go complete.

One more thing about that port: wg-easy 15 no dey accept plain HTTP unless you set INSECURE=true. You fit reach am over HTTPS with certificate wey dem no trust, or terminate TLS for reverse proxy wey dey in front of am. But reaching am over http:// with the default settings no dey work.

No publish UI port go internet

The compose file publish 51821 for every interface. Na login page be this for one box wey fit route your traffic, so e no suppose open to everybody. When Docker publish port, e dey write rules inside the DOCKER chain. This chain dey process before ufw, so ufw deny rule no go close the port. You need understand this trap by itself, and why Docker published ports dey ignore ufw explain everything for there.

The simple fix na to bind UI to loopback, then reach am through SSH tunnel:

    ports:
      - "51820:51820/udp"
      - "127.0.0.1:51821:51821/tcp"
    environment:
      - INSECURE=true

Then from your laptop:

ssh -L 51821:127.0.0.1:51821 youruser@your.server.address

Open http://127.0.0.1:51821 for browser on your laptop. SSH encrypt the traffic, nobody else fit get answer from the port, and INSECURE=true safe for here because the plain HTTP hop no ever comot from loopback interface.

Open UDP 51820, then check both firewalls

WireGuard need UDP 51820 make internet fit reach am. Docker publish the port, but plenty providers get separate network firewall wey dey in front of the VPS, and Docker no know anything about am. Open the port for both places. If na ufw you use manage the host firewall, these basic ufw rules for a VPS dey shorter than writing nftables by hand.

Check whether the container dey listen:

sudo ss -ulnp | grep 51820

You suppose see UDP socket wey dey listen. If nothing dey for that line, the container never bring the interface up, and sudo docker compose logs wg-easy go show the reason.

Create a client and scan it on a phone

For the UI, create one client and give am name wey you go recognise later, like the device wey e belong to. wg-easy go allocate the next free tunnel address and generate the key pair for you. Each client row get QR code and downloadable .conf file.

Install the official WireGuard app for the phone. Choose to add tunnel from QR code, then point the camera to the code for your screen. The tunnel go show with the name wey you type. Turn am on. The client row for the UI go start show transfer counters and recent handshake time. Once phone join the tunnel, e fit reach services wey you never publish to internet. Na so phone fit continue upload to self-hosted photo server from anywhere, without that server get even one port open to the whole internet. The same method fit cover media. Jellyfin library wey dem rebuild as 90s video store fit make sense to browse from hotel room, while e remain as private as e dey for your LAN. Alerts fit work for the other direction on the same tunnel. self-hosted ntfy server fit push message go that phone immediately backup job fail, without ever answering request from public internet.

If client no show handshake after you enable am, e no dey reach the server at all. Check UDP 51820, either for the provider firewall or for the endpoint address wey dem put inside the config. If client show handshake but internet no work, check forwarding or DNS instead.

For desktop, download the .conf file and import am into the WireGuard client instead of typing the config again. The private key for that file na generated once and shown once. Treat the file the same way you go treat SSH private key.

When you don outgrow the UI

wg-easy na the right tool while na people and phones be your peers. The UI dey faster than editing config files, and you fit revoke lost phone with one click.

You go reach the limit when you want something wey the UI no model. Site-to-site routing, where a peer's AllowedIPs covers one whole remote subnet instead of one address, na usually the first wall. Split tunnels with per-peer routing rules, or config wey your provisioning tool generate, come next. For that point, the hand-written setup no harder; e just different, and the plain WireGuard guide show the same tunnel built from wg0.conf. If you prefer stop running the control plane completely, WireGuard compared with Tailscale explain the managed option. Whether that trade-off fair depend on wetin the coordination server fit actually reach, and Tailscale's trust model worth reading before you hand over your network to am. Cost usually na the next question, and wetin the Tailscale free plan actually cover enough make household or small team pay nothing for am. After that point, billing count users instead of devices. That bill structure different from VPS wey you already dey pay for, so wetin Tailscale cost once you outgrow the free plan na the number to check before you migrate team. The full tunnel wey you just build get direct equivalent there, because advertising the VPS as a Tailscale exit node give you the same route out through the server. You approve am for the admin console instead of writing am inside each client config. The subnet wall get equivalent too, because advertising a whole private network from the VPS hand that network to every device for the tailnet without the per-peer AllowedIPs editing wey push you away from the UI. If you want that dashboard and automatic mesh routing but you no want another person's coordination server, running your own NetBird server on a VPS keep the control plane for hardware wey you own. But you go need handle the DNS and TLS setup wey wg-easy never ask you for.

If na the compose syntax above be the unfamiliar part, not the WireGuard part, Docker Compose basics on a VPS explain the file format and the everyday commands.

FAQ

WireGuard im WG_HOST and PASSWORD_HASH no follow?

Those variables na for wg-easy 14. Version 15 na rewrite, and upstream move almost all configuration go the admin panel for the web UI. The container no dey read either variable, so e go start normally, then ask you make you create admin account when you visit am first time. Set the client-facing host address for that setup page instead.

I need SYS_MODULE if my kernel already get WireGuard?

No. SYS_MODULE and the /lib/modules mount dey there so the container fit load the module when host no get am. For host wey sudo modprobe wireguard already succeed, the capability no dey used. Removing am na reasonable hardening step, and NET_ADMIN still dey required either way.

Client connect but internet no dey. Wetin dey wrong?

Handshake wey no get traffic almost always mean forwarding problem. Confirm say net.ipv4.ip_forward=1 and net.ipv4.conf.all.src_valid_mark=1 still dey inside the compose file, because hand-edited copy fit lose dem. If forwarding dey on, check the DNS server wey client receive. Tunnel wey send all traffic through VPN but point to DNS server wey e no fit reach again go look exactly like dead connection for browser.

How I fit back up my clients?

Everything dey inside the etc_wireguard named volume, for one wg0.json file. The UI also get backup button wey export the same data. Copy that file go somewhere outside the server before any upgrade. To restore am, upload the file during the setup step for fresh container.

I fit run wg-easy behind reverse proxy?

Yes. Put the proxy before TCP 51821, terminate TLS there, and set INSECURE=true for the container so e go accept the plain HTTP hop from the proxy. Keep UDP 51820 published directly, because VPN traffic na UDP and e no dey pass through HTTP proxy.