๐Ÿ”’ Your Own VPN with WireGuard

Encrypt untrusted Wi-Fi, reach your home lab, get a stable exit IP โ€” on a server you control

โฌคโฌค Intermediate ~1 hour โ‚ฌ0โ€“5/month
โš  Prerequisites: This assumes you're comfortable with SSH, basic Linux commands, and a firewall (ufw) โ€” covered in the self-host a website tutorial. Do that one first if any of this is new.
๐Ÿ“‹ Table of Contents
  1. Why run your own VPN?
  2. What you need
  3. Step 1 โ€” Install WireGuard on the server
  4. Step 2 โ€” Generate keys and write the server config
  5. Step 3 โ€” NAT, IP forwarding and the firewall
  6. Step 4 โ€” Start WireGuard
  7. Step 5 โ€” Add a client (laptop or phone)
  8. Step 6 โ€” Verify it's actually working
  9. Step 7 โ€” Hardening and maintenance
  10. Troubleshooting
?
Why run your own VPN?

Let's be honest about what this does and doesn't do, because commercial VPN marketing has muddied the waters badly.

It does not make you anonymous. Your traffic still exits from a single VPS, and that VPS is tied to your account, your payment method, and whatever ID your provider required. Anyone who can legally compel your VPS provider can connect that IP back to you. Running your own VPN swaps "trust a VPN company's no-logs claim" for "trust your VPS provider's jurisdiction" โ€” it does not remove the need for trust, it just relocates it to someone you chose.

What it is genuinely good for:

  • Untrusted Wi-Fi โ€” coffee shops, hotels, airports, conference networks. Everything you send is encrypted to your own server instead of being readable by whoever runs that access point.
  • Reaching your home lab โ€” once connected, your phone or laptop can talk to your home server, NAS, or Raspberry Pi as if you were sitting on the same LAN, from anywhere in the world.
  • A stable exit IP โ€” useful when a service treats your dynamic residential IP with suspicion, or when you just want one consistent address for your traffic.

What it is not: a way to hide from your VPS provider, a way to reliably unblock geo-restricted streaming (commercial VPNs fight an arms race against that detection that a single personal IP will lose fast), or a substitute for HTTPS and good password hygiene.

๐Ÿ’ก vs. a commercial VPN: A commercial provider gives you a shared exit IP among thousands of other users (better for blending in) and apps with a marketing team behind them. Your own WireGuard VPN gives you one IP that is provably yours, a config you fully understand, and zero recurring fee beyond a VPS you probably already have reasons to run.
โ˜…
What You Need
WhatWhyCost
VPS with a public IPv4Runs the WireGuard server; needs a real internet-facing addressโ‚ฌ0 (reuse one) โ€“ โ‚ฌ5/month
Domain name (optional)A memorable hostname for the Endpoint if your IP ever changesโ‚ฌ10โ€“15/year
WireGuard + qrencodeThe VPN software, plus QR codes for phone setupFREE, open source
WireGuard app on your devicesOfficial client for laptop/phoneFREE

This barely needs any horsepower โ€” WireGuard is famously lightweight. A 1 vCPU / 1 GB instance at Hetzner, Vultr or Infomaniak is plenty, and if you already have a VPS from the self-hosting tutorial, you can run WireGuard on it directly instead of renting a second box.

For provider options and pricing, see the Trusted VPS Providers section on the homepage. A few concrete links if you need one:

  • Hetzner Cloud โ€” cheapest way to get a solid European VPS
  • Vultr โ€” simple hourly billing, many locations worldwide (affiliate link โ€” costs you nothing)
  • Infomaniak โ€” Swiss, renewable-powered, the host this site runs on (affiliate link โ€” costs you nothing)
๐Ÿ’ก Domain not required: Unlike the website or email tutorials, a domain is genuinely optional here. You can put your VPS's raw IP address straight into the client config as the Endpoint. A domain just saves you from having to update every client config if that IP ever changes.
1
Install WireGuard on the Server

WireGuard has been part of the mainline Linux kernel since 5.6, and both Debian 12 and Ubuntu 24.04 ship kernels well past that, so there's no external repository or DKMS module to fight with โ€” just install the userspace tools.

