Deploy a Private WireGuard VPN with Docker Compose (QR Codes for Mobile)

Why this guide

WireGuard is a modern VPN that is fast, secure, and simple to manage. Running it in Docker keeps your host clean, makes upgrades trivial, and allows you to back up your configuration as plain files. In this tutorial, you will deploy a production-ready WireGuard VPN with Docker Compose on an Ubuntu server, generate QR codes for easy mobile onboarding, and enable best-practice settings like IPv6 forwarding and DNS control.

Prerequisites

You need an Ubuntu 22.04/24.04 host (cloud VM or home server), a public DNS name for the server (e.g., vpn.example.com), and permission to forward UDP port 51820 on your router if you are behind NAT. You will also need a non-root user with sudo privileges. Windows or macOS clients can connect too, but we will demonstrate mobile setup using QR codes as it is the quickest way to get started.

Step 1 — Install Docker and Compose Plugin

Update your host, install Docker, and add your user to the docker group so you can run it without sudo.

sudo apt update && sudo apt -y upgrade
sudo apt -y install docker.io docker-compose-plugin
sudo systemctl enable --now docker
sudo usermod -aG docker $USER
# Re-log or run: newgrp docker

Step 2 — Create the project structure

We will store compose files in /srv/wireguard and persist configuration in /srv/wireguard/config. The container will keep keys and peer files under this directory, which makes backups easy.

sudo mkdir -p /srv/wireguard/config
sudo chown -R $USER:$USER /srv/wireguard

Step 3 — Write docker-compose.yml

Create a Compose file that uses the well-maintained LinuxServer.io WireGuard image. Replace vpn.example.com with your DNS name and adjust timezone and peer names to your needs.

nano /srv/wireguard/docker-compose.yml

version: "3.8"
services:
  wireguard:
    image: lscr.io/linuxserver/wireguard:latest
    container_name: wireguard
    cap_add:
     - NET_ADMIN
     - SYS_MODULE
    ports:
     - 51820:51820/udp
    volumes:
     - ./config:/config
     - /lib/modules:/lib/modules:ro
    environment:
     - PUID=1000
     - PGID=1000
     - TZ=Etc/UTC
     - SERVERURL=vpn.example.com
     - SERVERPORT=51820
     - PEERS=phone,laptop
     - PEERDNS=1.1.1.1
     - INTERNAL_SUBNET=10.13.13.0
     - ALLOWEDIPS=0.0.0.0/0,::/0
    sysctls:
     - net.ipv4.conf.all.src_valid_mark=1
     - net.ipv4.ip_forward=1
     - net.ipv6.conf.all.forwarding=1
    restart: unless-stopped

A few notes: PEERS seeds the initial clients; you can add more later. ALLOWEDIPS controls routing. With 0.0.0.0/0,::/0 the client routes all traffic through the VPN (full tunnel). For a split tunnel to only reach the VPN subnet, set 10.13.13.0/24 (and optionally fd00:13:13::/64 if you use IPv6).

Step 4 — Open the firewall and forward the port

If UFW is enabled on the host, allow UDP 51820. Also forward UDP 51820 on your router to the server’s LAN IP. If you use a cloud VM, open the UDP port in your provider’s security group.

sudo ufw allow 51820/udp

Step 5 — Start the stack

Bring the container up and follow logs. On the first start, it creates server keys and peer files under config/.

cd /srv/wireguard
docker compose up -d
docker compose logs -f

Step 6 — Get peer configs and QR codes

The image includes helper scripts. To display a peer config and its QR code, run:

docker exec -it wireguard /app/show-peer phone

Install the WireGuard app on iOS or Android, tap the plus button, choose “Scan from QR code,” and scan the code from your terminal. For Windows/macOS/Linux clients, copy the text config printed by the command above into a file like phone.conf and import it in the WireGuard desktop app.

To add a new peer at any time, use:

docker exec -it wireguard /app/add-peer tablet

Step 7 — Verify the connection

Activate the tunnel on your device. From the server, confirm the handshake:

docker exec wireguard wg show

You should see latest handshake times and transfer counters increase as you pass traffic. From the client, visit https://ifconfig.io to confirm your public IP matches the server and that DNS resolves as expected.

Optional: Tune routing, MTU, and DNS

If you only want to reach resources on your home network and keep general browsing on the local internet, change ALLOWEDIPS in the peer config to the private ranges you care about (for example, 10.13.13.0/24,192.168.1.0/24). For mobile networks with strict NAT, enable a keepalive in the peer config by adding PersistentKeepalive = 25. If you notice slow speeds, set MTU = 1280 in the peer config to avoid fragmentation on cellular carriers.

For ad blocking, set PEERDNS to your Pi-hole or AdGuard Home address reachable through the tunnel, e.g., 10.13.13.2. You can also use privacy resolvers like 1.1.1.1 or 9.9.9.9.

Backups and updates

The critical state lives in /srv/wireguard/config. Back it up regularly with your favorite tool (rsync, Restic, Borg). To upgrade safely, pull the new image and recreate the container; your config remains intact.

cd /srv/wireguard
docker compose pull
docker compose up -d

Troubleshooting

No handshake? Verify the UDP port forward and make sure your DNS record points to the right public IP. On mobile networks behind Carrier Grade NAT, incoming connections may be blocked—host the server on a cloud VM or use a home ISP with a public IP. If the tunnel connects but no traffic flows, confirm IP forwarding is enabled (the Compose file includes sysctls) and that ALLOWEDIPS is correct on both ends. For double NAT routers, enable a full-cone/endpoint-independent NAT if available, or use an alternate UDP port like 51821.

You are done

With Docker Compose and WireGuard, you now have a lightweight, fast VPN that you can maintain in minutes. Add peers with one command, scan a QR code on your phone, and enjoy a private, encrypted tunnel wherever you are. Keep your system updated, back up the config folder, and you will have a reliable VPN for the long run.

Comments