wg-easy v15 (2026): Docker Compose Setup and a Locked-Down Panel

Install wg-easy v15 with Docker Compose: the current file, the setup wizard that replaced PASSWORD_HASH, v14 migration, 2FA, and a panel kept off the internet.

By Eric GerardFounder · VPNSmith - Self-host VPN & GDPR VPS specialist

Updated 14 min read14 min

Affiliate links: we may earn a commission, at no extra cost to you. Method

Generate my docker-composeRead the guide

fig. 01 / self-host-vpnencrypted tunnelUDP 51820DEVICE10.8.0.2SERVER10.8.0.1INTERNETNATAllowedIPs = 0.0.0.0/0, ::/0ListenPort = 51820 (server)
fig. 01 / self-host-vpn

wg-easy is a free Docker image that runs WireGuard with a web admin panel on your own server: you add devices, download their configs and show QR codes from the browser instead of editing wg0.conf. Version 15 is current; it needs a VPS with UDP 51820 open.

fig. 01 / self-host-vpnencrypted tunnelUDP 51820DEVICE10.8.0.2SERVER10.8.0.1INTERNETNATAllowedIPs = 0.0.0.0/0, ::/0ListenPort = 51820 (server)
fig. 01 / self-host-vpn

WireGuard is fast and modern, but the stock setup is files and the command line: generate keys, hand-edit wg0.conf, run wg set to add each device, copy configs around. For one device that is fine. For a family, a few laptops and a couple of phones, it gets tedious - and tedious is where mistakes (and leaked keys) happen. wg-easy solves exactly that: it keeps the real WireGuard underneath, untouched, and bolts a clean web dashboard on top. Add a client in two clicks, show a QR code for a phone, watch who is connected and how much they are transferring. This is the full 2026 setup, including the password change that trips up everyone upgrading.

What you need: a Linux VPS with a public IPv4 address and Docker, with UDP 51820 open. The lowest monthly price we read for such a box is HostPapa Unmanaged VPS Lite (affiliate link), 2.95 $ a month on a 3-year term, then the regular rate; this guide uses a Contabo Cloud VPS as its example host; the host requirements are detailed below.

Still choosing the server? Our cheapest-VPS ranking for WireGuard compares what each provider really bills for a usable endpoint.

Tip: if the tunnel refuses to connect, the cause is almost always the port. See our guide to WireGuard's default port (UDP 51820) and how to open, forward or change it.

Skip the copy-paste: the free wg-easy docker-compose generator writes the v15 docker-compose.yml for your setup in the browser: admin panel on localhost only or HTTPS on your own domain through Caddy, and an optional unattended first start.

Generate your wg-easy v15 docker-compose.yml here

Enter your server address and how you reach the panel: you get the compose file, the Caddyfile if needed and the install commands.

01Describe your server

Everything is generated in your browser. Nothing you type, including the password, is sent to our servers.

The address WireGuard clients connect to.

How will you reach the admin panel?

An A record for this name must point to the server, with ports 80 and 443 open.

Without unattended setup, open the panel after the first start: the wizard asks for the admin account, then the host and port. Enter the same UDP port as above.

02Copy the files

docker-compose.yml
# wg-easy v15 - generated on vpnsmith.com/en/tools/wg-easy-compose-generator
# Host: vpn.example.com - WireGuard port: 51820/udp - panel: https://wg.example.com

volumes:
  etc_wireguard:
  caddy_data:
  caddy_config:

services:
  wg-easy:
    image: ghcr.io/wg-easy/wg-easy:15
    container_name: wg-easy
    environment:
      - PORT=80
      - TRUSTED_PROXIES=10.42.42.43,fdcc:ad94:bacf:61a3::2b
    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"   # WireGuard: the only port clients need
    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

  caddy:
    image: caddy:2-alpine
    container_name: caddy
    ports:
      - "80:80/tcp"
      - "443:443/tcp"
      - "443:443/udp"
    networks:
      wg:
        ipv4_address: 10.42.42.43
        ipv6_address: fdcc:ad94:bacf:61a3::2b
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      - caddy_data:/data
      - caddy_config:/config
    restart: unless-stopped

