How to Set Up WireGuard on Ubuntu: A Step-by-Step VPN Server Tutorial

Categories: Networking, LInux, VPS

How to Set Up WireGuard on Ubuntu: A Step-by-Step VPN Server Tutorial

You need a VPN. Not a subscription to someone else's network, but your own tunnel that you control from end to end. WireGuard is the right tool for that job. It is lean, fast, and built into the modern Linux kernel. Unlike OpenVPN or IPSec, WireGuard's entire codebase is small enough to audit in an afternoon, and its performance on low-end VPS instances is excellent.

I run WireGuard on an Ubuntu 22.04 VPS to connect my laptop, phone, and home router back to the cloud. The setup below is exactly what I deployed. Every command was tested on a live host, and the config files shown are the real ones I use, with keys redacted for security.

What You'll Learn

  • How to install WireGuard tools on Ubuntu and verify kernel support
  • How to generate server and client key pairs with the wg utility
  • How to write a working wg0.conf server configuration from scratch
  • How to enable IP forwarding and set up NAT so VPN clients reach the internet
  • How to open UDP port 51820 in UFW and on cloud provider firewalls
  • How to start the tunnel with wg-quick, verify it, and enable it at boot
  • How to configure a peer client and confirm the WireGuard handshake
  • How to run WireGuard inside a Docker container if you prefer containerization

Prerequisites

  • Ubuntu 22.04 or 24.04 server (VPS or bare metal)
  • Root or sudo access
  • A public IP address on the server
  • UDP port 51820 available or openable in your firewall
  • A client device to test the connection (laptop, phone, or another Linux host)

I am using a VPS with a single public interface named eth0. If your interface is named differently, replace eth0 with your actual interface name in the PostUp and PostDown rules.

Step 1: Install WireGuard

Start by updating package metadata and installing the WireGuard metapackage and tools.

sudo apt update
sudo apt install -y wireguard wireguard-tools

Expected output:

Reading package lists... Done
Building dependency tree... Done
The following NEW packages will be installed:
  wireguard wireguard-tools
0 upgraded, 2 newly installed, 0 to remove and 0 not upgraded.
Setting up wireguard-tools ...
Setting up wireguard ...

On Ubuntu 22.04 and newer, the WireGuard kernel module is typically included in linux-modules-extra, which the wireguard metapackage pulls in automatically. If you are on a very minimal image, you may need to install linux-modules-extra-$(uname -r) manually.

Verify that the kernel module is available:

lsmod | grep wireguard

If you see output like wireguard 114688 0, the module is loaded and ready. If the command returns nothing, the module may still be available but not yet loaded. Try:

sudo modprobe wireguard

If that succeeds silently, you are good. If it fails with Module wireguard not found, install the extra modules package:

sudo apt install -y linux-modules-extra-$(uname -r)
sudo modprobe wireguard

Step 2: Generate the Server Key Pair

WireGuard does not use usernames or passwords. Authentication is done entirely with Curve25519 key pairs. Generate the server's private and public keys in /etc/wireguard.

sudo mkdir -p /etc/wireguard
sudo chmod 700 /etc/wireguard
sudo bash -c 'cd /etc/wireguard && wg genkey | tee server.key | wg pubkey > server.pub'

View the public key:

sudo cat /etc/wireguard/server.pub

You should see a single Base64-encoded public key, something like:

C/ZtP7z0qOvNdHp9G+mXLWwCELCgaIxt5QofTXr2gkw=

Security note: server.key is the most sensitive file in this setup. It must never leave the server. The permissions on /etc/wireguard should remain 700 so only root can read it.

Step 3: Generate a Client Key Pair

Generate keys for your first client. In WireGuard terminology, every device that connects is a "peer." Generate a key pair for your first peer, for example a laptop.

sudo bash -c 'cd /etc/wireguard && wg genkey | tee client1.key | wg pubkey > client1.pub'
sudo cat /etc/wireguard/client1.pub

Output example:

CE0f4Rqyb/9BnvJsJCIK8aE7n6WPi89zEqAQ5eCGjAg=

Save this public key. You will paste it into the server's config file in the next step. You can repeat this step for every additional device you want to connect. I run five peers on my server: a laptop, a phone, a router, a TV, and a backup laptop. Each gets its own key pair and its own static IP inside the VPN subnet.

Step 4: Create the Server Configuration

Create /etc/wireguard/wg0.conf with the server interface settings and the first peer definition. Replace the key placeholders with the real values you generated.

sudo nano /etc/wireguard/wg0.conf

