TL;DR:
- Install
wireguard, generate a server key pair, write/etc/wireguard/wg0.conf, turn on IPv4 forwarding, add NAT in UFW, and startwg-quick@wg0with systemd. - WireGuard runs over UDP only. This server listens on 51820/udp, the port in WireGuard's own examples.
- Ubuntu 24.04 ships wireguard-tools 1.0.20210914, and the WireGuard module comes with the Ubuntu kernel (part of Linux since 5.6). Ubuntu 26.04 ships 1.0.20250521 with the same wg and wg-quick commands.
- Phones import a client from a QR code made with
qrencode; Windows, macOS and Linux import the.conffile. - WireGuard publishes no hardware minimum. Ubuntu lists 1 GB of RAM and 4 GB of storage as the minimum for cloud images.
- The setup works when
sudo wg showlists alatest handshakefor each device.
Applies to: Ubuntu 24.04 LTS · wireguard-tools 1.0.20210914 · UFW 0.36.2 · checked October 2026
WireGuard is an open-source VPN protocol, built into the Linux kernel since version 5.6, that encrypts IP packets and sends them over UDP between peers that hold each other's public keys. This tutorial sets up a WireGuard server on Ubuntu 24.04 with wg-quick and systemd, routes client traffic through NAT in the Uncomplicated Firewall (UFW), and connects a phone by QR code and a laptop by config file. An optional section covers wg-easy for a web interface. A VPS WireGuard server keeps one public IP address and stays online while your devices move between home, mobile and public networks.
Prerequisites
- A VPS running Ubuntu 24.04 LTS or 26.04 LTS with root SSH access. Ubuntu's minimum for cloud images is 1 GB of RAM and 4 GB of storage. Arct Cloud Linux images accept an SSH public key at deploy, and the Cost Optimized
cvm.picoplan meets that minimum (1 vCPU, 1 GB RAM, 15 GB NVMe, 1 TB of outbound transfer per month). Only outbound transfer counts toward the allowance. Each gigabyte through the tunnel leaves the server once, plus WireGuard's packet headers. - A server location close to you. Websites see the server's IP address while the tunnel is up, and a nearby server keeps latency low. Arct Cloud allows personal VPN use for lawful purposes only. Its public locations are Amsterdam, London, Frankfurt, Salt Lake City and Tokyo, and the deploy flow shows the plans each one offers.
- A non-root user with sudo privileges and SSH key login. How to Generate an SSH Key on Windows, macOS, and Linux creates the key and the sudo user.
- The WireGuard app on each device, from the WireGuard install page.
Commands run as the sudo user. Replace values in capitals, such as YOUR_SERVER_IP, with your own. The examples use 203.0.113.10 as the server IP and 10.8.0.0/24 as the tunnel subnet.
Step 1: Install WireGuard and qrencode
The wireguard package installs the wg and wg-quick tools. The WireGuard kernel module ships with the Ubuntu kernel. qrencode draws QR codes in the terminal for phone clients.
sudo apt update
sudo apt install wireguard qrencode
wg --version
The output looks similar to this:
wireguard-tools v1.0.20210914 - https://git.zx2c4.com/wireguard-tools/
On Ubuntu 26.04 the version is v1.0.20250521.
Step 2: Generate the server key pair
Each WireGuard peer has a Curve25519 private key and a public key derived from it. Generate the server pair in /etc/wireguard, a directory the package creates with mode 700 so only root can read it:
wg genkey | sudo tee /etc/wireguard/server.key | wg pubkey | sudo tee /etc/wireguard/server.pub
sudo chmod 600 /etc/wireguard/server.key
The command prints the server public key, a 44-character string that ends in =. The private key stays on the server.
Step 3: Write the server configuration
wg-quick reads /etc/wireguard/wg0.conf and creates an interface named wg0. Create the file with the private key filled in:
sudo tee /etc/wireguard/wg0.conf > /dev/null <<EOF
[Interface]
Address = 10.8.0.1/24
ListenPort = 51820
PrivateKey = $(sudo cat /etc/wireguard/server.key)
EOF
sudo chmod 600 /etc/wireguard/wg0.conf
Address gives the server 10.8.0.1 inside the tunnel. ListenPort sets UDP port 51820; without it, WireGuard picks a random port each time the interface starts. Without a SaveConfig line, the file stays the source of truth.
Step 4: Turn on IP forwarding
Linux drops packets that arrive on one interface and are addressed to another network unless forwarding is on. Enable IPv4 forwarding in a sysctl file that applies at every boot:
echo "net.ipv4.ip_forward = 1" | sudo tee /etc/sysctl.d/99-wireguard.conf > /dev/null
sudo sysctl -p /etc/sysctl.d/99-wireguard.conf
The output looks similar to this:
net.ipv4.ip_forward = 1
UFW's own /etc/ufw/sysctl.conf leaves ip_forward commented out, so it does not undo this setting.
Step 5: Add NAT and firewall rules in UFW
Find the server's public network interface:
ip route show default
The output looks similar to this:
default via 203.0.113.1 dev eth0 proto static
The name after dev is the interface. This tutorial uses eth0. If your output shows another name, such as ens3, replace eth0 in the next two blocks.
Append a NAT block to /etc/ufw/before.rules. It rewrites the source address of tunnel traffic to the server's public IP, as in the IP masquerading example of the ufw-framework manual. Run this block once:
sudo tee -a /etc/ufw/before.rules > /dev/null <<'EOF'
*nat
:POSTROUTING ACCEPT [0:0]
-A POSTROUTING -s 10.8.0.0/24 -o eth0 -j MASQUERADE
COMMIT
EOF
Allow SSH first so the firewall keeps your session, then open the WireGuard port, allow forwarding from wg0 to eth0, and enable UFW. If SSH listens on a port other than 22, replace OpenSSH with YOUR_SSH_PORT/tcp:
sudo ufw allow OpenSSH
sudo ufw allow 51820/udp
sudo ufw route allow in on wg0 out on eth0
sudo ufw enable
Type y to confirm. If UFW was already active, run sudo ufw reload instead of sudo ufw enable so it loads the NAT block. Check the rules:
sudo ufw status verbose
The output looks similar to this:
Status: active
Logging: on (low)
Default: deny (incoming), allow (outgoing), deny (routed)
New profiles: skip
To Action From
-- ------ ----
22/tcp (OpenSSH) ALLOW IN Anywhere
51820/udp ALLOW IN Anywhere
22/tcp (OpenSSH (v6)) ALLOW IN Anywhere (v6)
51820/udp (v6) ALLOW IN Anywhere (v6)
Anywhere on eth0 ALLOW FWD Anywhere on wg0
Anywhere (v6) on eth0 ALLOW FWD Anywhere (v6) on wg0
Step 6: Start the WireGuard server on Ubuntu with systemd
The wg-quick@wg0 systemd service brings up wg0 at boot. Enable and start it, then check the interface:
sudo systemctl enable --now wg-quick@wg0
sudo wg show
The output looks similar to this:
interface: wg0
public key: YOUR_SERVER_PUBLIC_KEY
private key: (hidden)
listening port: 51820
systemctl status wg-quick@wg0 reports active (exited) because wg-quick up runs once and the interface stays in the kernel.
Step 7: Create a script that adds clients
Every client needs its own key pair, a tunnel IP and a [Peer] block on the server. A short script does all three and reloads wg0 without dropping connected devices. Create it:
sudo nano /usr/local/sbin/wg-add-client
Paste the script, replace YOUR_SERVER_IP with the server's public IP, then save with Ctrl+O and exit with Ctrl+X:
#!/bin/bash
set -euo pipefail
if [ "$#" -ne 2 ]; then
echo "Usage: wg-add-client NAME TUNNEL_IP" >&2
exit 1
fi
NAME="$1"
CLIENT_IP="$2"
ENDPOINT="YOUR_SERVER_IP:51820"
DIR=/etc/wireguard/clients
if [[ "$ENDPOINT" == YOUR_* ]]; then
echo "Set ENDPOINT in $0 to your server IP first" >&2
exit 1
fi
if [ -e "$DIR/$NAME.pub" ]; then
echo "Client $NAME already exists" >&2
exit 1
fi
if grep -qxF "AllowedIPs = $CLIENT_IP/32" /etc/wireguard/wg0.conf; then
echo "Tunnel IP $CLIENT_IP is already in use" >&2
exit 1
fi
umask 077
mkdir -p "$DIR"
wg genkey | tee "$DIR/$NAME.key" | wg pubkey > "$DIR/$NAME.pub"
cat > "$DIR/$NAME.conf" <<EOF
[Interface]
PrivateKey = $(cat "$DIR/$NAME.key")
Address = $CLIENT_IP/32
DNS = 1.1.1.1
[Peer]
PublicKey = $(cat /etc/wireguard/server.pub)
Endpoint = $ENDPOINT
AllowedIPs = 0.0.0.0/0, ::/0
PersistentKeepalive = 25
EOF
cat >> /etc/wireguard/wg0.conf <<EOF
[Peer]
PublicKey = $(cat "$DIR/$NAME.pub")
AllowedIPs = $CLIENT_IP/32
EOF
systemctl reload wg-quick@wg0
echo "Added $NAME with tunnel IP $CLIENT_IP: $DIR/$NAME.conf"
The script stops without changes if ENDPOINT still holds the placeholder, if the client name exists, or if another peer already uses the tunnel IP. Make it executable and add a phone and a laptop:
sudo chmod 700 /usr/local/sbin/wg-add-client
sudo wg-add-client phone 10.8.0.2
sudo wg-add-client laptop 10.8.0.3
The output looks similar to this:
Added phone with tunnel IP 10.8.0.2: /etc/wireguard/clients/phone.conf
Added laptop with tunnel IP 10.8.0.3: /etc/wireguard/clients/laptop.conf
AllowedIPs = 0.0.0.0/0, ::/0 routes all traffic into the tunnel. The tunnel carries IPv4 only, so the server drops IPv6 packets and your IPv6 address stays hidden. Sites that are reachable only over IPv6 do not load while the tunnel is up. DNS = 1.1.1.1 is Cloudflare's resolver; use any resolver you trust. PersistentKeepalive = 25 keeps the tunnel open through home and mobile NAT, the interval WireGuard's docs suggest.
Step 8: Connect your devices to the WireGuard server
Phone: scan a QR code
Print the phone config as a QR code. Widen the terminal if the code wraps.
sudo qrencode -t ansiutf8 -r /etc/wireguard/clients/phone.conf
In the WireGuard app, tap +, then Create from QR code on iOS or Scan from QR code on Android. Scan the code, name the tunnel and switch it on. The QR code contains the phone's private key, so run clear after the scan.
Windows and macOS: import the file
Copy the laptop config into your home directory on the server:
sudo install -m 600 -o "$USER" /etc/wireguard/clients/laptop.conf ~/laptop.conf
From a terminal on the laptop, download it, then delete the copy on the server with rm ~/laptop.conf:
scp YOUR_USER@YOUR_SERVER_IP:laptop.conf .
Replace YOUR_USER with your sudo user. In the WireGuard app, choose Import tunnel(s) from file, select laptop.conf and click Activate.
Linux: use wg-quick
On a Linux laptop:
sudo apt install wireguard
sudo install -m 600 laptop.conf /etc/wireguard/laptop.conf
sudo wg-quick up laptop
wg-quick applies the DNS line through resolvconf, which systemd-resolved provides on Ubuntu.
Step 9: Verify the handshake and exit IP
On the server, list the peers:
sudo wg show
The output looks similar to this:
interface: wg0
public key: YOUR_SERVER_PUBLIC_KEY
private key: (hidden)
listening port: 51820
peer: LAPTOP_PUBLIC_KEY
endpoint: 198.51.100.20:51413
allowed ips: 10.8.0.3/32
latest handshake: 9 seconds ago
transfer: 61.27 KiB received, 412.80 KiB sent
peer: PHONE_PUBLIC_KEY
endpoint: 198.51.100.44:49152
allowed ips: 10.8.0.2/32
latest handshake: 1 minute, 12 seconds ago
transfer: 148.62 KiB received, 1.21 MiB sent
A latest handshake line means the device and the server authenticated each other. On the Linux laptop, check the public IP that websites see:
curl -4 https://ifconfig.me
203.0.113.10
On a phone, any what-is-my-IP page shows the server's IP.
Remove a device and update WireGuard
To revoke a device, delete the [Peer] block with its tunnel IP (AllowedIPs = 10.8.0.2/32 for the phone) from /etc/wireguard/wg0.conf, delete its files, and reload:
sudo nano /etc/wireguard/wg0.conf
sudo rm -f /etc/wireguard/clients/phone.{key,pub,conf}
sudo systemctl reload wg-quick@wg0
The braces name the three files because your shell cannot list the root-only directory to expand a *. The reload runs wg syncconf, which keeps the other sessions up. After a device imports its config, you can delete its .key and .conf files; the server needs only the public key.
WireGuard updates arrive through sudo apt upgrade: the tools as a package and the kernel module with each kernel update. Reboot after a kernel update to load the new module.
Use wg-easy for a web interface (optional)
wg-easy is an open-source (AGPL-3.0) web interface that runs WireGuard in a Docker container and creates clients with QR codes from the browser. It replaces Steps 2 to 9 and uses UDP 51820 itself, so run it on a fresh server or stop the manual setup with sudo systemctl disable --now wg-quick@wg0. It needs Docker Engine and the Compose plugin: see How to Install Docker and Docker Compose on Ubuntu 24.04 and 26.04.
Download the official Compose file and open it:
sudo mkdir -p /etc/docker/containers/wg-easy
cd /etc/docker/containers/wg-easy
sudo curl -o docker-compose.yml https://raw.githubusercontent.com/wg-easy/wg-easy/master/docker-compose.yml
sudo nano docker-compose.yml
Uncomment environment: and set INSECURE=true. Publish the web UI on the loopback address only, and give the WireGuard port the explicit address 0.0.0.0, because the Docker tutorial's daemon settings publish ports that have no address on 127.0.0.1. Keep the 15 image tag, which wg-easy recommends: it follows the latest 15.x release without breaking changes. The edited lines look like this:
environment:
- INSECURE=true
image: ghcr.io/wg-easy/wg-easy:15
ports:
- "0.0.0.0:51820:51820/udp"
- "127.0.0.1:51821:51821/tcp"
Start the container:
sudo docker compose up -d
Docker publishes UDP 51820 through its own firewall rules, which bypass UFW. The UI port stays on 127.0.0.1. From your computer, open an SSH tunnel, then browse to http://localhost:51821:
ssh -L 51821:127.0.0.1:51821 YOUR_USER@YOUR_SERVER_IP
The setup page asks for an admin username and password, then whether you have an existing setup: choose No. It then asks for the host clients connect to (your server IP) and the port (51820). INSECURE=true lets the login cookie work over plain HTTP; the SSH tunnel encrypts that connection.
To update wg-easy to the latest 15.x release, pull the image and recreate the container:
cd /etc/docker/containers/wg-easy
sudo docker compose pull
sudo docker compose up -d
Troubleshoot common WireGuard errors
| Symptom | Cause | Fix |
|---|---|---|
No latest handshake line in sudo wg show | UDP 51820 is blocked, or the client has the wrong Endpoint or server public key | Check sudo ufw status for 51820/udp, then compare the client's Endpoint and PublicKey with the server |
| Handshake works, but no websites load | IP forwarding is off, or the NAT rule names the wrong interface | sysctl net.ipv4.ip_forward must print 1. Match -o eth0 in /etc/ufw/before.rules to ip route show default, then run sudo ufw reload |
| Small pages load, large downloads stall | The automatic MTU (route MTU minus 80) is too large for the client's network | Add MTU = 1280 under [Interface] in the client config |
wg-quick: `wg0' already exists | The systemd service already brought wg0 up | Use sudo systemctl restart wg-quick@wg0 instead of wg-quick up wg0 |
resolvconf: command not found on a Linux client | wg-quick needs resolvconf for the DNS line | Install systemd-resolved, or delete the DNS line |
FAQ
How do I create a VPN with WireGuard?
Install WireGuard on a server, generate a key pair for the server and for each device, list each device as a peer in the server config, and open one UDP port. On Ubuntu 24.04 that takes the nine steps above.
Does WireGuard use TCP or UDP?
WireGuard uses UDP only. Its documentation lists the lack of a TCP mode as a known limitation, because tunneling TCP over TCP performs poorly. A network that blocks outbound UDP also blocks WireGuard.
What is the default port number for WireGuard?
WireGuard has no fixed default port. Port 51820/udp appears in WireGuard's own examples and is the port this tutorial uses. If the config has no ListenPort line, WireGuard picks a random port each time the interface starts.
What are the downsides of using WireGuard VPN?
WireGuard runs over UDP only and has no user accounts or automatic address assignment, so each device needs its own key pair and a tunnel IP set by hand. It is not post-quantum secure by default; a preshared key adds that layer.
How do I add another device to my WireGuard server?
Run the wg-add-client script from Step 7 again with a new name and the next free tunnel IP, such as 10.8.0.4. It adds the peer, rejects a tunnel IP that another device already uses, and reloads the interface without dropping connected devices. Give every device its own config.
Can I run a WireGuard server on Ubuntu 26.04?
Yes. Ubuntu 26.04 LTS ships wireguard-tools 1.0.20250521 and UFW 0.36.2, which keep the wg, wg-quick and ufw commands this tutorial uses. The version check in Step 1 reports the newer release.
Why does WireGuard connect but have no internet?
A handshake without internet access points to IP forwarding being off or a NAT rule that names the wrong network interface. Check that forwarding is set to 1 (Step 4) and that the interface in the UFW NAT rule matches the server's default route (Step 5).
Next steps
- What Is a VPS? How It Works and What You Can Run on One
- How to Upgrade Ubuntu 24.04 to 26.04 LTS on a VPS
- WireGuard Quick Start and the wg-quick manual
Arct Cloud Cost Optimized plans run on AMD EPYC Milan 7003 Series processors up to 3.7 GHz with NVMe storage. Compare plans.
This work is licensed under CC BY-NC-SA 4.0.