Skip to content

TutorialsVPN & private DNS

How to run a WireGuard VPN with a web UI using wg-easy 15

Install wg-easy 15 with Docker Compose, put its web UI behind Caddy HTTPS, finish the setup wizard, turn on 2FA, create clients with QR codes and back them up.

  • Beginner
  • 25 min read
  • Updated

Tested on: Ubuntu 24.04 LTS, Ubuntu 26.04 LTS, Debian 12, Debian 13

This guide is not available in your language yet, so it is shown in English.

On this page
  1. Prerequisites
  2. Step 1 — Download the official Compose file
  3. Step 2 — Keep the web UI on localhost
  4. Step 3 — Open the firewall and add the Caddy site
  5. Step 4 — Start wg-easy
  6. Step 5 — Complete the setup wizard
  7. Step 6 — Turn on two-factor authentication
  8. Step 7 — Create clients and adjust server settings
  9. Back up and restore
  10. Update wg-easy
  11. Troubleshooting
  12. Error: WireGuard exited with the error: Cannot find device wg0
  13. Can't initialize iptables table nat: Table does not exist
  14. The login fails or the page reloads without logging in
  15. You forgot the admin password
  16. Clients connect but never get a handshake
  17. Next steps

wg-easy bundles a WireGuard VPN server and a web interface in one Docker container. Instead of editing configuration files, you create, disable and delete VPN clients in the browser and hand them out as QR codes or downloadable files. Version 15 is the current major release: it replaced the old environment-variable setup with a first-run setup wizard and an Admin Panel, added two-factor authentication, and expects the web UI to be served over HTTPS.

This guide uses the project's official Docker Compose file, keeps the web UI on 127.0.0.1 and publishes it through Caddy with a TLS certificate, then walks through the setup wizard, 2FA, creating clients, backups, updates and common errors. If you prefer to manage WireGuard by hand without a web interface, follow How to set up a WireGuard VPN server instead; do not run both on the same port.

Prerequisites

  • A server running Ubuntu 24.04 LTS, Ubuntu 26.04 LTS, Debian 12 or Debian 13 on x86_64 or arm64, the architectures wg-easy supports. Their kernels include WireGuard. The steps work on a HyperDC Linux VPS, VDS or dedicated server with root access.
  • A non-root user with sudo rights: see Secure a new Linux server and Set up SSH keys.
  • Docker Engine with the Compose plugin: Install Docker on Ubuntu or Install Docker on Debian. The wg-easy README mentions Docker's convenience script; the repository method in our guides is the one Docker recommends for production.
  • A domain name such as vpn.example.com with an A (and optionally AAAA) record pointing at the server, as a DNS-only record (not proxied through a CDN, because WireGuard traffic must reach the server directly), and Caddy installed as in Caddy as a reverse proxy.
  • UDP port 51820 allowed in your provider's network firewall, if it has one.
ResourceMinimum (official)Suggested starting point
CPUNot published1 vCPU
RAMNot published1 GB
DiskNot published5 GB free for the image and configuration

The project does not publish minimum requirements; the right-hand column is a conservative starting point. As with any VPN, bandwidth is usually the real limit.

Step 1 — Download the official Compose file

The basic installation guide downloads docker-compose.yml from the repository; any directory works, and this guide uses /opt/wg-easy:

Bash
sudo mkdir -p /opt/wg-easy
sudo chown $USER:$USER /opt/wg-easy
cd /opt/wg-easy
curl -fsSL -o docker-compose.yml https://raw.githubusercontent.com/wg-easy/wg-easy/master/docker-compose.yml
cat docker-compose.yml

Read the file before you start it. It runs ghcr.io/wg-easy/wg-easy:15, stores everything in the named volume etc_wireguard, mounts /lib/modules read-only, adds the NET_ADMIN and SYS_MODULE capabilities WireGuard needs, enables IP forwarding through sysctls, and creates a Docker network with fixed IPv4 and IPv6 addresses for the container.

Step 2 — Keep the web UI on localhost