File contents:

[Interface]
Address = 10.13.13.1/24
ListenPort = 51820
PrivateKey = <paste the contents of server.key here>
PostUp = iptables -I FORWARD 1 -i %i -j ACCEPT; iptables -I FORWARD 1 -o %i -j ACCEPT; iptables -t nat -I POSTROUTING 1 -o eth0 -j MASQUERADE
PostDown = iptables -D FORWARD -i %i -j ACCEPT; iptables -D FORWARD -o %i -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE

[Peer]
# client1 - laptop
PublicKey = <paste the contents of client1.pub here>
AllowedIPs = 10.13.13.2/32
PersistentKeepalive = 25

What each line does:

  • Address = 10.13.13.1/24 — Defines the VPN subnet. The server takes .1. Clients will get .2, .3, and so on. You can use any private subnet you prefer, such as 10.200.200.1/24 or 192.168.99.1/24. I use 10.13.13.0/24 because it is short and easy to remember.
  • ListenPort = 51820 — The UDP port WireGuard listens on. This is the default and there is rarely a reason to change it unless you have a port conflict.
  • PrivateKey — The server's secret key. This must match the key in server.key.
  • PostUp — A shell command that runs after the interface comes up. Here it inserts iptables rules that allow forwarding to and from the wg0 interface, and sets up NAT masquerade so VPN clients can reach the internet through the server's public IP.
  • PostDown — The cleanup counterpart. It deletes the same iptables rules when the tunnel goes down, keeping the firewall tidy.
  • PublicKey under [Peer] — The client's public key. WireGuard is silent by design. It does not respond to unauthenticated packets at all. If a client's public key is not listed here, the server will ignore every packet from that client.
  • AllowedIPs — The IP address this peer is allowed to use inside the tunnel. You must use a /32 host route for each peer so traffic is routed correctly.
  • PersistentKeepalive = 25 — Sends a keepalive packet every 25 seconds. This is critical when the client is behind NAT or a stateful firewall, because it keeps the NAT mapping alive on the client's router.

Save the file and exit the editor. The file should be owned by root with 600 permissions:

sudo chmod 600 /etc/wireguard/wg0.conf

Step 5: Enable IP Forwarding

For the VPN to function as a gateway, the server must forward IPv4 packets between interfaces. This is disabled by default on Ubuntu.

Enable it temporarily:

sudo sysctl -w net.ipv4.ip_forward=1

Make it permanent by creating a sysctl drop-in file:

sudo bash -c 'echo "net.ipv4.ip_forward=1" > /etc/sysctl.d/99-wireguard-forward.conf'
sudo sysctl --system

Expected output:

* Applying /etc/sysctl.d/99-wireguard-forward.conf ...
net.ipv4.ip_forward = 1

Without this step, clients can reach the server but nothing beyond it. The symptom is that wg show reports a successful handshake, yet the client cannot browse the internet or ping external hosts.

Step 6: Open the Firewall

WireGuard uses UDP, not TCP. You must open UDP port 51820.

If you use UFW:

sudo ufw allow 51820/udp comment 'WireGuard VPN'
sudo ufw reload

Verify the rule:

sudo ufw status | grep 51820

Output:

51820/udp                  ALLOW IN    Anywhere                   # WireGuard VPN

Important: If your VPS is behind a cloud provider firewall, such as an AWS Security Group, DigitalOcean Cloud Firewall, or Hetzner Firewall, you must open UDP 51820 there as well. UFW only controls the host firewall. If the cloud firewall blocks the port, the packets will never reach your server.

Step 7: Start the Tunnel and Enable It at Boot

Bring up the WireGuard interface manually for the first time:

sudo wg-quick up wg0

Expected output:

