DockerSelf-Hosting

wg-easy with Docker: WireGuard VPN and Web UI (Linux server, Windows and Mac clients)

Before you start

  • 19 min read
  • 21 to run in terminal
  • 4 file contents

Boxes marked Run in terminal are what you type; plain text explains why. Check what you need first

What you get in one minute#

wg-easy is a free, self-hosted WireGuard VPN server with a web admin panel: you create clients in a browser, download a config file or scan a QR code, and your phone or laptop connects to your own server.

wg-easy official logo

Logo: the official wg-easy project (github.com/wg-easy/wg-easy)

It suits a VPN for your own devices, a way back into your home network while travelling, or a private tunnel to services you run. The project calls itself "the easiest way to install & manage WireGuard on any Linux host". WireGuard and the Web UI run in one container, and nothing goes through a cloud account.

WhatValue (official docs, read October 2026)
Current versionwg-easy v15.5.0, released October 2026 (latest stable)
Image to pinghcr.io/wg-easy/wg-easy:15.5.0 (checked on the registry)
Time neededAbout 15 minutes for the Compose route (our estimate, not measured)
Size and RAMNot documented by the project
PortsUDP 51820 (WireGuard, forward on your router); TCP 51821 (Web UI, keep local or behind HTTPS)
LicenceAGPL-3.0-only (LICENSE at the release tag)
Official docswg-easy.github.io/wg-easy/latest and the repository

Screenshot of the wg-easy client list with an on/off switch and QR, download and link buttons for each client

Image: the official wg-easy project (README at v15.5.0). Its footer says v15.2.1, so v15.5.0 may look slightly different.

At a glance:

  • What it is: WireGuard plus a web panel in one Docker container. Clients are created, enabled and deleted in the browser.
  • What devices get: a config file or QR code per client, plus a connection list.
  • v15 is a rewrite: most v14 environment variables are gone; settings live in the Admin Panel. Do not reuse a v14 compose file.
  • Pin the tag: the project says latest still points to v14.
  • Platforms: the server must be a Linux host (x86_64 or arm64). Windows and macOS are clients only.
  • Privacy: keys and client configs stay on your server. The docs describe no telemetry; the update check is on by default and can be switched off.

How it fits together#

Devices reach the WireGuard container over one UDP port. The Web UI is a separate TCP port that should stay off the internet.

Two-lane diagram: a phone or laptop connects through a router that forwards UDP 51820 to the wg-easy container, which carries traffic to the home network or internet; below, you reach the Web UI through Caddy on port 443 over HTTPS, and the Web UI writes the config to a folder on the host

Drawn for this guide from the official compose file and docs. Only the router port forward faces the internet; the HTTPS route (dashed) is optional.

Before you start#

You need a Linux server you manage, Docker with Compose, and a way for your clients to reach it.

Linux server: Docker Engine and Docker Compose, an x86_64 or arm64 machine, and curl. The docs ask for a domain name or a public IP address.

Windows: a client only. Check the SSH client used for the tunnel in Step 3:

Run in terminalpowershell
ssh -V

Mac: a client only. Same check in Terminal:

Run in terminalbash
ssh -V

Checklist:

  • [ ] Docker and Docker Compose on the Linux server (docker --version, docker compose version)
  • [ ] x86_64 or arm64 (the docs say armv6 and armv7 cannot run v15)
  • [ ] Admin access to your router, to forward UDP 51820
  • [ ] A domain name or public IP your clients can reach
  • [ ] For the HTTPS section only: a DNS record for the server, and ports 80 and 443 free

Tested by DIY Lab Hub: not yet. The commands come from the official compose file, docs and release files read in October 2026. We did not run them.

Step 1: Create the data folder on the Linux server#

wg-easy keeps its settings and WireGuard keys in one folder. We use ~/docker_volumes/wg-easy, as all DIY Lab Hub guides do.

Linux#

Run in terminalbash
mkdir -p ~/docker_volumes/wg-easy/data
cd ~/docker_volumes/wg-easy
docker --version
docker compose version

