A complete, production-ready Docker Compose stack for self-hosting a WireGuard-based mesh VPN.
Securely connect your servers, virtual machines, and personal devices into a private network. It leverages Headscale for WireGuard networking, Headplane for a modern web UI, and Caddy for automatic HTTPS, alongside a custom Python update notifier.
- Headscale (v0.29.2): Open-source, self-hosted Tailscale control server for establishing WireGuard tunnels.
- Headplane: Beautiful Web UI for managing nodes, users, routing, and pre-auth keys.
- Caddy: Reverse proxy for automatic HTTPS, secure routing, and SSL/TLS certificate renewals via Let's Encrypt.
- Update Notifier: Python daemon that monitors juanfont/headscale GitHub releases and sends Discord alerts for updates and breaking changes.
This guide specifically covers the network configuration for Oracle Cloud (Ubuntu), but the steps will be similar for other cloud providers (AWS, GCP, Azure, DigitalOcean).
- A registered domain name (e.g.,
headscale.yourdomain.com). - An A-Record in your DNS provider pointing to your cloud VM's public IP address.
- Cloudflare Users: You MUST set the record to "DNS Only". Proxying the record will break WireGuard's direct peer-to-peer UDP connections.
You must open the following ports to allow traffic. For Oracle Cloud: You must allow these ports in both the Oracle Cloud Dashboard (VCN Security Lists) and the local Ubuntu OS firewall.
- TCP 80: Caddy reverse proxy (HTTP -> HTTPS redirection and Let's Encrypt SSL challenges).
- TCP 443: Caddy reverse proxy (Headscale API, Headplane UI, and DERP fallback).
- UDP 3478: Headscale built-in STUN/DERP server (Crucial for NAT traversal).
Ubuntu iptables rules (Oracle Cloud default is "two-layer" security, which makes these rules REQUIRED):
sudo iptables -I INPUT 6 -m state --state NEW -p tcp --dport 80 -j ACCEPT
sudo iptables -I INPUT 6 -m state --state NEW -p tcp --dport 443 -j ACCEPT
sudo iptables -I INPUT 6 -m state --state NEW -p udp --dport 3478 -j ACCEPT
sudo netfilter-persistent saveFollow these steps to deploy the stack on your own server.
- Clone the repository:
git clone https://github.com/hanafytech/headscale-stack.git
cd headscale-stack- Create the necessary directories and download the base Headscale config:
mkdir -p config data headplane-data headplane-config caddy_data caddy_config notifier-data
wget -O config/config.yaml https://raw.githubusercontent.com/juanfont/headscale/main/config-example.yaml Edit config/config.yaml and set server_url: https://headscale.yourdomain.com.
Change the listen address to listen_addr: 0.0.0.0:8080 (otherwise Caddy cannot route traffic to the container)!
- Configure the Built-in DERP Server (Firewall Bypass)
In
config/config.yaml, scroll to thederp:section. You must make three critical changes to bypass restrictive firewalls and prevent connection failures:
- Set
enabled: trueto turn on the embedded server. - Comment out the
ipv4andipv6fields. (Important: You must comment out or remove the default198.51.100.1and2001:db8::1dummy IPs, or your clients will attempt to route traffic to fake addresses and fail). - Empty the official relay list by setting
urls: []and commenting out the default Tailscale URL.
Modify the block to match this:
derp:
server:
enabled: true
region_id: 999
region_code: "headscale"
region_name: "Headscale Embedded DERP"
verify_clients: true
stun_listen_addr: "0.0.0.0:3478"
private_key_path: /var/lib/headscale/derp_server_private.key
automatically_add_embedded_derp_region: true
# ipv4: 198.51.100.1
# ipv6: 2001:db8::1
urls: []
# - [https://controlplane.tailscale.com/derpmap/default](https://controlplane.tailscale.com/derpmap/default)
paths: []
auto_update_enabled: true
update_frequency: 3h- Create
headplane-config/config.yamland populate it with your settings: (Note: Thepublic_urlvariable is crucial! It ensures the "Register Machine Key" UI displays your actual domain instead of internal Docker IPs).
server:
host: "0.0.0.0"
port: 3000
cookie_secret: "GENERATE_A_RANDOM_32_CHARACTER_SECURE_STRING_HERE"
headscale:
url: "http://headscale:8080"
public_url: "https://headscale.yourdomain.com"
config_path: "/etc/headscale/config.yaml"-
Update External Variables:
- Caddyfile: Open the
Caddyfileand ensure your domain is correct. - Discord Notifier (Optional): Open
docker-compose.yamland replaceyour_webhook_url_hereunder thenotifierservice with your actual Discord Webhook URL.
- Caddyfile: Open the
-
Deploy the stack:
docker compose pull
docker compose up -d- To log into Headplane for the first time, generate an API key directly from the Headscale container:
docker exec headscale headscale apikeys create --expiration 90d- Navigate to your deployed domain (e.g.,
https://headscale.yourdomain.com/admin). - Enter the generated API key into the login screen to authenticate.
- Create your first user. You can do this in the Headplane UI under the "Users" tab, or via the command line:
docker exec headscale headscale users create myuser-
Install the Tailscale client on the device you want to connect.
- For Linux:
curl -fsSL https://tailscale.com/install.sh | sh - For Windows/macOS/Mobile: Download the app from the Tailscale website.
- For Linux:
-
Once installed, connect the device to your custom VPN by running the following in your client's terminal:
tailscale up --login-server https://headscale.yourdomain.com- The terminal will output a machine key/registration link. Copy it, go to the Headplane UI, click Add Device -> Register Machine Key, paste the key, and select your user to authenticate the device!
If you plan on registering multiple devices, you can generate a reusable pre-authentication key on your Headscale server instead of completing the browser-based login for each device.
Generate a reusable pre-authentication key:
docker exec headscale headscale preauthkeys create --user 1 --reusable --expiration 24hNote: Replace
1with the ID of the Headscale user you want to register the device under.
The command will return an auth key similar to:
hskey-auth-1234567890
You can then register devices directly by running:
tailscale up --login-server https://headscale.yourdomain.com --authkey hskey-auth-1234567890- VPN Control Server: Headscale (Go)
- Web Interface: Headplane (Node.js/SvelteKit)
- Reverse Proxy: Caddy (Go)
- Scripting: Python 3.11
- Containerization: Docker