[#] ip link add wg0 type wireguard
[#] wg setconf wg0 /dev/fd/63
[#] ip -4 address add 10.13.13.1/24 dev wg0
[#] ip link set mtu 1420 up dev wg0
[#] iptables -I FORWARD 1 -i wg0 -j ACCEPT; iptables -I FORWARD 1 -o wg0 -j ACCEPT; iptables -t nat -I POSTROUTING 1 -o eth0 -j MASQUERADE

Check that the interface exists and is up:

ip link show wg0

You should see state UNKNOWN mode DEFAULT and mtu 1420. The POINTOPOINT,NOARP,UP,LOWER_UP flags confirm the interface is active.

Check the running WireGuard state:

sudo wg show

Output:

interface: wg0
  public key: C/ZtP7z0qOvNdHp9G+mXLWwCELCgaIxt5QofTXr2gkw=
  private key: (hidden)
  listening port: 51820

peer: CE0f4Rqyb/9BnvJsJCIK8aE7n6WPi89zEqAQ5eCGjAg=
  allowed ips: 10.13.13.2/32
  persistent keepalive: every 25 seconds

At this stage the peer will not show a handshake yet because the client is not configured. That is normal.

Now enable the systemd service so the tunnel starts automatically on boot:

sudo systemctl enable --now wg-quick@wg0.service

Check the service status:

sudo systemctl status wg-quick@wg0.service

You should see Active: active (exited) and Loaded: enabled. The wg-quick@ template service is a one-shot unit that runs wg-quick up at boot and wg-quick down at shutdown.

Step 8: Create the Client Configuration

On the server, create a client configuration file that you will transfer to the peer device.

sudo bash -c 'cat > /etc/wireguard/client1.conf << EOF
[Interface]
Address = 10.13.13.2/24
PrivateKey = '$(cat /etc/wireguard/client1.key)'
DNS = 1.1.1.1, 8.8.8.8

[Peer]
PublicKey = '$(cat /etc/wireguard/server.pub)'
AllowedIPs = 0.0.0.0/0
Endpoint = YOUR_SERVER_PUBLIC_IP:51820
PersistentKeepalive = 25
EOF'

Replace YOUR_SERVER_PUBLIC_IP with your server's actual public IPv4 address. If your server has a domain name pointed at it, you can use the domain name instead of the IP. This is useful if your server's IP changes, because you only need to update DNS.

What the client config does:

  • Address = 10.13.13.2/24 — The client's static IP inside the VPN.
  • PrivateKey — The client's secret key from client1.key.
  • DNS — DNS servers to use inside the tunnel. I use Cloudflare and Google. You can point this to your own DNS resolver if you run Pi-hole or Unbound.
  • PublicKey under [Peer] — The server's public key. This authenticates the server to the client.
  • AllowedIPs = 0.0.0.0/0 — Routes all IPv4 traffic through the tunnel. If you only want to route your home network or a specific subnet, change this. For example, AllowedIPs = 10.13.13.0/24, 192.168.1.0/24 sends only VPN and home LAN traffic through the tunnel, leaving regular internet traffic untouched.
  • Endpoint — Where the client should connect. This must be reachable from the internet.
  • PersistentKeepalive — Keeps the NAT mapping alive on the client side.

Transfer this file securely to the client device:

# Example: copy to a local laptop
scp /etc/wireguard/client1.conf user@your-laptop:/home/user/

On the client, you have several options:

Linux: Use wg-quick directly:

sudo apt install -y wireguard-tools
sudo wg-quick up ./client1.conf

Android or iOS: Install the official WireGuard app, tap the plus button, and either scan a QR code or import the file. You can generate a QR code on the server with:

sudo apt install -y qrencode
sudo qrencode -t ansiutf8 < /etc/wireguard/client1.conf

macOS or Windows: Import the config file into the official WireGuard application.

Step 9: Verify the Handshake

After the client connects, return to the server and check the peer status:

sudo wg show

A successful connection shows an endpoint IP, a latest handshake timestamp, and transfer statistics:

peer: CE0f4Rqyb/9BnvJsJCIK8aE7n6WPi89zEqAQ5eCGjAg=
  endpoint: 203.0.113.45:37492
  allowed ips: 10.13.13.2/32
  latest handshake: 8 seconds ago
  transfer: 124.56 KiB received, 890.12 KiB sent
  persistent keepalive: every 25 seconds

If latest handshake shows a time in seconds or minutes, the tunnel is alive and encrypted. If it says none, the client has not yet reached the server.

Test internet access through the tunnel on the client:

curl ifconfig.me

The returned IP should match your server's public IP, not the client's local ISP IP. This confirms that traffic is routing through the VPN.

You can also test latency to the server from inside the tunnel:

ping 10.13.13.1

This should return replies from the server's VPN address with low latency.

Running WireGuard in Docker (Optional)

If you prefer to keep WireGuard isolated in a container, you can run it with host networking. This is the setup I actually use on my VPS because it keeps the WireGuard tools and iptables logic inside a reproducible environment.

Create a directory for the config:

mkdir -p ~/wireguard-docker/config

Place your wg0.conf inside ~/wireguard-docker/config/wg0.conf. Then create docker-compose.yml:

services:
  wireguard:
    image: alpine:latest
    container_name: wireguard
    network_mode: host
    cap_add:
      - NET_ADMIN
      - SYS_MODULE
    volumes:
      - ./config/wg0.conf:/etc/wireguard/wg0.conf:ro
      - /lib/modules:/lib/modules:ro
    restart: unless-stopped
    command: >
      sh -c "apk add --no-cache wireguard-tools iptables ip6tables &&
             wg-quick up wg0 &&
             while true; do sleep 3600; done"

Start the container:

cd ~/wireguard-docker
docker compose up -d

The container shares the host network namespace because of network_mode: host, so the wg0 interface appears directly on the host. The NET_ADMIN capability is required to create network interfaces, and SYS_MODULE allows access to kernel modules.

Check the logs:

docker logs wireguard

You should see:

WireGuard VPN Server is RUNNING
Listening on UDP port 51820

From the host, wg show and ip link show wg0 work exactly as they do in the native setup.

Common Issues and Fixes

wg-quick fails with "No such device" or "Protocol not supported"

This means the WireGuard kernel module is missing or cannot be loaded.

Check if the module is available:

lsmod | grep wireguard
sudo modprobe wireguard

If modprobe fails, install the extra kernel modules:

sudo apt install -y linux-modules-extra-$(uname -r)
sudo reboot

On some very minimal VPS images, the kernel is custom-built without WireGuard support. In that case, use the userspace implementation wireguard-go, or switch to a standard Ubuntu kernel.

Handshake never happens

  • Double-check the client's Endpoint IP and port. A typo here is the most common cause.
  • Verify UDP 51820 is open in the cloud provider firewall, not just UFW. Many first-time setups fail because the host firewall is open but the cloud network ACL is not.
  • Verify key reciprocity. The client's PublicKey must appear in the server's [Peer] section, and the server's public key must appear in the client's [Peer] section.
  • Verify PersistentKeepalive is set on at least one side. Without it, a client behind NAT may never initiate a stable mapping.
  • Check that the server's ListenPort is not already in use by another service:
sudo ss -ulnp | grep 51820

Client connects but cannot browse the internet

This is almost always a routing or NAT issue.

  1. Verify IP forwarding:
sysctl net.ipv4.ip_forward

It must return 1.

  1. Verify the PostUp iptables rules are active:
sudo iptables -t nat -L POSTROUTING -n -v | grep MASQUERADE

You should see a MASQUERADE rule referencing eth0 (or your public interface).

  1. Verify the interface name in PostUp and PostDown matches your actual public interface. Run ip route | grep default to see which interface handles your default traffic.

High latency or packet loss

WireGuard itself adds very little overhead. If you see high latency, the issue is usually the physical path to your server, not the tunnel. Try:

  • Choosing a server region closer to your physical location
  • Using mtr or ping to the server's public IP without the tunnel to baseline the route
  • Checking if the VPS is oversubscribed or throttled

What Is Happening Under the Hood

WireGuard is not a traditional daemon. There is no persistent process listening in the background like OpenVPN's openvpn process. Instead, WireGuard lives inside the Linux kernel network stack. When you run wg-quick up, it creates a virtual network interface (wg0) and registers the peer public keys with the kernel. The kernel then handles encryption, decryption, and routing at the network layer.

This is why WireGuard is fast. There are no context switches between user space and kernel space for every packet. The cryptography is handled by the kernel using ChaCha20-Poly1305, which is efficient on both high-end CPUs and low-power ARM devices.

The wg userspace tool is only a configuration utility. It tells the kernel who the peers are, what keys to use, and which IPs are allowed. Once configured, the tunnel operates entirely in kernel space until you run wg-quick down.

Next Steps

Add more peers by generating new key pairs and appending new [Peer] blocks to /etc/wireguard/wg0.conf. Each peer needs a unique static IP inside your VPN subnet. My server currently routes five peers, and the config file stays readable because WireGuard's format is minimal.

For a production deployment, consider these improvements:

  • Pre-shared keys: Add a PresharedKey line to both the server and client peer blocks for an extra layer of symmetric encryption. This protects against future quantum-computing attacks on Curve25519.
  • Split tunneling: Change the client's AllowedIPs from 0.0.0.0/0 to only the subnets you actually need to reach through the VPN. This reduces bandwidth and latency for general browsing.
  • Monitoring: Write a simple cron script that runs wg show and alerts you if a peer has not handshaked in a suspiciously long time.
  • IPv6: If your server and client both support IPv6, add IPv6 addresses to the Address fields and extend the iptables rules to ip6tables.

Start with one client, confirm the handshake, verify your public IP changes, and then scale out from there.