Windows (PowerShell)#

Not supported as the server. The docs name Linux hosts, and the container needs the host's WireGuard kernel module and /lib/modules. Run this step on a Linux machine.

Mac (Terminal)#

Not supported as the server. The docs do not list macOS as a host, and we did not test it. Run this step on a Linux machine.

Check it worked. The folder exists:

Run in terminalbash
ls -ld ~/docker_volumes/wg-easy/data

Tip: Install Docker from the official Docker documentation (docs.docker.com/engine/install), which the wg-easy basic installation page links to. Skip install scripts from blogs.

Step 2: Write the Compose file and start wg-easy#

This is the official compose file with three changes: the image is pinned to 15.5.0, the config lives in ./data, and the Web UI is published on 127.0.0.1 only. The official example publishes it on every interface. INSECURE=true is needed only while you open the Web UI over plain HTTP; you remove it in the HTTPS step.

Linux#

Run in terminalbash
cd ~/docker_volumes/wg-easy
cat > docker-compose.yml << 'EOF'
services:
  wg-easy:
    image: ghcr.io/wg-easy/wg-easy:15.5.0
    container_name: wg-easy
    restart: unless-stopped
    environment:
      - INSECURE=true
    networks:
      wg:
        ipv4_address: 10.42.42.42
        ipv6_address: fdcc:ad94:bacf:61a3::2a
    volumes:
      - ./data:/etc/wireguard
      - /lib/modules:/lib/modules:ro
    ports:
      - "51820:51820/udp"
      - "127.0.0.1:51821:51821/tcp"
    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
EOF
docker compose up -d
docker compose ps
docker compose logs --tail=50 wg-easy

Windows (PowerShell)#

Not supported as the server. The same file belongs on a Linux machine; see Step 1.

Mac (Terminal)#

Not supported as the server. See Step 1.

Check it worked. docker compose ps shows wg-easy running. The image's health check runs wg show every minute, so the status should turn healthy after about a minute. The docs publish no expected log text; read the log for errors.

Tip: Use docker compose down and docker compose up -d to change things. The docs warn against start and stop, which can leave the container inconsistent.

Step 3: Finish the setup wizard through an SSH tunnel#

The Web UI now listens only on the server's loopback address. An SSH tunnel from your computer carries your local port to it, so you open nothing on the router.

Linux#

On the server, open http://127.0.0.1:51821. From another Linux computer, open the tunnel and leave it running:

Run in terminalbash
ssh -N -L 51821:127.0.0.1:51821 <your-user>@<your-server-address>

Then open http://localhost:51821 on that computer.

Windows (PowerShell)#

Run the same tunnel from PowerShell and keep the window open:

Run in terminalpowershell
ssh -N -L 51821:127.0.0.1:51821 <your-user>@<your-server-address>

Then open http://localhost:51821. If ssh is missing, install the OpenSSH client from Windows optional features.

Mac (Terminal)#

Same command as Linux:

Run in terminalbash
ssh -N -L 51821:127.0.0.1:51821 <your-user>@<your-server-address>

Then open http://localhost:51821.

The wizard asks for an admin username and password. Choose the new setup, then enter the Host and Port. The Host goes into every client config, so use your domain name or public IP, not a home-network address such as 10.x.x.x. The port is 51820.

Check it worked. You can log in. From the server, docker exec wg-easy wg show lists the wg0 interface and its listen port once the setup has created it.

Tip: Store the admin password in a password manager before you close the wizard. A reset needs shell access to the server.

Step 4: Forward UDP 51820 on your router#

Clients connect on UDP 51820 only. Never forward TCP 51821.

Linux#

On your router, forward UDP 51820 to the server's LAN address. If the server runs a firewall, allow that port only:

Run in terminalbash
sudo ufw allow 51820/udp

ufw is one example; use your own firewall. The docs only say to open UDP 51820.

Windows (PowerShell)#

