How to Set Up a WireGuard VPN on Ubuntu Server (Reach Your Home Network from Anywhere)

The safe way to use your server from outside the house isn't to open SSH, Samba and everything else to the internet. It's a VPN: one port open, and once connected your phone or laptop behaves as if it were on your home Wi-Fi. WireGuard is the modern choice — fast, simple, built into the Linux kernel — and wg-easy wraps it in a web page so adding a device is a QR code.
Quick reference
| Task | What |
|---|---|
| Install | Docker Compose file for wg-easy |
| Open the port | sudo ufw allow 51820/udp + forward UDP 51820 on the router |
| Admin page | http://server-ip:51821 (local network only) |
| Add a device | New client → scan the QR code in the WireGuard app |
| Dynamic home IP | a free dynamic DNS name (DuckDNS, No-IP) |
Before you start
- Docker installed (tutorial 24).
- Your public IP (curl ifconfig.me) or, better, a dynamic DNS name that follows it when it changes. DuckDNS is free and takes five minutes.
- Access to your router to forward a port.
1. The compose file
mkdir -p ~/docker/wg-easy && cd ~/docker/wg-easy
nano compose.yaml
services:
wg-easy:
image: ghcr.io/wg-easy/wg-easy:15
container_name: wg-easy
environment:
- WG_HOST=myhome.duckdns.org
- WG_DEFAULT_DNS=192.168.1.1
- WG_ALLOWED_IPS=192.168.1.0/24, 10.8.0.0/24
volumes:
- ./data:/etc/wireguard
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
restart: unless-stopped
- WG_HOST — your dynamic DNS name or public IP. Clients connect to this.
- WG_DEFAULT_DNS — your router (or Pi-hole, tutorial 34) so local names resolve over the VPN.
- WG_ALLOWED_IPS — only home-network traffic goes through the tunnel (split tunnel). Set it to 0.0.0.0/0 to send everything through home, which is handy on dodgy café Wi-Fi.
docker compose up -d
2. Open the door
sudo ufw allow 51820/udp
On the router, forward UDP port 51820 to the server's IP. (UDP, not TCP — most routers have a dropdown.)
3. Add your phone
The admin page listens on localhost only. Reach it through an SSH tunnel from your laptop:
ssh -L 51821:127.0.0.1:51821 john@192.168.1.50
Then open http://localhost:51821. On first visit it asks you to create an admin login. Click New client, name it "Phone", and a QR code appears. Install the WireGuard app on the phone, tap + → Create from QR code, scan, and toggle it on.
Turn Wi-Fi off on the phone and open http://192.168.1.50 — if your server answers over mobile data, the VPN works.
4. Add a laptop
New client again, download the .conf file, and import it into the WireGuard app for Windows or Mac.
Keeping the server reachable
If your ISP changes your public IP, the DuckDNS client updates the name automatically — run its tiny updater from cron or as another container. Without dynamic DNS, you'd have to edit every client whenever the IP changed.
Troubleshooting
Phone shows "connected" but nothing loads. Handshake isn't completing — the port forward is wrong or blocked. Check sudo ufw status, the router rule (UDP!), and that WG_HOST really resolves to your current public IP.
Works on mobile data but not from another Wi-Fi. That network blocks UDP 51820. Nothing you can do server-side; some networks block all VPNs.
ISP uses CGNAT (your public IP starts with 100.x) so port forwarding does nothing. Use Tailscale instead — same idea, no port forwarding needed, free for personal use.
Can reach the server but not other devices at home. WG_ALLOWED_IPS must include your LAN range, and the sysctl ip_forward line must be present.
Lost the admin password. Delete ./data/wg-easy config via docker compose down, remove the admin entry from the data folder, and up -d again.
Wrapping up
One compose file, one UDP port forwarded, a QR code per device. Everything else on the server stays locked to your home network, and you still reach all of it from a beach. If your ISP won't let you forward ports, Tailscale gets you the same result in ten minutes.