By default the file publishes the web UI on port 51821 on every address of the server. Docker-published ports bypass ufw, so change only that line to bind to loopback. Open the file with nano docker-compose.yml; the result looks like this (comments from the original are kept):

YAML
volumes:
  etc_wireguard:

services:
  wg-easy:
    #environment:
    #  Optional:
    #  - 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"
      - "127.0.0.1:51821:51821/tcp"
    restart: unless-stopped
    cap_add:
      - NET_ADMIN
      - SYS_MODULE
      # - NET_RAW # Uncomment if using Podman
    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

Port 51820/udp stays public, because that is where VPN clients connect. Leave INSECURE at its default of false: the UI will only accept logins over HTTPS, which Caddy provides in the next step.

Step 3 — Open the firewall and add the Caddy site

Allow SSH, web traffic and the WireGuard port. Docker publishes 51820/udp regardless of ufw, but the rule documents the intent and keeps working if you ever change the setup:

Bash
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw allow 51820/udp
sudo ufw enable

Add a site block to /etc/caddy/Caddyfile that proxies to the loopback port. Caddy obtains a certificate for the domain automatically:

Caddyfile
vpn.example.com {
    reverse_proxy 127.0.0.1:51821
}
Bash
sudo systemctl reload caddy

The project's own Caddy example runs Caddy in a container on a shared Docker network; a Caddy on the host with a loopback port, as here, achieves the same result. Nginx with Certbot or Traefik work too.

Step 4 — Start wg-easy

Bash
docker compose up -d
docker compose ps
docker compose logs --tail 30
curl -I https://vpn.example.com

docker compose ps shows the wg-easy container as running, the logs show no WireGuard or iptables errors, and curl gets a response from the UI over a valid certificate.

Step 5 — Complete the setup wizard