The router step is identical on every system. Follow the Linux block in your router's web page; there is no server command for Windows.

Mac (Terminal)#

Same as Linux: the router step is the same, and the firewall command runs on the Linux server only.

Check it worked. After Step 5, a client connects and docker exec wg-easy wg show lists that peer with a recent handshake. A test from outside your network is the real proof; a port scan does not prove UDP is open.

Tip: Reach the admin page from outside with the HTTPS section below, not by forwarding TCP 51821.

Step 5: Create the first client and connect it#

Each device gets its own client. For each one you download the config, show a QR code, or use the CLI.

Linux#

Add a client in the Web UI, then download its config or show its QR code. From the server:

Run in terminalbash
cd ~/docker_volumes/wg-easy
docker compose exec -it wg-easy cli clients:list
docker compose exec -it wg-easy cli clients:qr <client_id>

Replace <client_id> with an ID from the list.

Windows (PowerShell)#

Create the client in the Web UI on your PC, download the config and import it into the WireGuard app for Windows (the official install page lists Windows 10, 11 and newer). The file holds a private key; keep it private.

Mac (Terminal)#

Download the config in the Web UI and import it into the WireGuard app from the Mac App Store, listed on the official WireGuard install page. Keep the file private.

Check it worked. Enable the client, connect the app, and run docker exec wg-easy wg show on the server: the peer shows a recent handshake.

Tip: Never paste a client config into a chat or a screenshot. It contains the private key. Show a QR code only to your own phone.

Put it behind HTTPS (with or without Traefik)#

Once setup works, put the Web UI behind HTTPS so it is reached at https://wg-easy.<your-domain> and needs no host port. Local-only users can keep the SSH tunnel and skip this section. Both routes need a domain with a DNS record pointing to the server.

With Traefik#

On a server that already runs Traefik v3 with the Docker provider, add labels and remove the Web UI host port.

This assumes an external Docker network called proxy and an entrypoint called https (the official example names it websecure; use yours). Create the network once:

Run in terminalbash
docker network create proxy

Replace docker-compose.yml with the file below. The interface_name and gw_priority settings follow the official FAQ: with two Docker networks, the interfaces can swap on restart and break routing.

File contentyaml
services:
  wg-easy:
    image: ghcr.io/wg-easy/wg-easy:15.5.0
    container_name: wg-easy
    restart: unless-stopped
    networks:
      wg:
        interface_name: eth0
        gw_priority: 1
        ipv4_address: 10.42.42.42
        ipv6_address: fdcc:ad94:bacf:61a3::2a
      proxy:
        interface_name: eth1
        gw_priority: 0
    volumes:
      - ./data:/etc/wireguard
      - /lib/modules:/lib/modules:ro
    ports:
      - "51820:51820/udp"
    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
    labels:
      - "traefik.enable=true"
      - "traefik.docker.network=proxy"
      - "traefik.http.routers.wg-easy.rule=Host(`wg-easy.<your-domain>`)"
      - "traefik.http.routers.wg-easy.entrypoints=https"
      - "traefik.http.routers.wg-easy.tls=true"
      - "traefik.http.services.wg-easy.loadbalancer.server.port=51821"

networks:
  wg:
    driver: bridge
    enable_ipv6: true
    ipam:
      driver: default
      config:
        - subnet: 10.42.42.0/24
        - subnet: fdcc:ad94:bacf:61a3::/64
  proxy:
    external: true

The INSECURE=true line is gone, as in the official Traefik example. One closed issue (number 2772) reports a Traefik setup that needed it for an OIDC redirect; add it back only if your login loops or redirects to plain HTTP.

Start it with docker compose up -d in ~/docker_volumes/wg-easy.

Check it worked. curl -I https://wg-easy.<your-domain>/ returns an HTTP status line over HTTPS, and the Web UI opens with a valid certificate.

Without Traefik: Caddy#

Choose this if you have no Traefik. Caddy gets and renews its own certificates.

