A couple of years ago I ditched commercial VPNs for a setup I control end‑to‑end. It’s faster, cheaper long‑term, and it does exactly what I need: secure my traffic on hostile networks, give me a stable exit IP I own, and let me reach my homelab from anywhere.
This post is a practical guide to building the same thing with WireGuard, plus how to make it fast and private without sending data to a third‑party. We’ll cover:
- What you actually get from a self‑hosted VPN (and what you don’t)
- A production‑ready WireGuard server on Debian 12 (VPS or at home)
- Client setups for iOS, Android, macOS, Windows, Linux
- Optional: run your own recursive DNS (Unbound) so your queries stay yours
- Performance tuning that actually moves the needle
- Common errors I’ve hit and how to fix them fast
If you’re comfortable with a terminal and basic networking, you’re good. Grab a coffee.
Why self‑host at all?
Control and privacy. With a self‑hosted VPN you decide what’s logged (ideally nothing), where your exit IP lives, and how DNS is handled. For most day‑to‑day privacy use cases, that’s huge. I also use mine to geofence admin panels, enforce MFA prompts from a fixed egress IP, and stream my LAN media when travelling.
Performance. WireGuard is notably lean. On decent hardware you’ll saturate 1 Gbps easily and go higher on modern CPUs. The protocol uses modern crypto (Curve25519 + ChaCha20‑Poly1305) and lives in the kernel on Linux.
Cost. A small EU VPS with 1 vCPU/1–2 GB RAM is plenty and typically costs less than a month of a commercial VPN. Or host at home if you have a public IPv4 and/or IPv6.
Reality check: a self‑hosted VPN does not make you anonymous . Your VPS provider can tie activity to your account, and sites still know who you are when you log in. If you need anonymity, use Tor. A self‑hosted VPN is for privacy, integrity on untrusted networks, and control.
Architecture at a glance
- Server: Debian 12 on a VPS (or a home box) with UDP/51820 open.
- Tunnel: WireGuard
wg0with an RFC1918 IPv4 subnet and a ULA IPv6 prefix. - NAT & firewall:
nftableswith default‑deny, NAT44/66 enabled. - DNS (optional but recommended): Unbound recursive resolver bound to the WG subnet. Clients use it over the tunnel.
[Phone/Laptop] ⇄ Internet ⇄ [VPS: wg0 + nftables + Unbound] ⇄ Internet
⤷ optional route to homelab over wg peer(s)
Prerequisites
- Debian 12 server with a public IPv4. IPv6 strongly recommended.
- Port 51820/UDP reachable.
- Root or sudo.
Hostnames are optional. I usually point a DNS A/AAAA at the VPS for convenience.
Step 1: Install WireGuard
sudo apt update
sudo apt install -y wireguard wireguard-tools qrencode nftablesEnable nftables at boot:
sudo systemctl enable --now nftablesStep 2: Create keys and base config
We’ll use 10.66.66.0/24 for IPv4 and a ULA for IPv6. Change to taste.
sudo umask 077
cd /etc/wireguard
wg genkey | tee server.key | wg pubkey > server.pubCreate /etc/wireguard/wg0.conf:
[Interface]
Address = 10.66.66.1/24, fd86:ea04:1115::1/64
ListenPort = 51820
PrivateKey = <paste contents of /etc/wireguard/server.key>
# Good defaults
SaveConfig = false
MTU = 1420
# We’ll let nftables handle firewalling/NAT; see next step.Tip: If you’re on PPPoE or see weird “some sites don’t load” symptoms later, drop
MTUto 1280 and retest. We’ll cover MTU testing below.
Step 3: Firewall and NAT with nftables
Edit /etc/nftables.conf to something like this (replace eth0 with your public NIC name):
flush ruleset
# Replace with your real interfaces
define pub_if = "eth0"
define wg_if = "wg0"
table inet filter {
chain input {
type filter hook input priority 0; policy drop;
iifname lo accept
ct state established,related accept
meta l4proto { icmp, ipv6-icmp } accept
udp dport 51820 iifname $pub_if accept # WireGuard
# Optional: allow SSH from everywhere or your management IPs only
tcp dport 22 iifname $pub_if accept
}
chain forward {
type filter hook forward priority 0; policy drop;
ct state established,related accept
iifname $wg_if oifname $pub_if accept # WG → Internet
iifname $pub_if oifname $wg_if accept # Internet → WG (return traffic)
}
chain output {
type filter hook output priority 0; policy accept;
}
}
table ip nat {
chain postrouting {
type nat hook postrouting priority 100; policy accept;
oifname $pub_if masquerade
}
}
table ip6 nat {
chain postrouting {
type nat hook postrouting priority 100; policy accept;
oifname $pub_if masquerade
}
}Apply it:
sudo nft -f /etc/nftables.conf
sudo nft list ruleset | lessEnable IP forwarding:
printf 'net.ipv4.ip_forward=1\nnet.ipv6.conf.all.forwarding=1\n' | sudo tee /etc/sysctl.d/99-wg-forward.conf
sudo sysctl --systemStep 4: Bring up WireGuard
sudo systemctl enable --now wg-quick@wg0
wg showYou should see your interface listening on 51820.
Step 5: Add your first client (phone/laptop)
Generate client keys on the server or your laptop:
wg genkey | tee alice.key | wg pubkey > alice.pubAppend this peer to /etc/wireguard/wg0.conf on the server:
[Peer]
# Alice’s phone
PublicKey = <contents of alice.pub>
AllowedIPs = 10.66.66.2/32, fd86:ea04:1115::2/128Reload without dropping the interface:
sudo wg setconf wg0 <(wg-quick strip /etc/wireguard/wg0.conf)Create alice.conf for the client:
[Interface]
Address = 10.66.66.2/32, fd86:ea04:1115::2/128
PrivateKey = <contents of alice.key>
DNS = 10.66.66.1
[Peer]
PublicKey = <server public key from /etc/wireguard/server.pub>
Endpoint = your.server.name.or.ip:51820
AllowedIPs = 0.0.0.0/0, ::/0
PersistentKeepalive = 25Split‑tunnel? Instead of
0.0.0.0/0, ::/0, setAllowedIPsto the subnets you want routed via the VPN (for example10.66.66.0/24or your homelab ranges).
Import on mobile via QR
On the server:
qrencode -t ansiutf8 < alice.confOpen the WireGuard mobile app, add a tunnel, and scan the QR in your terminal.
Optional: your own private DNS with Unbound
Why bother? Because it keeps your DNS queries inside your tunnel instead of sending them to a third‑party. We’ll run Unbound locally and point WireGuard clients at it.
Install:
sudo apt install -y unboundCreate /etc/unbound/unbound.conf.d/wg.conf:
server:
interface: 10.66.66.1
access-control: 10.66.66.0/24 allow
do-ip4: yes
do-ip6: yes
do-udp: yes
do-tcp: yes
qname-minimisation: yes
hide-identity: yes
hide-version: yes
harden-glue: yes
harden-dnssec-stripped: yes
edns-buffer-size: 1232
prefetch: yes
# Debian ships root hints via dns-root-data; explicit root-hints is optional
# root-hints: "/usr/share/dns/root.hints"
forward-zone:
name: "."
forward-first: no
# If you prefer full recursion, remove this block entirely.Start it:
sudo systemctl enable --now unboundConfirm DNS from a client after connecting WG:
# from your laptop/phone shell
nslookup example.com 10.66.66.1If you want ad/tracker blocking for your devices, drop Pi‑hole in front and set its upstream to Unbound. I run Unbound standalone to keep the stack simple.
Performance tuning that actually helps
WireGuard is already quick. Tune only if you hit limits.
1) MTU sanity
Start with MTU = 1420. If some sites hang or you see stalls on mobile data, reduce to 1280 . Quick path MTU test from a client:
# IPv4: find largest size that does not fragment
ping -M do -s 1400 your.server.ip
# If it fails, reduce -s in steps of 10 until it succeeds, then add 28 bytes for ICMP/IPv4 overhead2) Buffers for high‑throughput links
For fast WANs with high RTT, larger UDP buffers can help:
cat <<'EOF' | sudo tee /etc/sysctl.d/99-wg-buffers.conf
net.core.rmem_max = 8388608
net.core.wmem_max = 8388608
EOF
sudo sysctl --system3) Measure properly
Use iperf3 through the tunnel to isolate issues:
# On a server reachable over WG
iperf3 -s
# On client
iperf3 -c 10.66.66.1 -P 4Expect high hundreds of Mbps to multi‑Gbps on modern CPUs. If speeds are poor, check CPU single‑core utilisation, NIC offloads, and MTU first.
Don’t over‑tune. Changing
txqueuelen, random kernel knobs, or exotic qdiscs tends to create bufferbloat or placebo gains. Keep it boring.
Security and privacy notes
- Treat
*.keylike SSH private keys.chmod 600and never commit them anywhere. - Use a random listen port if you want to be slightly less obvious than 51820.
- If you care about “no logs,” don’t enable verbose logging on the server. I keep
journalddefault and avoid WG debug logs in production. - Self‑hosting protects you from ISP snooping on hostile networks and from commercial VPN logging, but your VPS account is still tied to your identity. This is privacy, not anonymity.
Troubleshooting: common gotchas
- Handshake never completes: a firewall is blocking UDP/51820 or your
Endpointis wrong. Verify port is open from outside and the server sees packets (sudo tcpdump -ni eth0 udp port 51820). - Connected but no Internet: NAT or forwarding is missing. Recheck
nftablesNAT postrouting andnet.ipv4.ip_forward=1. - Some sites hang/time out: almost always MTU/fragmentation. Drop
MTUto 1280 on client and server and retry. - UFW users: remember UFW needs route rules for forwarded traffic. If you stick with UFW instead of raw
nftables, addPostUp = ufw route allow in on wg0 out on eth0and the matchingPreDowndelete. SaveConfig=trueate my comments:wg-quick downwrites the live config back and can strip comments. I keepSaveConfig=falseand edit files explicitly.- Revoke a device:
wg set wg0 peer <client-public-key> removethen delete its stanza fromwg0.confand reload.
Optional extras I like
- Site‑to‑site: add another
[Peer]with its own subnet inAllowedIPsand let the server route between them. For routed IPv6, ideally get a routed /64 or /56 from your provider and assign real IPv6 to peers. - Kill‑switch on clients: use your OS firewall to drop all traffic except via
wg0if you need to prevent leaks when the tunnel drops. - Backups: I keep
/etc/wireguard/in a private repo with age/sops. Keys are rotated yearly.
Who shouldn’t self‑host
- If you need many rotating exit IPs across countries for streaming arbitrage, use a provider. Self‑hosting is about control, not cat‑and‑mouse with content farms.
- If you need anonymity against strong adversaries, use Tor. A VPN, self‑hosted or not, won’t cut it.
Wrap‑up
Self‑hosting a VPN is one of those projects that pays back every day. WireGuard keeps the mental overhead low, and running your own DNS closes a huge privacy gap. Build it once, keep it patched, and enjoy not sending your traffic to yet another third‑party.