SSD Nodes Learn 8GB RAM — $66/yr
How to do am Matt ConnorBy Matt Connor · Updated 2026-08-02

How to Run wg-easy WireGuard for Docker Compose

Run wg-easy for Docker Compose with the right ports, NET_ADMIN, sysctls, and version 15 settings, plus QR codes wey make phone VPN setup easy.

Wetin you dey build

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

The tunnel itself na ordinary WireGuard. The kernel module dey move the packets, so throughput na the same as 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, and na this manual WireGuard setup for a VPS dey discuss.

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

Version 15 move settings comot from 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 rewrite. The official migration notes talk am plainly say v15 no dey use the same environment variables like v14, and say most of dem don move go admin panel for the 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, and then ask you to create admin account for browser. This no be bug. Na the new setup flow.

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

Di compose file

Create one directory for di stack, then write di official compose file inside am. Na upstream file be this, and e no 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

Di contents 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 di server key and every client wey you create. Back up dat volume, otherwise rebuild go throw away all your peers. If you prefer make you see those files for di host filesystem, replace am with bind mount. Before you do that, read di difference between bind mounts and named volumes, because di permissions dey behave differently.

Why e need NET_ADMIN, SYS_MODULE and the sysctls

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

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

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

net.ipv4.ip_forward=1 dey make the kernel forward packets wey no address to the box itself. Without am, client go connect, handshake go succeed, and then every packet to the internet go drop. So ping 1.1.1.1 go time out 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 go route back into the tunnel. Strict reverse path filtering dey see packet wey source address no match the expected route and dey drop am. This sysctl dey tell the kernel to accept marked packets. Na this 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 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 on 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 reach the server. That host address go enter the Endpoint line for every client config, so e must be the public IP or the 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 allow plain HTTP unless you set INSECURE=true. You fit reach am over HTTPS with an untrusted certificate, 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 the UI port to the internet

The compose file dey publish 51821 for every interface. Na login page 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 evaluate before ufw, so ufw deny rule no go close am. You suppose understand this trap by itself, and why Docker published ports ignore ufw explain am fully.

The simple fix na to bind the 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 dey encrypt the traffic, the port no go answer anybody else, and INSECURE=true safe for here because the plain HTTP hop no dey leave the loopback interface.

Open UDP 51820, and check both firewalls

WireGuard need make UDP 51820 reachable from internet. Docker publish am, but many providers put another network firewall for front of the VPS, and Docker no know anything about am. Open the port for both places. If na ufw you dey use manage the host firewall, the basic ufw rules for a VPS na the shorter way pass writing nftables by hand.

Check whether the container really dey listen:

sudo ss -ulnp | grep 51820

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

Create client and scan am for phone

For UI, create client and give am name wey you go recognise later, like the device wey e belong to. wg-easy dey 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 UI go start to show transfer counters and recent handshake time.

Client wey no show handshake after you enable am no dey reach the server at all. This one dey point to UDP 51820, either for the provider firewall or for the endpoint address wey dey inside the config. Client wey show handshake but internet no work dey point to forwarding or DNS instead.

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

When e go outgrow the UI

wg-easy na the correct tool as long as your peers na people and phones. The UI dey faster pass editing config files, and revoking one lost phone na just one click.

You go reach the limit when you want something wey the UI no dey model. Site to site routing, where one peer e get AllowedIPs wey covers one complete 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, na the next ones. For that point, the hand-written setup no hard pass; e just different, and the plain WireGuard guide dey show the same tunnel built from wg0.conf. If you prefer make you stop running the control plane completely, WireGuard compared with Tailscale dey explain the managed option.

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

FAQ

Why wg-easy dey ignore my WG_HOST and PASSWORD_HASH?

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

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 the host no get am. For host wey sudo modprobe wireguard already dey succeed, the capability no dey used. Removing am na reasonable hardening step, and NET_ADMIN still dey required either way.

The client dey connect but internet no dey. Wetin be the problem?

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 the client receive. Tunnel wey dey send all traffic through the 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 dey export the same data. Copy that file go somewhere outside the server before any upgrade. To restore am, upload am during the setup step for fresh container.

I fit run wg-easy behind reverse proxy?

Yes. Put the proxy in front of 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.