Caddy's docs describe automatic Let's Encrypt certificates for a public domain, and tls internal for a local certificate at home.

Create the Caddy folder:

Run in terminalbash
mkdir -p ~/docker_volumes/caddy/{data,config}
cd ~/docker_volumes/caddy

Save this as ~/docker_volumes/caddy/docker-compose.yml. The image is caddy:2.11.7, the latest Caddy release read in October 2026:

File contentyaml
services:
  caddy:
    image: caddy:2.11.7
    container_name: caddy
    restart: unless-stopped
    ports:
      - "80:80/tcp"
      - "443:443/tcp"
      - "443:443/udp"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      - ./data:/data
      - ./config:/config
    networks:
      - proxy

networks:
  proxy:
    external: true

Save this as ~/docker_volumes/caddy/Caddyfile for a public domain:

File contenttext
wg-easy.example.com {
    reverse_proxy wg-easy:51821
}

For a home-only name, use tls internal instead:

File contenttext
wg-easy.home.arpa {
    tls internal
    reverse_proxy wg-easy:51821
}

Caddy reaches wg-easy by container name on the shared proxy network, so the Web UI needs no host port. In ~/docker_volumes/wg-easy/docker-compose.yml, remove the INSECURE=true lines and the 127.0.0.1:51821 port line, then add the proxy network exactly as in the Traefik example above. Start both stacks:

Run in terminalbash
cd ~/docker_volumes/caddy && docker compose up -d
cd ~/docker_volumes/wg-easy && docker compose up -d

For a home-only certificate, Caddy's docs say that in a container you install the root certificate on each device yourself, with caddy trust or by copying it out. The exact path varies, so locate it first:

Run in terminalbash
docker exec caddy find /data -name root.crt
docker cp caddy:<path-from-find> ./caddy-root.crt

Install caddy-root.crt as a trusted root on each device. These OS steps are not from the wg-easy or Caddy docs: on Windows, use "Trusted Root Certification Authorities"; on macOS, Keychain Access with "Always Trust"; on Linux, your distribution's CA store.

Windows and Mac are clients here: run docker cp and the Caddy commands on the Linux server only.

Check it worked. curl -I https://wg-easy.example.com/ returns an HTTP status line over HTTPS. For a home-only name, the browser shows a padlock only after the root is trusted on that device.

Tip: Let's Encrypt needs the DNS record to point at the server and ports 80 and 443 to reach it. A certificate error usually means the name in the Caddyfile differs from the DNS record.

Security and testing#

Once the Web UI is reachable from outside, it is an admin panel on the internet. These controls matter most.

