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.

How to Build a Zero-Config Mesh VPN with Tailscale for Secure Remote Access

Overview

Tired of port forwarding, dynamic DNS, and brittle VPN configs? Tailscale gives you a zero-config mesh VPN built on WireGuard, letting your devices talk to each other securely from anywhere. In this step-by-step guide, you will install Tailscale on Linux, Windows, and macOS, enable MagicDNS, set up an exit node, publish LAN subnets, and lock everything down with access controls. By the end, you will have a private, encrypted network for your home lab or remote team that takes minutes to deploy and scales without hassle.

Prerequisites

You need a Tailscale account (Google, Microsoft, GitHub, or email sign-in), admin access to your devices, and a stable internet connection. For subnet routing and exit nodes, a Linux or always-on device is recommended. Enable multi-factor authentication in your identity provider for best security.

Step 1: Create Your Tailnet

Go to the Tailscale website and sign in to create your tailnet. This is your private network. Open the Admin Console and confirm your tailnet name. Under Settings, enable device approvals if you want manual approval before new devices join. This is useful for production and shared environments.

Step 2: Install Tailscale

Linux (Debian/Ubuntu)
Run:
curl -fsSL https://tailscale.com/install.sh | sh
sudo tailscale up
When prompted, sign in to link the device. If your distro uses a service, ensure tailscaled is running.

Linux (Fedora/RHEL derivatives)
Install and start:
sudo dnf install tailscale -y
sudo systemctl enable --now tailscaled
sudo tailscale up

Windows
Download the Windows client from Tailscale, install it, and sign in. The client will assign a 100.x Tailscale IP and show your hostname in the Admin Console.

macOS
Install the app from the Mac App Store or from Tailscale’s website. Sign in to connect the Mac to your tailnet.

iOS/Android
Install the mobile app and sign in. You can toggle the VPN on/off and optionally route all traffic through an exit node.

Step 3: Turn On MagicDNS (Human-Friendly Names)

In the Admin Console, open Settings → DNS and enable MagicDNS. This lets you reach devices by name, for example builder.tailnet-name.ts.net, instead of by 100.x IPs. Keep “Override local DNS” enabled on clients so name resolution just works across platforms.

Step 4: Enable Tailscale SSH (Passwordless, Keyless)

In Settings → Tailscale SSH, enable it for your tailnet. On servers, run:
sudo tailscale up --ssh
You can now SSH between devices using identity-based auth, e.g.:
ssh ubuntu@server-name
Access is controlled by the tailnet policy (ACLs) rather than managing per-host SSH keys.

Step 5: Use an Exit Node (Full-Tunnel Internet)

An exit node routes all internet traffic from a device through a trusted peer (great for coffee shops and travel). On the machine that will be the exit node, run:
sudo tailscale up --advertise-exit-node
In the Admin Console, approve the exit node. On the client, open the Tailscale app and choose “Use exit node” → select your device. Optionally enable “Allow LAN access” to still reach your local network while tunneling the internet.

Step 6: Publish Your LAN with a Subnet Router

A subnet router lets remote devices reach a private LAN (e.g., 192.168.1.0/24) through Tailscale. On a Linux host connected to that LAN, enable IP forwarding:
sudo sysctl -w net.ipv4.ip_forward=1
sudo sysctl -w net.ipv6.conf.all.forwarding=1
Persist these settings in /etc/sysctl.d/99-tailscale.conf. Then advertise routes:
sudo tailscale up --advertise-routes=192.168.1.0/24
In the Admin Console → Machines, approve the advertised routes. Clients can now access printers, NAS devices, and servers on that LAN using IP or hostnames (with your DNS). If needed, add --snat=false to preserve client IPs for upstream firewall logs.

Step 7: Lock It Down with ACLs and Tags

Open the Admin Console → Access Controls and edit the policy. Use groups and tags to define who can reach what. Example: allow helpdesk to RDP to Windows servers, and engineers to SSH into Linux hosts. A minimal snippet could look like:
{ "groups": { "group:helpdesk": ["[email protected]"] }, "tagOwners": { "tag:server": ["group:helpdesk", "group:eng"] }, "acls": [ { "action": "accept", "src": ["group:helpdesk"], "dst": ["tag:server:3389"] }, { "action": "accept", "src": ["group:eng"], "dst": ["tag:server:22"] } ] }
Apply tags on devices by running:
sudo tailscale up --advertise-tags=tag:server
Only tagged and authorized devices will accept those connections.

Step 8: Headless and Auto-Join with Auth Keys

For servers and containers, create a reusable or short-lived auth key in the Admin Console → Keys. On the device, run:
sudo tailscale up --authkey=tskey-abcdef --hostname=ci-runner-01 --advertise-tags=tag:server
Use ephemeral keys for throwaway CI agents, and rotate long-lived keys on a schedule. You can also inject TS_AUTHKEY as an environment variable in Docker or systemd units.

Step 9: Troubleshooting Essentials

If a device looks offline, first check the local service:
sudo systemctl status tailscaled (Linux). Then test reachability:
tailscale status
tailscale ping device-name
tailscale netcheck
Ensure outbound UDP 41641 is open; Tailscale falls back to relays (DERP) if direct NAT traversal fails. On Linux firewalls, allow UDP/41641 and established/related traffic. If routes are not working, confirm “Accept routes” is enabled and IP forwarding is on. For deep diagnostics, run tailscale bugreport and review logs in the Admin Console.

Security Best Practices

Require SSO and MFA for all users. Enable device approval and machine key expiry. Use groups and tags to enforce least privilege in ACLs. Restrict exit node usage to trusted admins. Regularly prune unused devices, rotate auth keys, and audit connections in the logs. Avoid exposing services publicly; instead, use MagicDNS, Tailscale SSH, or consider Tailscale Funnel selectively with HTTPS for public endpoints.

What You Can Do Next

With your mesh VPN live, map drives to a NAS over the tailnet, RDP into Windows servers from anywhere, tunnel VS Code SSH to a remote lab, or back up endpoints securely to a central repository. Tailscale scales from a weekend project to a production-ready fabric without the usual VPN pain. Most changes are policy-driven, so you can iterate quickly and keep operations clean.

Popular Posts

Install Ollama and Open WebUI on Ubuntu 24.04 with NVIDIA GPU Acceleration (Step-by-Step)

Install Ollama + Open WebUI on Ubuntu 24.04 with NVIDIA GPU Acceleration (Step-by-Step)

Install a Local AI Chatbot on Ubuntu 24.04 with Ollama and Open WebUI (Step-by-Step)

Trending Now

Recovering from Btrfs Boot Failures Using GUI Tools on Fedora

By the end of this guide the reader will be able to identify a Btrfs‑based Fedora installation, boot from a live USB, list and restore snapshots using the graphical utilities btrfs‑assistant and snapper, and verify that the system returns to a functional state without resorting to the command line. Understanding the Btrfs Layout Used by Fedora Fedora Workstation and Fedora KDE install the root filesystem as a single Btrfs partition that contains two default sub‑volumes. One sub‑volume holds the traditional “/” hierarchy, while the second is dedicated to /var/lib/machines . The latter exists to keep container images out of snapshot operations; it remains empty on systems that do not run virtual machines. Because Btrfs stores data in sub‑volumes rather than separate partitions, a snapshot captures the state of an entire sub‑volume at a point in time. The installer (Anaconda) automatically registers these sub‑volumes with the snapper service. Snapper maintains a series of read‑only ...