alice@your-server:~$
# Update first, then install WireGuard tools + qrencode (for phone setup later) sudo apt update && sudo apt upgrade -y sudo apt install wireguard qrencode -y # Confirm the tools are present wg --version wireguard-tools v1.0.20210914
๐Ÿ’ก "wireguard" vs "wireguard-tools": The wireguard package is a small metapackage. On modern kernels it mostly just pulls in wireguard-tools (the wg and wg-quick commands) since the kernel module itself is already built in. Either package name works; installing wireguard is the safe default.
2
Generate Keys and Write the Server Config

WireGuard uses public-key cryptography instead of usernames and passwords โ€” every peer (server and each client) gets a keypair, and the config just lists which public keys are allowed to talk to whom.

alice@your-server:~$
# /etc/wireguard is readable by root only, so open a root shell for the key work sudo -i # Work inside /etc/wireguard, and make sure new files aren't world-readable cd /etc/wireguard umask 077 # Generate the server keypair wg genkey | tee server_private.key | wg pubkey > server_public.key # You'll need both values in a moment cat server_private.key wJ3f9k...(your server's private key)...= cat server_public.key Hs8qP2...(your server's public key)...=

Now create the interface config. WireGuard config files are plain text โ€” no daemons to babysit, no database.

alice@your-server:~$ sudo nano /etc/wireguard/wg0.conf
[Interface] Address = 10.8.0.1/24 ListenPort = 51820 PrivateKey = <paste the contents of server_private.key here> # Peers ([Peer] blocks for each client) get added in Step 5
๐Ÿ’ก What is 10.8.0.1/24? This is the VPN's own private network โ€” a virtual subnet that only exists between the server and its connected clients. 10.8.0.1 is the server's address inside the tunnel; clients will get 10.8.0.2, 10.8.0.3, and so on. It has nothing to do with your VPS's real public IP.
3
NAT, IP Forwarding and the Firewall

By default, a client on the VPN subnet (10.8.0.x) can reach the server itself, but not the wider internet through it. To turn your VPS into an actual gateway, you need three things: forwarding enabled in the kernel, a NAT rule so outbound packets look like they came from the server, and a firewall rule opening the VPN's UDP port.

First, find your server's real internet-facing interface โ€” you'll need its name for the NAT rule:

alice@your-server:~$
ip route show default default via 65.21.100.1 dev eth0 proto dhcp src 65.21.100.42 metric 100 # The "dev" field is what you need โ€” here it's eth0. Yours might be ens3, enp1s0, etc.

Add PostUp/PostDown lines to the [Interface] block in /etc/wireguard/wg0.conf, replacing eth0 with the interface you just found:

/etc/wireguard/wg0.conf (add inside [Interface])
PostUp = iptables -A FORWARD -i wg0 -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE PostDown = iptables -D FORWARD -i wg0 -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE

nftables equivalent, if your system uses a pure nftables ruleset instead of iptables:

/etc/wireguard/wg0.conf (nftables version)
PostUp = nft add table inet wg0nat; nft add chain inet wg0nat post { type nat hook postrouting priority 100 \; }; nft add rule inet wg0nat post oifname "eth0" masquerade; nft add table inet wg0filter; nft add chain inet wg0filter fwd { type filter hook forward priority 0 \; }; nft add rule inet wg0filter fwd iifname "wg0" accept PostDown = nft delete table inet wg0nat; nft delete table inet wg0filter

Next, enable IP forwarding permanently in the kernel:

alice@your-server:~$
echo 'net.ipv4.ip_forward = 1' | sudo tee /etc/sysctl.d/99-wireguard.conf sudo sysctl --system * Applying /etc/sysctl.d/99-wireguard.conf ... net.ipv4.ip_forward = 1

Finally, open the WireGuard port in your firewall:

