Setting Up WireGuard VPN on Your Own Server

WireGuard VPN tunnel connecting a laptop to a server with encrypted data flow

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


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 point PEERDNS at 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_ADMIN and SYS_MODULE capabilities 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.