How to Self-Host WireGuard VPN with Docker Compose: Secure Access to Your Home Network

Why WireGuard?

There’s a particular kind of peace that comes from knowing you can reach your home network from anywhere in the world — your files, your services, your smart home — all through an encrypted tunnel that’s fast, modern, and surprisingly simple to set up.

That’s WireGuard. Unlike OpenVPN with its sprawling configuration and heavy handshake, WireGuard is lean. The entire codebase is around 4,000 lines of C — small enough to audit over a coffee. It uses state-of-the-art cryptography (Noise protocol framework, Curve25519, ChaCha20, Poly1305) and connects almost instantly. Think of it as the SSH of VPNs: minimal, fast, and reliable.

In this guide, we’ll set up WireGuard in a Docker container with a clean web UI for managing clients, so you can generate QR codes for your phone and add new devices without touching the command line again.

Prerequisites

  • A Linux server with Docker and Docker Compose installed
  • Root or sudo access
  • A public IP address (or a VPS)
  • Port 51820/udp open in your firewall
  • Basic familiarity with the terminal

This guide assumes Ubuntu/Debian, but the Docker setup works on any Linux distribution.

Step 1: Create the Project Directory

Let’s start by creating a clean directory structure:

mkdir -p ~/wireguard-docker/config
cd ~/wireguard-docker

Simple enough. All our configuration and data will live here.

Step 2: Create the Docker Compose File

We’ll use the weejewel/wg-easy image, which bundles WireGuard with a friendly web-based admin interface. Create docker-compose.yml:

cat <<'EOF' > docker-compose.yml
version: "3.8"

services:
  wg-easy:
    image: weejewel/wg-easy
    container_name: wg-easy
    environment:
      - WG_HOST=your-domain-or-ip.com
      - PASSWORD=your-secure-admin-password
      - WG_PORT=51820
      - WG_DEFAULT_ADDRESS=10.8.0.x
      - WG_DEFAULT_DNS=1.1.1.1
      - WG_ALLOWED_IPS=0.0.0.0/0, ::/0
    ports:
      - "51820:51820/udp"
      - "51821:51821/tcp"
    volumes:
      - ./config:/etc/wireguard
    cap_add:
      - NET_ADMIN
      - SYS_MODULE
    sysctls:
      - net.ipv4.ip_forward=1
      - net.ipv4.conf.all.src_valid_mark=1
    restart: unless-stopped
EOF

Let’s break down what each part does:

  • WG_HOST: Your server’s public IP or domain name. This is what clients will connect to.
  • PASSWORD: The admin password for the web UI. Make this strong. This is your VPN’s front door.
  • WG_PORT: The UDP port WireGuard listens on. Default is 51820.
  • WG_DEFAULT_ADDRESS: The IP range for VPN clients. 10.8.0.x gives you addresses from 10.8.0.2 to 10.8.0.254.
  • WG_DEFAULT_DNS: DNS server for clients. Cloudflare’s 1.1.1.1 is fast and privacy-respecting. You can also use your Pi-hole if you set one up.
  • WG_ALLOWED_IPS: 0.0.0.0/0, ::/0 routes all traffic through the VPN (full tunnel). If you only want to access your home network, use your LAN subnet instead, like 192.168.1.0/24.
  • Port 51821/tcp: This is the web admin interface. We’ll access it to manage clients.
  • NET_ADMIN and SYS_MODULE: These capabilities let WireGuard create network interfaces inside the container.
  • sysctls: Enable IP forwarding so traffic can flow through the tunnel.

Important: Replace your-domain-or-ip.com with your actual server IP or domain, and your-secure-admin-password with a real password. Don’t skip this step — I’ve seen too many tutorials leave placeholders and people wonder why things don’t work.

Step 3: Open the Firewall

WireGuard uses UDP, not TCP. This trips people up. Make sure you’re opening the right protocol:

# If you're using UFW (Ubuntu)
sudo ufw allow 51820/udp
sudo ufw allow 51821/tcp

