Commercial VPN apps cost $60–$120 a year, log more than they admit to, and share servers with thousands of strangers. A KVM-based VPS — the standard virtual-machine instance type sold by DigitalOcean, Linode, and Vultr — gives you a dedicated Linux box for about $4–$6 a month, and WireGuard turns it into a VPN server in under fifteen minutes. This is the actual command-by-command setup, verified against DigitalOcean’s and Vultr’s current documentation.
Why KVM matters, and why WireGuard
“KVM” (kernel-based virtual machine) is the virtualization type behind nearly every modern cloud VPS — it gives your instance its own kernel and full root access, unlike older container-based hosting (OpenVZ) that shares a kernel with other tenants and often blocks the networking modules WireGuard needs. Every droplet on DigitalOcean, every Linode instance, and every Vultr Cloud Compute instance is KVM, so any of the three works for this guide. Pick the cheapest plan with a public IPv4 address — 1 CPU and 1GB RAM is overkill for a personal VPN.
WireGuard was merged into the Linux kernel in version 5.6 (2020) and is now the default choice over OpenVPN because its codebase is roughly 4,000 lines versus OpenVPN’s 100,000+, it uses modern cryptography (Curve25519, ChaCha20, Poly1305, BLAKE2s) with no negotiated cipher suites to misconfigure, and it consistently benchmarks faster on mobile due to its UDP-only, stateless design.
Step 1: Spin up the VPS and install WireGuard
Create an Ubuntu 24.04 instance on any provider, note its public IP, and SSH in as root or a sudo user. WireGuard has shipped in Ubuntu’s default repositories since 18.04, so no PPA is required:
sudo apt update
sudo apt install wireguard qrencode -y
wg --version
qrencode isn’t strictly part of WireGuard — it’s a small utility you’ll use later to turn your phone’s config into a scannable QR code.
Step 2: Generate the server’s key pair
WireGuard authenticates peers with public-key cryptography instead of usernames and passwords. Generate the server’s private and public keys, and lock down the private key’s permissions immediately:
cd /etc/wireguard
umask 077
wg genkey | tee server_private.key | wg pubkey > server_public.key
Run cat server_private.key and cat server_public.key to view them — you’ll paste the private key into the server config next.
Step 3: Write the server configuration
First, identify your server’s default network interface — you’ll need its name for the NAT rule:
ip -o -4 route show to default | awk '{print $5}'
This typically prints eth0 on DigitalOcean/Linode or enp1s0 on Vultr. Create /etc/wireguard/wg0.conf with that interface name substituted in:
[Interface]
Address = 10.8.0.1/24
ListenPort = 51820
PrivateKey = <paste server_private.key here>
PostUp = iptables -A FORWARD -i wg0 -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
PreDown = iptables -D FORWARD -i wg0 -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE
# Peers are appended below, one [Peer] block per device
10.8.0.1/24 is the VPN’s private subnet — the server is always the first address in it, and each client gets the next one (10.8.0.2, 10.8.0.3, and so on). Port 51820/UDP is WireGuard’s conventional default; you can pick a different one, but it must match what clients connect to.
Step 4: Turn on IP forwarding
By default, Linux won’t route traffic between network interfaces — you have to explicitly enable it so the server can forward client traffic out to the internet:
echo 'net.ipv4.ip_forward = 1' | sudo tee -a /etc/sysctl.conf
sudo sysctl -p
Step 5: Open the firewall
If you’re running UFW (Ubuntu’s default firewall), allow the WireGuard port before starting the service, or you’ll have a working tunnel that nothing can reach:
sudo ufw allow 51820/udp
sudo ufw allow OpenSSH
sudo ufw enable
DigitalOcean and Linode leave the cloud-level firewall open by default (UFW is what’s actually blocking you); Vultr’s Cloud Firewall and DigitalOcean’s optional Cloud Firewall product are configured separately in their dashboards if you’ve enabled one — add a matching UDP 51820 rule there too.
Step 6: Start WireGuard and enable it at boot
sudo systemctl start wg-quick@wg0
sudo systemctl enable wg-quick@wg0
sudo systemctl status wg-quick@wg0
sudo wg show wg0
wg show wg0 should list your interface, its public key, and listening port with no peers yet. If systemctl status shows “active (exited)” rather than “active (running),” that’s normal — wg-quick is a one-shot script that configures the interface and then exits.
Step 7: Create a client and generate its QR code
Each device (phone, laptop) is its own “peer” with its own key pair. Generate one:
cd /etc/wireguard
wg genkey | tee client1_private.key | wg pubkey > client1_public.key
Add this peer to the bottom of /etc/wireguard/wg0.conf on the server:
[Peer]
PublicKey = <paste client1_public.key here>
AllowedIPs = 10.8.0.2/32
Then reload the config without dropping the tunnel:
sudo wg syncconf wg0 <(wg-quick strip wg0)
Now write the client’s own config file — this is the file the phone or laptop actually imports — as client1.conf:
[Interface]
PrivateKey = <paste client1_private.key here>
Address = 10.8.0.2/24
DNS = 1.1.1.1
[Peer]
PublicKey = <paste server_public.key here>
Endpoint = <your server's public IP>:51820
AllowedIPs = 0.0.0.0/0
PersistentKeepalive = 25
AllowedIPs = 0.0.0.0/0 routes all of the client’s traffic through the tunnel (a full VPN); set it to just 10.8.0.0/24 instead if you only want access to the server’s private network, not full internet routing. PersistentKeepalive = 25 sends a keepalive packet every 25 seconds — necessary for phones, which otherwise drop the UDP mapping on cellular networks.
Turn that file into a QR code you can scan with the official WireGuard app (iOS/Android) instead of typing it in by hand:
qrencode -t ansiutf8 < client1.conf
In the WireGuard mobile app, tap the "+" button, choose "Scan from QR code," and point the camera at the terminal. On desktop, copy client1.conf to the machine running the official WireGuard client and import it directly — there's no QR step needed there.
Treat that QR code and .conf file like a password: anyone who scans it gets full access to the tunnel. Delete client1.conf from the server's disk once you've transferred it, since the private key doesn't need to live there.
Verify it actually works
Connect from the client, then from inside the tunnel check that your traffic is actually exiting through the VPS:
ping -c 4 10.8.0.1
curl ifconfig.me
The ping should succeed, and curl ifconfig.me should return your VPS's public IP, not your home or office IP. Back on the server, sudo wg show wg0 will now show the peer with a "latest handshake" timestamp and transfer counters — proof data is actually flowing.
Repeat Step 7 for every additional device, incrementing the IP each time (10.8.0.3, 10.8.0.4...). One $5/month KVM instance comfortably handles a household's worth of phones and laptops, and because you control the box end to end, there's no provider logging your browsing to worry about — only your own.