Open https://vpn.example.com. The wizard asks for:

  1. User setup: an admin user name and a long password from your password manager.
  2. Existing setup: choose No for a new installation. (Choose Yes only to import a wg0.json backup from wg-easy 14.)
  3. Host setup: the host clients connect to and the port. Enter vpn.example.com (or the server's public IP) and 51820. This value is written into every client configuration, so it must be the server's public address. IPv6 addresses go in square brackets, such as [2001:db8::1].

After the wizard you log in to the client list. For automated deployments, the documentation also describes INIT_* environment variables (such as INIT_USERNAME, INIT_PASSWORD, INIT_HOST and INIT_PORT) that skip the wizard on the first start; remove them afterwards so the password does not stay in the Compose file.

Step 6 — Turn on two-factor authentication

Open the menu in the top-right corner, go to Account and choose Enable Two Factor Authentication. Scan the QR code with an authenticator app and confirm with the current code. From now on the login asks for the code as well. Keep the authenticator backed up; disabling 2FA from the Account page requires being logged in.

Step 7 — Create clients and adjust server settings

Click New Client, give it a name per device (for example laptop or phone), optionally set an expiry date, and create it. For each client you can then:

  • show a QR code to scan with the WireGuard app on a phone,
  • download the .conf file for the desktop apps,
  • create a one-time link to send the configuration to a user,
  • disable, edit or delete it later.

Import the configuration in the WireGuard app and connect. The client list then shows the latest handshake and traffic for that client.

The Admin Panel holds the server-wide settings: the default DNS servers and allowed IPs written into new client configurations (0.0.0.0/0, ::/0 sends all traffic through the VPN; a private range gives a split tunnel), interface options such as the client address range, and the experimental Per-Client Firewall under Interface, which limits what each client may reach and is enforced on the server with iptables. If you run AdGuard Home for your VPN clients, follow the AdGuard Home example in the wg-easy documentation.

Back up and restore

All state lives in the Docker volume: the wg-easy database with users, clients and keys, plus the generated WireGuard configuration. Compose prefixes the volume name with the project folder name, so check it first:

Bash
docker volume ls
sudo mkdir -p /opt/backups
cd /opt/wg-easy
docker compose stop
docker run --rm -v wg-easy_etc_wireguard:/data -v /opt/backups:/backup ubuntu tar czf /backup/wg-easy-$(date +%F).tar.gz -C /data .
docker compose start
sudo chmod 600 /opt/backups/wg-easy-*.tar.gz

Clients are disconnected for the few seconds the container is stopped. The archive contains every private key, so keep it private and copy it off the server together with docker-compose.yml.

To restore, for example on a new server after Steps 1 to 3, let Compose create the container and volume without starting them, replace the volume's contents with the archive, and start wg-easy. The rm step empties the volume first, so any configuration currently in it is replaced:

Bash
cd /opt/wg-easy
docker compose down
docker compose create
docker run --rm -v wg-easy_etc_wireguard:/data -v /opt/backups:/backup ubuntu sh -c "rm -rf /data/* && tar xzf /backup/wg-easy-2026-10-09.tar.gz -C /data"
docker compose up -d

The keys are unchanged, so existing clients reconnect without new configurations, as long as the host name in the setup still points to the server.

Update wg-easy

Read the release notes and take a backup first. With the 15 tag, updates stay within version 15. The project's update guide pulls and recreates in one command:

Bash
cd /opt/wg-easy
docker compose up -d --pull always
docker compose logs --tail 30
docker image prune

Before a future major version, read its migration guide, change the image tag yourself, and keep the backup until the new version runs. The documentation also shows automatic updates with Watchtower; with a VPN that you depend on, manual updates let you choose the moment.

Troubleshooting

Error: WireGuard exited with the error: Cannot find device wg0

The WireGuard kernel module is not loaded. Load it now and on every boot, then restart the container:

Bash
sudo modprobe wireguard
echo wireguard | sudo tee /etc/modules-load.d/wireguard.conf
docker compose restart

Can't initialize iptables table nat: Table does not exist

The host kernel has not loaded the module for the iptables nat table. The wg-easy FAQ lists the fix: load iptable_nat (and ip6table_nat for the IPv6 message) with sudo modprobe and add the module names to the files in /etc/modules-load.d/ as above.

The login fails or the page reloads without logging in

You are opening the UI over plain HTTP, for example after publishing port 51821 directly instead of going through Caddy. Version 15 only accepts logins over a secure connection. Use https://vpn.example.com; set INSECURE=true only for a UI that is never reachable from outside your local network.

You forgot the admin password

Reset it with the built-in command-line tool. It asks for the new password:

Bash
docker compose exec -it wg-easy cli db:admin:reset

Clients connect but never get a handshake

UDP 51820 does not reach the server, or clients use the wrong address. Check your provider's firewall, make sure vpn.example.com is a DNS-only record that resolves to the server (a CDN proxy cannot carry WireGuard traffic), and check the host and port in the Admin Panel; after changing them, download or scan the client configurations again.

Next steps

Frequently asked questions

Why does wg-easy 15 refuse to log in over plain HTTP?

Version 15 expects the web UI to be served over a secure connection. Access over HTTP only works when you set INSECURE=true, which the project calls insecure and only suitable for a UI that is not reachable from outside your local network. Put the UI behind a reverse proxy with HTTPS instead.

Which image tag should I use?

The documentation recommends pinning the major version tag, 15, which receives all 15.x updates. Avoid latest: it still points to version 14.

Can I run wg-easy and a manual WireGuard server on the same machine?

Not on the same UDP port. wg-easy creates its own WireGuard interface inside the container. Use one approach per server, or give the second one a different port and address range.

How do I upgrade from wg-easy 14?

Download the wg0.json backup from the old web UI, stop the old container, start version 15 and choose the option to import an existing configuration in the setup wizard. Old environment variables are not migrated; most settings now live in the Admin Panel. armv6 and armv7 cannot use version 15.

What happens if I lose my 2FA device?

The documentation only describes disabling 2FA from the Account page while you are logged in, and the built-in CLI documents a password reset but no 2FA reset. Back up your authenticator app so that losing a phone does not lock you out.

Sources

Wachtwoord genereren

Please confirm