Why WireGuard?
Every time you connect to coffee shop Wi-Fi, your traffic flows through a network you don’t control. Your DNS queries, your unencrypted HTTP requests, even the metadata of your encrypted connections — all visible to whoever runs that router. A VPN solves this by creating an encrypted tunnel back to a network you do control: your home.
There are many VPN protocols out there — OpenVPN, IPSec, L2TP — but WireGuard is different. It’s roughly 4,000 lines of code compared to OpenVPN’s 100,000+. It’s faster, simpler, auditable, and built into the Linux kernel since 5.6. It’s the VPN protocol that finally makes self-hosting a VPN feel easy rather than heroic.
In this tutorial, we’ll set up WireGuard using Docker Compose, generate client configurations for your phone and laptop, and verify that everything works. By the end, you’ll have encrypted access to your home network from anywhere in the world.
Prerequisites
- A Linux server with Docker and Docker Compose installed (Ubuntu 22.04+ recommended)
- Root or sudo access
- A public IP address (or a VPS — this works on any cloud provider)
- Port 51820/UDP open in your firewall (and forwarded on your router if behind NAT)
- About 20 minutes
Step 1: Create the Project Directory
Let’s keep things tidy. Create a dedicated directory for your WireGuard setup:
mkdir -p ~/wireguard && cd ~/wireguard
Step 2: Create the Docker Compose File
We’ll use the popular linuxserver/wireguard image. It’s well-maintained, supports both kernel-mode (fast) and userspace (portable) WireGuard, and has excellent documentation.
Create docker-compose.yml:
cat <<'EOF' > docker-compose.yml
version: "3.8"
services:
wireguard:
image: lscr.io/linuxserver/wireguard:latest
container_name: wireguard
cap_add:
- NET_ADMIN
- SYS_MODULE
environment:
- PUID=1000
- PGID=1000
- TZ=America/New_York
- SERVERURL=your-domain-or-ip
- SERVERPORT=51820
- PEERS=phone,laptop,tablet
- PEERDNS=auto
- INTERNAL_SUBNET=10.13.13.0
- ALLOWEDIPS=0.0.0.0/0
- LOG_CONFS=true
volumes:
- ./config:/config
- /lib/modules:/lib/modules
ports:
- "51820:51820/udp"
sysctls:
- net.ipv4.ip_forward=1
- net.ipv4.conf.all.src_valid_mark=1
restart: unless-stopped
EOF
Let’s break down the important bits:
- PEERS=phone,laptop,tablet — This auto-generates a config file for each named client. Change these to whatever devices you want. You can add more later.
- SERVERURL — Set this to your server’s public IP or a domain name (like
wireguard.yourdomain.com). This gets embedded in client configs so they know where to connect. - INTERNAL_SUBNET=10.13.13.0 — The VPN’s internal IP range. 10.13.13.0/24 is the default and avoids most home network conflicts.
- ALLOWEDIPS=0.0.0.0/0 — Routes ALL client traffic through the VPN (full tunnel). If you only want to access your home network, change this to your LAN subnet, e.g.,
192.168.1.0/24. - /lib/modules — Mounts the host’s kernel modules so WireGuard can use the kernel implementation (much faster). If your kernel doesn’t have WireGuard built in, the container falls back to a userspace implementation automatically.
Step 3: Start the Container
docker compose up -d
Check that it’s running:
docker compose logs -f wireguard
You’ll see the server keys generated and QR codes printed to the log for each peer. Ctrl+C to exit the log view — the container keeps running in the background.
Step 4: Get Your Client Configurations
The container auto-generates config files for each peer you named. They live in ./config/:
ls config/
# Output might look like:
# peer_phone/ peer_laptop/ peer_tablet/ wg0.conf
Each peer directory contains a .conf file (for WireGuard apps) and a .png QR code (for mobile apps). To view the QR codes in a terminal:
# View a QR code directly in supported terminals
cat config/peer_phone/peer_phone.conf
Copy the .conf file contents to your devices. On your phone, install the WireGuard app (free on iOS and Android), tap the + button, and either scan the QR code or import the config file.
Step 5: Connect and Verify
Activate the VPN on your phone or laptop. Then verify the tunnel is working:
# On the server, check active peers:
docker exec wireguard wg show
You should see your connected peers with recent handshake timestamps and data transfer stats.
On your client, visit ip.jerithai.com or ifconfig.me — the IP shown should be your server’s public IP, not your local one. That means your traffic is routing through the VPN.
Step 6: Add More Peers Later
Need to add a friend’s device or a new laptop? It’s easy:
# Stop the container
docker compose down
# Edit docker-compose.yml and add the new peer name:
# PEERS=phone,laptop,tablet,work-laptop
# Restart
docker compose up -d
# The new config appears in config/peer_work-laptop/
Or, to add a peer without restarting, you can use wg commands directly inside the container:
docker exec -it wireguard bash
wg set wg0 peer <CLIENT_PUBLIC_KEY> allowed-ips 10.13.13.4/32
But the config-file approach is cleaner and persists across restarts.
Common Pitfalls & Troubleshooting
❌ “Handshake did not complete”
This usually means the UDP port isn’t reachable. Check:
- Your server’s firewall:
sudo ufw allow 51820/udp - Your router’s port forwarding (if behind NAT): forward UDP 51820 to your server’s LAN IP
- Some ISPs block uncommon UDP ports — try changing
SERVERPORTto 443 or 53
❌ Connected but no internet
Check IP forwarding is enabled on the host:
cat /proc/sys/net/ipv4/ip_forward
# Should return 1
If it returns 0:
echo 'net.ipv4.ip_forward=1' | sudo tee -a /etc/sysctl.conf
sudo sysctl -p
❌ DNS leaks
If PEERDNS=auto causes issues, set it explicitly to a privacy-respecting DNS:
- PEERDNS=1.1.1.1,9.9.9.9
Or point it to your own Pi-hole if you set one up (that’s a great next tutorial to read!).
❌ Container won’t start: “Operation not permitted”
Make sure the cap_add: NET_ADMIN and SYS_MODULE lines are present. On some hosts, you may also need:
sudo modprobe wireguard
Security Hardening Tips
Once it’s working, consider these improvements:
- Use a domain name instead of a hard-coded IP in
SERVERURL. If your home IP changes, update DNS and restart — client configs already point to the domain. - Set up fail2ban on the host to block brute-force attempts on exposed ports.
- Keep the image updated:
docker compose pull && docker compose up -d - Use split tunneling (change
ALLOWEDIPSto your LAN subnet only) if you only need access to home services — this saves bandwidth and reduces latency for general browsing. - Back up
./config/— it contains your private keys. Lose them, and you’ll need to reconfigure every client.
What’s Next?
With WireGuard running, you now have a secure tunnel into your home network. Here’s what pairs beautifully with it:
- Pi-hole — Route your mobile DNS through your home Pi-hole for ad-blocking everywhere
- Self-hosted services — Access your Nextcloud, Home Assistant, or media server without exposing them to the public internet
- Remote development — SSH into your home machines from anywhere
That’s it. You went from zero to a fully functional, self-hosted VPN in about 15 minutes. WireGuard’s simplicity is its superpower — no certificate authorities, no complex PKI, no 400-page configuration guides. Just keys, endpoints, and encrypted packets.
Welcome to the self-hosted VPN club. Your coffee shop Wi-Fi sessions will never be the same.
Leave a Reply