networks:
  wg:
    driver: bridge
    enable_ipv6: true
    ipam:
      driver: default
      config:
        - subnet: 10.42.42.0/24
        - subnet: fdcc:ad94:bacf:61a3::/64
Caddyfile
wg.example.com {
	reverse_proxy wg-easy:80
}

03Run the install commands

install.sh
# 1. Firewall: SSH first, then the WireGuard port
sudo ufw allow OpenSSH
sudo ufw allow 51820/udp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw allow 443/udp
sudo ufw enable
# Note: ports published by Docker bypass ufw. Also open/close them in your provider firewall.

# 2. Create the folder and paste the generated file(s)
sudo mkdir -p /etc/docker/containers/wg-easy
cd /etc/docker/containers/wg-easy
sudo nano docker-compose.yml
sudo nano Caddyfile

# 3. Start wg-easy
sudo docker compose up -d
# Watch the logs (Ctrl+C to quit)
sudo docker compose logs -f wg-easy

# 4. Open the panel: https://wg.example.com

This file needs a server with a public IP

A Contabo Cloud VPS 4 (4 vCPU, 8 GB RAM, dedicated IPv4, unlimited traffic) costs 5,50 € a month on a 24-month term (price checked 17 Oct 2026), which covers all wg-easy needs.

Affiliate link: we earn a commission at no extra cost to you.

Card declined, or prefer a US host? Ultahost VPS Basic is 5.52 $/mo on 24 months (read on 21 October 2026) and also takes Bitcoin, Alipay, WeChat Pay and Qiwi; check its terms for your country. See Ultahost VPS

See the plan at Contabo