ControlHow
Admin passwordLong and unique. Enable 2FA (TOTP) on the Account page; disabling needs the current password.
Web UI off the internetKeep 127.0.0.1 (Step 2) or no host port behind a proxy. Never forward 51821.
WireGuard portForward only UDP 51820.
Config folder~/docker_volumes/wg-easy/data holds server and client private keys. Readable only by you.
BackupsTreat as secret: chmod 600, never in a shared folder.
Rate limitsNot built in. The docs ask you to limit /api/auth/password, /api/auth/verify-2fa and /cnf/* in Caddy or Traefik.
Client IPs behind a proxySet TRUSTED_PROXIES to the proxy address only. Invalid values stop the container.
Per-client firewallExperimental per the docs; needs iptables and ip6tables on the host. Test before relying on it.

Quick tests on Linux after each change:

Run in terminalbash
cd ~/docker_volumes/wg-easy
docker compose ps
docker compose logs --tail=100 wg-easy
docker exec wg-easy wg show

Windows and Mac can run curl -I https://wg-easy.<your-domain>/ in PowerShell or Terminal (curl.exe ships with Windows 10 and 11).

What leaves the machine: the WireGuard traffic you send through the tunnel, and the update check. DISABLE_VERSION_CHECK defaults to false; the docs do not name the server it contacts. Set DISABLE_VERSION_CHECK=true in environment: to switch it off. The README also lists Gravatar avatars, which the docs do not describe.

Common mistakes#

These come from the official issue tracker, the docs' own warnings, and common AI assistant answers.

MistakeWhy it happensDo this instead
Using the latest tagThe docs say latest still points to v14. Issues #2405 (closed January 2026) and #2167 (open) report itPin 15.5.0 and change the tag yourself when you update
Keeping v14 settings in a v15 filev15 uses other variables; most settings moved to the Admin Panel. Issue #2262 reports Error: WG_HOST Environment Variable Not Set! after a v14-style setupStart from this guide's file; set the rest in the Admin Panel. For migration, use the official v14 to v15 guide
Publishing the Web UI on every interfaceThe official example maps 51821:51821/tcp with no address, so it listens everywhere; behind a port forward that means the internetPublish 127.0.0.1:51821:51821/tcp, or use HTTPS with no UI port
Forwarding TCP instead of UDPWireGuard uses UDP; the docs list only "UDP 51820"Forward UDP 51820 and nothing else for the tunnel
Using a home-network address as the HostThe wizard Host goes into every client config, so an internal address works only at homeUse a domain name or public IP, with dynamic DNS if your IP changes
Copying a compose file from another issueIssue #2499 (closed March 2026) reports wrong-login errors after a copied file was editedCopy from the official docs or this guide, and read each variable
Adding a second Docker network without interface settingsInterfaces can swap on restart and break routing. Issue #1795 is openSet interface_name and gw_priority as in the FAQ and the HTTPS section
Running docker compose stop and startThe docs warn this can leave the container inconsistentUse docker compose down and docker compose up -d
Running the server on Windows or macOSThe docs name Linux hosts only. Issue #1195 (closed August 2024) describes a Docker for Mac networking change that broke wg-easyRun the server on Linux; use Windows or Mac as clients
AI assistants: writing v14 variables such as PASSWORD_HASH or WG_HOST into v15Assistants often copy v14 examples; v15 does not read themUse only variables listed in the official optional configuration page
AI assistants: recommending latest or an unpinned imageMany examples use latest, which the project says to avoidPin 15.5.0 (or 15 if you accept minor updates) and read the release notes

Tip: Before copying a compose file, check the image tag and variable names against the official file at the release tag you pin. Those two lines cause most v15 breakage.

Tip: Keep the Web UI on 127.0.0.1 until HTTPS works. The SSH tunnel is safer than an open port while you learn the setup.

Tip: After every change run docker compose logs --tail=100 wg-easy. Most errors print there with the exact text.

Troubleshooting#

ProblemSolution
Unexpected behaviour after an update or new installCheck the running image with docker inspect wg-easy (Config.Image). Pin ghcr.io/wg-easy/wg-easy:15.5.0, then docker compose up -d
Error: WG_HOST Environment Variable Not Set!v14 settings are still in the file. Remove them and set the host in the wizard or Admin Panel (issue #2262)
Wrong username or passwordReset the admin password with the CLI (below)
Lost the 2FA code after a password resetThe reset did not clear 2FA in the reported case (issue #2460, closed February 2026). Check your version's release notes
Clients connect but get no internetTwo Docker networks may have swapped interfaces. Set interface_name and gw_priority (FAQ); see issue #1795
Connection stops after docker compose restartIssue #2208 reports this (closed February 2026). Use down then up -d, and read the logs
Cannot find device "wg0"The WireGuard kernel module is not loaded: sudo modprobe wireguard (official FAQ), then restart
can't initialize iptables table natsudo modprobe iptable_nat (official FAQ). Some newer kernels lack the module (issue #2645)
Container restarts in a loop after a kernel updateRead docker compose logs --tail=200 wg-easy. Issues #2645 (Raspberry Pi kernel 6.18) and #2627 describe missing legacy iptables modules
H3Error: Command failed: wg show wg0 dumpReported on v15.3.0 (issue #2697, closed July 2026). Check you run the latest 15.x and compare with the issue
UI does not open over plain HTTPAdd INSECURE=true to environment: (docs: plain HTTP needs it). Remove it when HTTPS is in front

The official FAQ error texts are Error: WireGuard exited with the error: Cannot find device "wg0" and can't initialize iptables table `nat': Table does not exist (do you need to insmod?).

Resetting the admin password#

The docs use the CLI inside the container. Enter the new password at the prompt:

Run in terminalbash
cd ~/docker_volumes/wg-easy
docker compose exec -it wg-easy cli db:admin:reset

Or pass it as an argument, escaping special characters for your shell:

Run in terminalbash
docker compose exec -it wg-easy cli db:admin:reset --password <new_password>

Known bugs (open, October 2026)#

  • Issue #2167: the latest tag lagged behind the newest 15.x release. Pin 15.5.0.
  • Issue #1795: internet routing fails with several Docker networks. Use the FAQ interface settings.
  • Issue #2622 (feature request): v15 forces the server address to .1 in a shared WireGuard network.

Run docker compose ps, docker compose logs --tail=200 wg-easy, docker inspect wg-easy and docker exec wg-easy wg show first.

Updates and backups#

Update. Read the release notes, change the tag in docker-compose.yml, then:

Run in terminalbash
cd ~/docker_volumes/wg-easy
docker compose pull
docker compose up -d
docker compose logs --tail=100 wg-easy

Back up before each update. Upgrades from 15.3.0 to 15.4.0 had failure reports in August 2026 (issues #2738 and #2777, both closed).

Back up. The Web UI Backup button downloads wg0.json (the docs use it for migration). For the full folder, stop with down (not stop), archive, and start again:

Run in terminalbash
cd ~/docker_volumes/wg-easy
docker compose down
mkdir -p backups
tar -czf "backups/wg-easy-$(date +%F).tar.gz" data
chmod 600 backups/wg-easy-*.tar.gz
docker compose up -d

To run it nightly at 03:00 on Linux, add a crontab entry that runs the same commands. Keep a copy of the archive on another machine.

Restore test. Unpack one archive into a new empty folder, never over the live one, and check that wg0.json and the keys are present:

Run in terminalbash
mkdir -p /tmp/restore-test && tar -xzf backups/wg-easy-<date>.tar.gz -C /tmp/restore-test
ls /tmp/restore-test/data

Windows (PowerShell)#

Not for the server. Copy the archive off the Linux server from your PC:

Run in terminalpowershell
scp <your-user>@<your-server-address>:~/docker_volumes/wg-easy/backups/wg-easy-<date>.tar.gz .

Mac (Terminal)#

Use the same scp command from Terminal.

Performance and resources#

The docs publish no memory, CPU or disk figures, so this guide gives none. Measure your own server:

  • Resources: docker stats wg-easy shows live CPU and memory.
  • Health: the image's check runs wg show every minute; docker compose ps shows the result.
  • Clients: the Web UI shows connected clients and per-client transfer charts.
  • Metrics: the README lists Prometheus support; this guide does not cover it.
  • Several instances: WG_INTERFACE sets the interface name (default wg0).

FAQ#

Is wg-easy free to use? Yes, under AGPL-3.0-only. If you modify it and offer it to others over a network, the AGPL asks you to share your changes.

Does it need a GPU? The docs do not mention one. They list x86_64 and arm64 as supported.

Can I run it on a Raspberry Pi? On a 64-bit arm64 system, yes according to the docs. armv6 and armv7 cannot run v15. Kernel updates have broken the container on Raspberry Pi (issue #2645).

Does it send my data to the project? The docs describe no telemetry. The update check is on by default; DISABLE_VERSION_CHECK=true turns it off. The docs do not name the server it contacts.

Can I use Windows or macOS as the server? No. The docs say "any Linux host". Use Windows or Mac as clients.

Can I skip the reverse proxy? Yes, if the Web UI stays on your local network. The docs call that route insecure and ask for HTTPS when the UI is reachable from outside.

Sources#

Checked against the official docs on 2026-10-30.