# If you're using iptables directly
sudo iptables -A INPUT -p udp --dport 51820 -j ACCEPT
sudo iptables -A INPUT -p tcp --dport 51821 -j ACCEPT

If you’re running on a cloud provider (DigitalOcean, AWS, Linode, etc.), you’ll also need to open these ports in the provider’s security group or firewall settings. The cloud firewall is a separate layer from your server’s firewall — both need to allow the traffic.

Step 4: Start the Container

docker compose up -d

Give it a few seconds to initialize. Check that it’s running:

docker compose ps
docker compose logs wg-easy

You should see a log line like WireGuard Interface created and the web UI starting up. If you see errors about kernel modules, you may need to install WireGuard on the host:

sudo apt update
sudo apt install wireguard

Most modern kernels (5.6+) have WireGuard built in, so this is usually not needed. But if the container complains, this fixes it.

Step 5: Access the Web UI and Create Your First Client

Open your browser and navigate to:

http://your-server-ip:51821

Log in with the password you set in the compose file. You’ll see a clean interface with a “New Client” button.

  1. Click New Client
  2. Give it a name (e.g., “My Phone” or “Laptop”)
  3. Click Create
  4. Click the QR code icon to scan with your phone, or download the .conf file

That’s it. You now have a working VPN client configuration.

Step 6: Connect Your Devices

On your phone: Install the WireGuard app (available on iOS and Android), scan the QR code, and toggle the connection on. You should see “Handshake completed” within a second or two.

On your laptop: Install WireGuard from wireguard.com/install, import the .conf file, and activate the tunnel.

To verify everything is working, visit ip.jerithai.com or ifconfig.me while connected. Your IP should show your server’s IP, not your local one.

Common Pitfalls and Troubleshooting

“No handshake” / Can’t connect

This is the most common issue. Check these in order:

  1. Is port 51820/UDP open on both the server firewall and the cloud provider’s firewall?
  2. Is WG_HOST set to the correct public IP or domain?
  3. Did you use udp and not tcp in your firewall rules?
  4. Check the container logs: docker compose logs wg-easy

Connected but no internet

If the handshake completes but you can’t browse:

  • Make sure IP forwarding is enabled: cat /proc/sys/net/ipv4/ip_forward should return 1
  • Check that WG_DEFAULT_DNS is set to a working DNS server
  • If you’re behind NAT, ensure your router forwards UDP 51820 to your server

Slow speeds

WireGuard is generally very fast. If speeds are poor:

  • Check your server’s bandwidth — a cheap VPS might be the bottleneck
  • Try a different WG_MTU value (add - WG_MTU=1420 to the environment section)
  • Make sure your server isn’t CPU-constrained (WireGuard is lightweight, but a $5/month VPS can still be overwhelmed)

Security Hardening Tips

A VPN is only as secure as its setup. Here are a few things to consider:

  • Use a strong admin password for the web UI. This is exposed to the internet.
  • Restrict the web UI port (51821) to only your IP if possible: change the port mapping to "127.0.0.1:51821:51821" and access it through an SSH tunnel or put it behind an authenticated reverse proxy.
  • Keep the container updated: docker compose pull && docker compose up -d
  • Use split tunneling if you don’t need all traffic routed through the VPN. Change WG_ALLOWED_IPS to your home LAN subnet (e.g., 192.168.1.0/24) so only traffic destined for your home network goes through the tunnel.
  • Limit client creation — only create configs for devices you actually own and control.

Wrapping Up

There’s something deeply satisfying about running your own VPN. You’re not trusting a third-party provider with your traffic. You’re not wondering about their logging policies. You own the server, you control the keys, and the whole thing fits in a single Docker Compose file.

WireGuard represents a philosophy that I think resonates with a lot of us in the self-hosting world: do one thing, do it well, and keep it simple. No bloat. No unnecessary complexity. Just a fast, secure tunnel to the things you care about.

Set it up once, scan a QR code, and you’re home — no matter where you are.

Happy tunneling.

Leave a Reply

Your email address will not be published. Required fields are marked *