What wg-easy actually is (and isn't)

wg-easy is a single Docker container that runs a WireGuard server and a small web application to manage it. The important part to understand up front: it does not replace or re-implement WireGuard. The tunnel is still the same kernel WireGuard, with the same keys and the same protocol on the wire. wg-easy is management, not a new VPN. That has two consequences:

  • No speed penalty. Throughput and latency are identical to a hand-written config, because the data path never touches the web app.
  • The web UI is the attack surface. The dashboard can create clients, so whoever controls it controls your VPN. Protecting that panel is the whole security story (more below).

What you get for that: a browser page listing every client, one-click create/disable/delete, a QR code per client for phones, and a live per-peer transfer counter. For onboarding a handful of devices without touching a terminal, nothing is faster.

Turquoise fibre-optic cables looping down into a row of white connectors on rack-mounted equipment
Turquoise fibre-optic cables looping down into a row of white connectors on rack-mounted equipment

Before you start: the host you need

wg-easy is a container, so you need a Linux host that can run Docker and has a public IP. For a VPN you reach from anywhere, that means a small VPS - WireGuard is so light that a 4-6 €/month instance saturates its network port long before the CPU notices. Contabo is the example host here for its price-to-bandwidth ratio, and the bare-metal walkthrough is in our Contabo + WireGuard self-host guide if you ever want to compare the manual route.

If price is what decides, the lowest sticker we found is HostPapa's Unmanaged VPS Lite (affiliate link), read on its VPS page on 28 October 2026: 1 CPU core, 1 GB of RAM, 25 GB of NVMe, 4 TB of monthly transfer and root SSH access, 2.95 $ a month with a 3-year term, then the regular rate, with a 30-day money-back guarantee. Root access means you install Docker yourself, and 1 GB holds Docker plus the wg-easy container for a personal VPN. HostPapa does not publish the port speed, so confirm the public IPv4 and the renewal price at checkout.

If you would rather have more headroom than the lowest price, Contabo is the host this guide was written against.

Get a Contabo VPS to run wg-easy →

A second option if you would rather have a 30-day refund window: Ultahost's VPS Basic, read on ultahost.com on 21 October 2026 at 5.52 $ a month billed for 24 months (list 9.19 $), with 1 CPU core, 1 GB of DDR5, 30 GB of NVMe, a dedicated IPv4 and a 30-day money-back guarantee. 1 GB is enough for Docker plus the wg-easy container on a personal VPN. The plan publishes no port speed or traffic allowance, so check those before you rely on it for heavy use.

On the host you need Docker and the Compose plugin installed, the WireGuard kernel module available (every modern distro, Linux 5.6+, ships it), and the ability to open UDP 51820 in both the host firewall and your provider's security group.

The docker-compose file (wg-easy v15)

Version check (24 October 2026): the current release is v15.5.0 (24 October 2026). v15 is a full rewrite: WG_HOST, PASSWORD_HASH and most other v14 variables are gone, and their settings now live in the web UI. A v14 tutorial pasted onto the :latest image starts a container that silently ignores your host and password. Everything below is the v15 way, taken from the project's own compose file and docs.

v15.5.0 in one minute: it ships the October 2026 Node.js security fixes, so update even if nothing is broken: sudo docker compose pull && sudo docker compose up -d (the :15 tag used below follows the new release, and your clients and keys stay in the volume). It also moves its built-in AmneziaWG support to version 3.1, adds WG_INTERFACE (default wg0) to run two instances on one host, and adds TRUSTED_PROXIES for a panel behind a reverse proxy (see the lock-down section). Source: the v15.5.0 release notes and the project's optional configuration docs.

This file follows the official one, with one deliberate change: the web UI is published on 127.0.0.1 only, so the admin panel is never reachable from the internet and you open it through an SSH tunnel. Want an HTTPS panel on your own domain through Caddy, or a fully unattended first start? Our wg-easy docker-compose generator writes that variant for you.

services:
  wg-easy:
    image: ghcr.io/wg-easy/wg-easy:15
    container_name: wg-easy
    environment:
      - INSECURE=true   # panel over plain HTTP, reached only through an SSH tunnel
    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"             # WireGuard data: open to the internet
      - "127.0.0.1:51821:51821/tcp"   # web UI: bound to localhost only
    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

volumes:
  etc_wireguard:

What each part is doing, and where it usually breaks:

  • 127.0.0.1:51821:51821/tcp keeps the panel off the public interface. This matters more than it looks: ports that Docker publishes bypass UFW, so a plain "51821:51821/tcp" is open to the world even if ufw status says otherwise.
  • INSECURE=true is required because the panel is served over plain HTTP here (the encryption is the SSH tunnel). Without it, v15 marks its login cookie Secure, the browser drops it over HTTP, and you land back on the login page after every attempt.
  • cap_add: NET_ADMIN and sysctls: net.ipv4.ip_forward=1 let the container build the WireGuard interface and route for your clients. Missing either gives you a tunnel that connects and then routes nothing.
  • The named volume etc_wireguard holds the database with every key and client, so docker compose down followed by up keeps your devices.

Put it in place and start it, using the directory layout of the official docs:

sudo mkdir -p /etc/docker/containers/wg-easy
cd /etc/docker/containers/wg-easy
sudo nano docker-compose.yml   # paste the file above
sudo docker compose up -d
sudo docker compose logs -f wg-easy   # Ctrl+C once it is listening

Then reach the panel from your own machine through the tunnel:

# on your own computer, not on the server
ssh -L 51821:127.0.0.1:51821 user@YOUR_SERVER_IP
# then open http://localhost:51821 in your browser

First start: the setup wizard (no more PASSWORD_HASH)

The bcrypt PASSWORD_HASH and the wgpw command that every v14 tutorial spends a section on are gone. On the first visit, v15 opens a setup wizard instead:

  1. Create the admin account: a username and a password. wg-easy stores the password itself as an argon2 hash, so there is nothing to generate or escape in a .env file.
  2. "Do you have an existing setup?" Answer No for a fresh install (Yes is the v14 import, below).
  3. Host and port: the public IP or domain your clients will dial (an IPv6 address goes in brackets, such as [2001:db8::1]) and the UDP port, 51820 unless you changed the mapping. Clients get this exact value as their Endpoint.

DNS, the client address range and the default allowed IPs are now set in the Admin Panel, not in environment variables. To skip the wizard entirely (Ansible, cloud-init), v15 reads INIT_ENABLED=true with INIT_USERNAME, INIT_PASSWORD, INIT_HOST and INIT_PORT on the very first start only; remove them once the container is up so the password does not sit in the file. The generator can write those lines for you.

Locked out? Reset the admin password from the server:

cd /etc/docker/containers/wg-easy
sudo docker compose exec -it wg-easy cli db:admin:reset

Coming from v14

There is no in-place upgrade. In the old panel, click Backup to download wg0.json and note your old environment variables, run docker compose down, start the v15 file above, then answer Yes in the wizard and upload wg0.json: keys and clients come across, the old variables do not, so set DNS and allowed IPs again in the Admin Panel. One hard limit from the migration guide: armv6 and armv7 hosts (older Raspberry Pi) cannot move to v15 and have to stay pinned on :14.

Staying on v14, or leaving the panel altogether? The same backup file can be pasted into WireGuard Manager, our hosted dashboard (free for 1 server and 3 peers): since wg0.json holds each client's private key, every device's .conf and QR code are available again at once, and it writes a plain wg0.conf for wg-quick if you retire wg-easy. Nothing on the server changes when you import.

Add your first client

Inside the dashboard, click New Client, give it a name (e.g. laptop, pixel-phone), and wg-easy generates the keypair and a ready config. For a computer, download the .conf and import it into the WireGuard app or config templates; for a phone, click the QR icon and scan it with the WireGuard mobile app - no typing, no file transfer. The per-client toggle disables a device instantly without deleting it, which is the right way to revoke a lost phone. More on this in PiVPN Client Management.

Client imported but nothing works? Three causes cover almost every case, and each has a tool. Status stays at "no handshake": UDP 51820 is not reaching the container, so run the WireGuard port checker, which writes the firewall rule for your host (ufw, firewalld, nftables, iptables) and a tcpdump test that shows whether the packets arrive. Handshake fine but pages hang or load half-way: an MTU problem, which the MTU calculator sizes for your link (set the result in the client config). Want only some traffic in the tunnel, or keep your home LAN reachable: the AllowedIPs calculator gives the exact list to paste into wg-easy's default allowed IPs.

Locking down the admin panel - do this, not later

The web UI can mint VPN clients, so an exposed, weakly-protected panel is the real danger with wg-easy. Three layers, in order of preference:

  1. Never publish 51821 to the internet. In your VPS provider's firewall, leave UDP 51820 open and keep TCP 51821 closed to the world. Reach the panel only from your own IP, or - cleanest - connect to the VPN first and browse to the dashboard over the tunnel, so the admin port is never internet-facing at all.

  2. Use a strong password, then turn on 2FA. v15 has TOTP two-factor login: open the menu at the top right, then Account, enable two-factor authentication and scan the QR code with your authenticator app. Treat the panel login like a server root password.

  3. Put TLS in front of it if you must reach it remotely: a reverse proxy (Caddy, Traefik, nginx) terminating HTTPS on a real certificate, ideally with an allow-list. Plain HTTP on a public port is the one configuration to avoid.

    Since v15.5.0, also set TRUSTED_PROXIES to your proxy's source address (for a Caddy container on the same Docker network, its IP on that network). wg-easy then reads X-Forwarded-Host and X-Forwarded-For from that address only, and failed logins are logged with the real client IP, so a tool such as CrowdSec bans the attacker rather than your proxy. A malformed address stops the container from starting: check docker compose logs after the change.

A self-hosted VPN that any passer-by can add themselves to is worse than no VPN. The panel is the keys to the kingdom - protect it accordingly.

wg-easy vs the alternatives

  • vs PiVPN - PiVPN installs WireGuard straight on the host and manages peers from the terminal (pivpn add, pivpn -qr). No container, no web UI. Pick PiVPN for a lean, scriptable, bare-metal box; pick wg-easy when you genuinely want a clickable dashboard and live stats.
  • vs a hand-written config - Editing wg0.conf yourself is the most transparent and lightest option, ideal for a single-purpose VPN box and infrastructure-as-code. wg-easy trades a little extra surface (the web app) for fast, no-CLI client management.
  • vs other self-host stacks - If you want a mesh (every device talking directly, NAT traversal handled for you) rather than a classic hub-and-spoke server, wg-easy is the wrong shape; that is Tailscale/Headscale/NetBird territory, compared in our best self-host VPN guide.

wg-easy also runs happily alongside other containers, which is why it pairs naturally with a broader WireGuard-in-Docker setup.

When wg-easy is the right call - and when it isn't

Use wg-easy when you onboard several people or devices, want QR-code phone setup, like seeing live connection status, or simply prefer a panel to a config file. It turns "set up a VPN client" from a five-minute CLI chore into two clicks.

Skip wg-easy when the box does nothing but serve one or two WireGuard peers (a hand-written config is simpler and has one less moving part), when you can't safely keep the admin port off the public internet, or when you actually want a mesh rather than a server. For everyone else, wg-easy is the fastest honest path from a fresh VPS to a working, manageable WireGuard VPN.

A VPN changes your IP. It does not change your browser.

A tunnel replaces the address a service sees. It changes nothing about what your browser announces on its own - fonts, screen, time zone, graphics stack - and that combination is often stable enough to recognise you across sessions, tunnel or no tunnel.

Measure your own browser fingerprint - passive, no questions, no account, no email.

The honest limitation

wg-easy makes WireGuard easier to manage, not more private or more secure than WireGuard already is. It shares the host kernel, runs with NET_ADMIN, and is exactly as locked-down as the host and the admin panel you put in front of it. It does not audit your clients, does not stop DNS leaks on its own (set DNS in the Admin Panel and verify), and does not protect you if you leave 51821 open. Treat it as a convenience layer over real WireGuard: deploy it, harden the host and the panel, keep the data port public and the admin port private - and you have a self-hosted VPN you can actually run for a household without living in the terminal.

Affiliate link

Price checked 17 Oct 2026, 24-month term

Frequently asked questions

What is wg-easy?
wg-easy is an open-source project that wraps WireGuard in a small web interface. You run it as a single Docker container; it manages the WireGuard server for you and gives you a browser dashboard to add and remove clients, download or QR-scan their configs, and watch live upload/download per peer. The encryption is still plain WireGuard running in the host kernel - wg-easy only handles peer management and the UI, so there is no performance penalty versus a hand-written config.
Is wg-easy safe to expose to the internet?
Only the WireGuard data port (UDP 51820 by default) should face the internet. The admin web port (51821) must NOT be open to the world - anyone who reaches it and guesses or brute-forces the password can mint themselves a VPN client. Lock the web port to your own IP with a firewall rule, put it behind a reverse proxy with TLS, or reach it over the tunnel itself. In v15, bind the panel to 127.0.0.1 and reach it through an SSH tunnel, or serve it over HTTPS, and enable the built-in two-factor login: an exposed panel is the main risk.
What is the difference between wg-easy and PiVPN?
Both make WireGuard easy, but differently. PiVPN is an installer script that sets up WireGuard (or OpenVPN) directly on the host and gives you CLI commands (pivpn add, pivpn -qr) to manage peers - no container, no web UI. wg-easy runs as a Docker container and its whole point is the browser dashboard. Choose PiVPN if you like a bare-metal, terminal-driven setup on a Raspberry Pi or VPS; choose wg-easy if you want a clickable panel and live stats.
Do I still need a VPS for wg-easy?
If you want a VPN you can connect to from anywhere with a stable public IP, yes - wg-easy still needs a host with a routable address, exactly like any WireGuard server. A small VPS (4-6 €/month) is the standard choice and saturates its uplink with WireGuard easily. For remote access to a home network only, you can run wg-easy on a home server behind router port-forwarding instead, but you lose the clean static IP.
Why does wg-easy connect but route no traffic?
Almost always the host firewall or IP forwarding. The container needs the NET_ADMIN capability and the host must forward packets (net.ipv4.ip_forward=1) - wg-easy sets these in its compose file, but a cloud provider's separate security group can still block UDP 51820. Open that port in the provider's firewall, not just on the host. If the handshake completes but pages still hang, the next suspect is MTU rather than wg-easy itself.