alice@your-server:~$
# If ufw isn't enabled yet, allow SSH FIRST โ€” do this before "ufw enable" or you'll lock yourself out sudo ufw allow OpenSSH # Open WireGuard's UDP port sudo ufw allow 51820/udp # Only run this if ufw wasn't already active from a previous tutorial sudo ufw enable sudo ufw status 22/tcp (OpenSSH) ALLOW Anywhere 51820/udp ALLOW Anywhere
โš  Don't lock yourself out: If ufw is not already active, always confirm SSH is allowed before running ufw enable. Keep your current SSH session open and test a fresh connection in a second terminal before closing it.
โš  Cloud firewalls are separate: Many providers (Hetzner Cloud Firewall, Vultr's firewall groups, and similar features elsewhere) filter traffic before it even reaches your OS. If you've enabled one of those, you also need to allow inbound UDP 51820 there โ€” ufw alone won't be enough.
4
Start WireGuard

WireGuard ships with a systemd unit template that reads /etc/wireguard/<name>.conf โ€” since our file is wg0.conf, the service is wg-quick@wg0.

alice@your-server:~$
sudo systemctl enable --now wg-quick@wg0 sudo systemctl status wg-quick@wg0 โ— wg-quick@wg0.service - WireGuard via wg-quick(8) for wg0 Active: active (exited) # Confirm the interface is up sudo wg show wg0 interface: wg0 public key: Hs8qP2...(server public key)...= private key: (hidden) listening port: 51820

No peers listed yet โ€” that's expected, you haven't added a client. That's next.

5
Add a Client (Laptop or Phone)

Each device gets its own keypair and its own small slice of the VPN subnet. You can generate the client keypair on the server for convenience (shown below), or directly on the device using the official app โ€” either way, the private key should never be needed anywhere else once it exists.

alice@your-server:~$
# Root shell again (type "exit" when you're done with the key files) sudo -i cd /etc/wireguard umask 077 wg genkey | tee client1_private.key | wg pubkey > client1_public.key cat client1_private.key mK7x...(client's private key)...= cat client1_public.key Qz4t...(client's public key)...=

Add a [Peer] block to the server's /etc/wireguard/wg0.conf, using the client's public key and a unique VPN IP:

/etc/wireguard/wg0.conf (append)
[Peer] PublicKey = <client1_public.key contents> AllowedIPs = 10.8.0.2/32

Apply the change without a full restart, or just restart the service:

alice@your-server:~$
# Option A โ€” reload the running config from the file you just edited # (run as root: the <( ) part must be able to read wg0.conf too) sudo bash -c 'wg syncconf wg0 <(wg-quick strip wg0)' # Option B โ€” simpler, causes a brief reconnect sudo systemctl restart wg-quick@wg0

Now write the client's config โ€” this file goes on the laptop or phone, never on the server:

client1.conf
[Interface] PrivateKey = <client1_private.key contents> Address = 10.8.0.2/24 DNS = 9.9.9.9 [Peer] PublicKey = <server_public.key contents> Endpoint = your.vps.ip.address:51820 AllowedIPs = 0.0.0.0/0 PersistentKeepalive = 25

DNS: 9.9.9.9 is Quad9's public resolver โ€” no logging, malware-domain filtering, a reasonable default. Point it at your own resolver instead if you run one (e.g. a Pi-hole reachable over the tunnel).

AllowedIPs โ€” split tunnel vs. full tunnel:

  • AllowedIPs = 0.0.0.0/0 (shown above) is a full tunnel โ€” every packet from the device routes through the VPS. Best for untrusted Wi-Fi, since it protects all your traffic, not just some of it.
  • AllowedIPs = 10.8.0.0/24 is a split tunnel โ€” only traffic destined for the VPN subnet (i.e. your home lab) goes through the tunnel; everything else uses your normal internet connection directly. Faster, but doesn't protect general browsing.

PersistentKeepalive = 25: Almost every client sits behind some form of NAT (a home router, a mobile carrier's network). Without a keepalive, that NAT's mapping for the tunnel expires after a period of silence, and the server loses the ability to reach the client first. Sending a tiny packet every 25 seconds keeps that mapping alive. Leave this out only if the client itself has a public IP with no NAT in front of it.

๐Ÿ’ก QR code for phones: Instead of transferring the file, generate a QR code and scan it directly with the WireGuard app's "Create from QR code" option:
alice@your-server:~$
qrencode -t ansiutf8 < client1.conf (a QR code renders directly in your terminal โ€” scan it, then delete client1.conf from the server)

Install the official WireGuard app for your platform from wireguard.com/install โ€” Windows, macOS, Linux, Android and iOS are all covered, all open source, all from the WireGuard project itself.

6
Verify It's Actually Working

Connect from your client, then check from both ends. A tunnel that "looks connected" but isn't actually carrying traffic is a classic false sense of security.

alice@your-server:~$
sudo wg show wg0 peer: Qz4t...(client public key)...= endpoint: 203.0.113.55:41234 allowed ips: 10.8.0.2/32 latest handshake: 12 seconds ago transfer: 4.31 KiB received, 18.02 KiB sent

A recent "latest handshake" and non-zero transfer means the tunnel is genuinely up. latest handshake: (none) means it never negotiated at all โ€” see Troubleshooting below.

On the client, once connected (with a full tunnel), confirm your traffic is really exiting through the VPS:

your-laptop:~$
curl ifconfig.me 65.21.100.42 # Should match your VPS's public IP, not your home/cafรฉ connection

DNS leak check: Visit dnsleaktest.com and run the extended test. You should see only the resolver you configured (Quad9 or your own), never your ISP's or the local Wi-Fi network's DNS servers.

7
Hardening and Maintenance
  • Automatic security patches: sudo apt install unattended-upgrades -y keeps the OS patched without you having to remember.
  • SSH keys only: Disable password authentication on the VPS (PasswordAuthentication no in /etc/ssh/sshd_config) โ€” covered in the self-host tutorial's hardening step. A VPN is worthless if the box hosting it is trivially brute-forced.
  • Key rotation: There's no built-in expiry, so rotate periodically yourself โ€” generate a fresh keypair for a device, swap the PublicKey in its server [Peer] block and its own [Interface] section, then reload with wg syncconf.
  • Removing a lost or stolen device: Delete that device's [Peer] block from /etc/wireguard/wg0.conf, then apply it immediately:
alice@your-server:~$
# Immediate effect (until next full sync/reboot rewrites it from the file anyway) sudo wg set wg0 peer <lost-device-public-key> remove # Then also remove the matching [Peer] block from wg0.conf so it stays gone sudo nano /etc/wireguard/wg0.conf

Once removed, that device's key is no longer accepted by the server โ€” it can never reconnect, no matter how it tries.

๐Ÿ’ก IPv6: WireGuard supports IPv6 fine (an AllowedIPs entry like fd00::/64 plus matching ip6tables/nft rules), but most personal setups don't need it. Treat it as optional and out of scope here โ€” IPv4-only is a perfectly complete VPN.
!
Troubleshooting

Handshake never happens (wg show stays at latest handshake: (none)):

  • UDP 51820 is blocked somewhere โ€” check sudo ufw status on the server, and separately check any cloud-level firewall your provider offers (these sit in front of the OS and are easy to forget).
  • Wrong Endpoint in the client config โ€” a typo'd IP, the wrong port, or (if the client is on the same LAN during testing) a private IP instead of the server's real public one.
  • Mismatched keys โ€” the single most common mistake. The client's [Peer] PublicKey must be the server's public key, and the server's [Peer] PublicKey must be that client's public key. It's easy to paste the wrong one in the wrong place.

Handshake succeeds but no internet access:

  • Confirm forwarding is actually on: sysctl net.ipv4.ip_forward should print 1.
  • Re-check the egress interface name with ip route show default and make sure it matches exactly what's in your PostUp/PostDown MASQUERADE rule โ€” a provider migration or a renamed NIC is a common silent breakage.
  • Test incrementally: from the client, ping 10.8.0.1 (the server, inside the tunnel) should always work once the handshake succeeds. If ping 8.8.8.8 then fails, the tunnel itself is fine and the problem is specifically forwarding/NAT on the server, not WireGuard.

DNS isn't resolving (IP addresses work, domain names don't):

  • Double-check the DNS = line in the client config points at a resolver that's actually reachable (e.g. 9.9.9.9).
  • On Linux clients, wg-quick needs resolvconf or openresolv installed to actually apply that DNS setting system-wide (sudo apt install openresolv). The official mobile and desktop apps handle this automatically โ€” this mostly bites manual wg-quick setups.
  • Confirm it's really DNS and not general connectivity: if curl https://9.9.9.9 works but curl https://example.com doesn't, it's DNS.

๐Ÿ”’ Your Traffic Is Yours โ€” What's Next?

โ†’ Need a VPS first? See Trusted VPS Providers โ† Back: Self-host your own website โ†’ Next: Run your own email server with Mailcow โ†’ WireGuard official documentation โ†’ r/selfhosted โ€” community support