Setting Up WireGuard VPN on Your Own Server
Most commercial VPN providers promise "no logs" and "military-grade encryption" while routing your traffic through servers you don't control. You're trusting a marketing claim with your entire internet activity. There's a better option: run your own VPN.
A WireGuard VPS setup gives you full control over your traffic, your logs, and your encryption keys. WireGuard is faster than OpenVPN, easier to configure than IPSec, and the entire codebase is around 4,000 lines — small enough to audit. I run WireGuard on the same Hetzner VPS that hosts this blog, and the setup took me about 15 minutes.
By the end of this post, you'll have a working WireGuard VPN server with client configs for your phone and laptop.
Prerequisites
- A Linux VPS with a public IP (I use Hetzner — here's how I set mine up)
- SSH access to your server (hardened, ideally)
- Docker and Docker Compose installed
- A device to connect from (Linux, macOS, Windows, iOS, or Android)
Why WireGuard Over OpenVPN?
I ran OpenVPN for years before switching. Here's why WireGuard wins for self-hosters:
| Feature | WireGuard | OpenVPN |
|---|---|---|
| Codebase | ~4,000 lines | ~100,000 lines |
| Protocol | UDP only | TCP or UDP |
| Speed | Near wire speed | 20-30% overhead |
| Connection time | ~100ms | 5-10 seconds |
| Config complexity | Simple key pairs | Certificates + PKI |
| Kernel integration | Built into Linux 5.6+ | Userspace |
WireGuard is in the Linux kernel since 5.6. It's not a third-party module — it's part of the operating system. That matters for performance and long-term support.
Install WireGuard with Docker Compose
You could install WireGuard directly on the host, but I prefer Docker for consistency with the rest of my stack. The linuxserver/wireguard image handles key generation and config templating automatically.
Create a directory for your WireGuard config:
sudo mkdir -p /opt/wireguard
cd /opt/wireguard
Create the compose file:
# docker-compose.yml
services:
wireguard:
image: lscr.io/linuxserver/wireguard:latest
container_name: wireguard
cap_add:
- NET_ADMIN
- SYS_MODULE
environment:
- PUID=1000
- PGID=1000
- TZ=Europe/Helsinki
- SERVERURL=<YOUR_SERVER_IP>
- SERVERPORT=51820
- PEERS=laptop,phone,tablet
- PEERDNS=1.1.1.1
- INTERNAL_SUBNET=10.13.13.0
- ALLOWEDIPS=0.0.0.0/0
volumes:
- ./config:/config
- /lib/modules:/lib/modules
ports:
- 51820:51820/udp
sysctls:
- net.ipv4.conf.all.src_valid_mark=1
restart: unless-stopped
Replace <YOUR_SERVER_IP> with your VPS's public IP address. The PEERS variable creates client configs automatically — one for each name you list.
Start the container:
docker compose up -d
WireGuard generates all the keys and configs on first boot. Check the logs to confirm:
docker compose logs wireguard
You should see lines like [#] ip link add wg0 type wireguard and config generation for each peer.
Configure Your Firewall for WireGuard
If you followed my VPS hardening guide, you have UFW running. You need to allow WireGuard's UDP port:
sudo ufw allow 51820/udp comment "WireGuard VPN"
You also need IP forwarding enabled. Check if it's already on:
sysctl net.ipv4.ip_forward
If the output is 0, enable it permanently:
echo "net.ipv4.ip_forward=1" | sudo tee -a /etc/sysctl.d/99-wireguard.conf
sudo sysctl -p /etc/sysctl.d/99-wireguard.conf
Note: The Docker container sets src_valid_mark via sysctls, but IP forwarding must be enabled on the host. Without it, traffic enters the VPN tunnel but never leaves.Grab Your Client Configs
The container auto-generated configs for each peer you listed. Find them in the config directory:
ls /opt/wireguard/config/peer_laptop/
ls /opt/wireguard/config/peer_phone/
Each peer folder contains: - peer_laptop.conf — the full WireGuard config file - peer_laptop.png — a QR code for mobile setup
View the laptop config:
cat /opt/wireguard/config/peer_laptop/peer_laptop.conf
Expected output:
[Interface]
Address = 10.13.13.2/32
PrivateKey = <auto-generated-private-key>
ListenPort = 51820
DNS = 1.1.1.1
[Peer]
PublicKey = <server-public-key>
PresharedKey = <auto-generated-psk>
Endpoint = <YOUR_SERVER_IP>:51820
AllowedIPs = 0.0.0.0/0
Connect Your Devices
Linux
Install the WireGuard client:
sudo apt install wireguard
Copy the config file to your client machine:
scp user@<YOUR_SERVER_IP>:/opt/wireguard/config/peer_laptop/peer_laptop.conf /etc/wireguard/wg0.conf
Start the tunnel:
sudo wg-quick up wg0
Verify you're connected:
curl ifconfig.me
This should return your VPS IP, not your home IP. To disconnect:
sudo wg-quick down wg0
To start WireGuard automatically on boot:
sudo systemctl enable wg-quick@wg0
macOS and Windows
Download the official WireGuard app from wireguard.com/install. Import the .conf file and click "Activate." That's it.
iOS and Android
Install the WireGuard app from the App Store or Play Store. Then scan the QR code directly from your terminal:
docker exec wireguard /app/show-peer phone
This displays the QR code in your terminal. Scan it with the mobile app and toggle the connection on.
Adding More Peers Later
Need to add a new device? Update the PEERS environment variable:
environment:
- PEERS=laptop,phone,tablet,work-laptop
Then restart the container:
docker compose down && docker compose up -d
WireGuard generates the new peer config without touching existing ones. Your current devices stay connected.
Split Tunnel vs Full Tunnel
The config above routes all traffic through the VPN (AllowedIPs = 0.0.0.0/0). This is a full tunnel — everything goes through your server.
For a split tunnel (only route specific traffic), edit the client config:
[Peer]
AllowedIPs = 10.13.13.0/24, 192.168.1.0/24
This only routes traffic to your VPN subnet and home network through the tunnel. Everything else uses your local internet connection. Split tunneling is better for performance when you only need access to services on your server.
Security Considerations
WireGuard is secure by design, but your setup can still have weak points:
- Key management. The private keys in
/opt/wireguard/config/are the keys to your VPN. If someone gets them, they're on your network. Protect this directory:chmod 700 /opt/wireguard/config/. - DNS leaks. The config sets DNS to
1.1.1.1(Cloudflare). If you want full privacy, run your own DNS resolver (Pi-hole or Unbound) and pointPEERDNSat your server's WireGuard IP instead. - Exposed UDP port. Port 51820 is visible to scanners. WireGuard is silent by design — it doesn't respond to unauthenticated packets — but you can change the port to something less obvious if you prefer.
- Container capabilities. The
NET_ADMINandSYS_MODULEcapabilities are required for WireGuard to create network interfaces. This is more permissive than a typical container — review the Docker security best practices to lock down the rest of your stack.
Troubleshooting
Problem: Client connects but can't reach the internet. Cause: IP forwarding is disabled on the host. Fix: Run sysctl net.ipv4.ip_forward=1 and make it permanent in /etc/sysctl.d/.
Problem: Connection times out. Cause: UDP port 51820 is blocked by your firewall or hosting provider. Fix: Check sudo ufw status and your Hetzner firewall rules in the cloud console. Both must allow 51820/udp.
Problem: QR code doesn't display in terminal. Cause: Terminal doesn't support the character encoding. Fix: Use a modern terminal emulator, or copy the .conf file manually instead.
Problem: Handshake completes but no data flows. Cause: NAT or routing issue on the server. The src_valid_mark sysctl isn't set. Fix: Verify with sysctl net.ipv4.conf.all.src_valid_mark — should return 1. The Docker compose file sets this, but check it's applied.
Problem: Existing peers break after adding new ones. Cause: You recreated the container without preserving the config volume. Fix: The configs are stored in ./config which is bind-mounted. As long as you don't delete that directory, existing peers survive restarts. If you lost them, clients need new configs.
Conclusion
You now have a self-hosted WireGuard VPN that you fully control. No subscription fees, no trust-me-bro logging policies, and connection speeds that barely dip below your raw bandwidth.
From here, consider:
- Adding Pi-hole behind WireGuard for ad-blocking on all your devices
- Using a split tunnel for accessing your self-hosted services remotely
- Setting up Fail2Ban to monitor your server logs
- Reviewing your overall VPS hardening if you haven't already
If you need a VPS to run this on, I use Hetzner for all my projects — solid performance, fair pricing, and Helsinki data center latency is great for Europe.