# 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.

Difficulty: Beginner\
Tested on: Ubuntu 24.04 LTS, Ubuntu 26.04 LTS, Debian 12, Debian 13

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](/guides/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](/guides/secure-a-new-linux-server) and [Set up SSH keys](/guides/ssh-keys).
- Docker Engine with the Compose plugin: [Install Docker on Ubuntu](/guides/install-docker-ubuntu) or [Install Docker on Debian](/guides/install-docker-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](/guides/caddy-reverse-proxy).
- UDP port 51820 allowed in your provider's network firewall, if it has one.

| Resource | Minimum (official) | Suggested starting point |
|---|---|---|
| CPU | Not published | 1 vCPU |
| RAM | Not published | 1 GB |
| Disk | Not published | 5 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](/guides/nginx-reverse-proxy-certbot) or [Traefik](/guides/traefik-reverse-proxy) 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

> **Warning**
>
> Until the wizard is finished, whoever opens the site first creates the admin account. Complete this step immediately after Step 4.

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](/guides/install-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](https://github.com/wg-easy/wg-easy/releases) 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

- Understand what wg-easy automates by reading [How to set up a WireGuard VPN server](/guides/wireguard-vpn-server).
- Block ads and trackers for all VPN clients with [AdGuard Home](/guides/install-adguard-home).
- Learn more about Compose files in [Docker Compose basics](/guides/docker-compose-basics).
- Compare servers for your own VPN on the [VPN server](/vpn-server) page.
- Read the [wg-easy documentation](https://wg-easy.github.io/wg-easy/latest/) for the API, metrics and advanced configuration.

## 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.

---

Source: <https://hyperdc.com/guides/tutorials/install-wg-easy>\
Updated: 2026-10-09
