How to Build a 100% Self-Hosted Cloud-to-LAN Tunnel and Egress VPN Using Headscale, Traefik, and WireGuard

How to Build a 100% Self-Hosted Cloud-to-LAN Tunnel and Egress VPN Using Headscale, Traefik, and WireGuard
A complete guide to exposing local homelab services to the internet with ZERO open router ports, while using an Oracle Cloud VPS as a secure egress VPN gateway.
Introduction
If you run a homelab or self-host applications on your local area network (LAN), you eventually face two major challenges:
- Ingress (Cloud LAN): Exposing your local applications (Jellyfin, Nextcloud, custom web apps) to the public internet securely, with valid HTTPS certificates, without opening ports on your home router or revealing your home ISP’s public IP address.
- Egress (LAN Cloud Internet): Routing outbound internet traffic from your phone, laptop, or home network through a trusted remote server (like a cloud VPS) so your connection is encrypted and masks your physical location.
While commercial SaaS products like Tailscale or Cloudflare Tunnels can solve parts of this, they rely on third-party control planes.
In this guide, we will build a 100% self-hosted, sovereign infrastructure on an Oracle Cloud Infrastructure (OCI) VPS running Headscale (the open-source, self-hosted Tailscale control server), Traefik v3 (the reverse proxy), and WireGuard.
The Complete Architecture Overview
[ Public Internet Users ]
│
▼ (HTTPS / Port 443)
┌─────────────────────────────────────────────────────────────────┐
│ OCI VPS (Ubuntu) │
│ │
│ ┌─────────────────────────────┐ ┌─────────────────────────┐ │
│ │ Traefik v3 │ │ Headscale Container │ │
│ │ (Auto Let's Encrypt Certs) │ │ (Control Plane) │ │
│ └──────────────┬──────────────┘ └────────────▲────────────┘ │
│ │ │ │
└─────────────────┼───────────────────────────────┼───────────────┘
│ Encrypted WireGuard Mesh │
│ (CGNAT: 100.64.0.0/10) │
┌─────────────────┼───────────────────────────────┼───────────────┐
│ ▼ │ │
│ ┌─────────────────────────────┐ │ │
│ │ Local Home Server │ │ │
│ │ (MagicDNS: "homeserver") │ │ │
│ └─────────────────────────────┘ │ │
│ │ │
│ ┌──────────────────────────────────────────────┴────────────┐ │
│ │ Local LAN Clients (Phone / Laptop / PC) │ │
│ │ Toggle "Exit Node" -> Internet exits out of OCI VPS IP │ │
│ └───────────────────────────────────────────────────────────┘ │
│ HOME LOCAL LAN │
└─────────────────────────────────────────────────────────────────┘
What This Setup Gives You:
- Zero Open Home Ports: Your home router blocks all incoming traffic. The connection to your VPS is an outbound, persistent WireGuard tunnel.
- Auto-Renewing Let's Encrypt SSL Certificates: Traefik on the VPS handles TLS termination seamlessly for your custom domain (
app.yourdomain.com). - Immunity to Home IP Changes: MagicDNS handles dynamic host mapping. Even if your home ISP changes your public IP daily, the connection never breaks.
- 1-Click Egress VPN: Route cellular or public Wi-Fi traffic out through your OCI VPS data center IP at any time.
Prerequisites
- A Cloud VPS: An OCI VPS (or any Linux VPS with 1GB+ RAM, Ubuntu 22.04/24.04 recommended).
- A Custom Domain Name: Point an
A Recordfor your domain and wildcard (e.g.,*.yourdomain.comandheadscale.yourdomain.com) to your VPS’s public IP address. - DNS Proxy Settings: If using Cloudflare, ensure the proxy toggle for your domain is set to DNS Only (Gray Cloud) during initial Let's Encrypt validation.
Phase 1: Preparing the VPS Environment
1. Enable OS Kernel IP Forwarding
To allow the VPS to act as a VPN router for outbound internet traffic, enable packet forwarding:
echo "net.ipv4.ip_forward=1" | sudo tee -a /etc/sysctl.conf
echo "net.ipv6.conf.all.forwarding=1" | sudo tee -a /etc/sysctl.conf
sudo sysctl -p
2. Configure Local OS Firewalls
OCI instances often ship with strict default iptables rules that block incoming traffic. Ensure Ports 80 (HTTP), 443 (HTTPS), and 51820 (WireGuard UDP) are open:
sudo iptables -I INPUT -p tcp --dport 80 -j ACCEPT
sudo iptables -I INPUT -p tcp --dport 443 -j ACCEPT
sudo iptables -I INPUT -p udp --dport 51820 -j ACCEPT
sudo netfilter-persistent save
(Also verify that these ports are opened in your Oracle Cloud VCN Security List).
Phase 2: Deploying Headscale behind Traefik
We will run Headscale inside Docker, using Traefik as the reverse proxy to manage automatic Let's Encrypt certificates over port 443.
1. Project Directory Structure
On your VPS, create a directory for the stack:
mkdir -p ~/headscale/config ~/headscale/lib
cd ~/headscale
2. Create the Environment File (.env)
Create ~/headscale/.env:
DOMAIN=headscale.yourdomain.com
TRAEFIK_NETWORK=traefik-public
CERT_RESOLVER=letsencrypt
3. Create the Headscale Configuration (config/config.yaml)
Create ~/headscale/config/config.yaml.
Important Note: Modern Headscale (v0.23+) requires explicit
noisekey paths, a nesteddatabaseblock, and the updatedprefixesschema.
server_url: https://headscale.yourdomain.com:443
listen_addr: 0.0.0.0:8080
# Persistent Private Keys
private_key_path: /var/lib/headscale/private_key
noise:
private_key_path: /var/lib/headscale/noise_private_key
metrics_listen_addr: 127.0.0.1:9090
grpc_listen_addr: 0.0.0.0:50443
grpc_allow_insecure: false
# SQLite Database Storage
database:
type: sqlite
sqlite:
path: /var/lib/headscale/db.sqlite
# Socket configuration inside tmpfs
unix_socket: /var/run/headscale/headscale.sock
unix_socket_permission: "0770"
# Tailscale CGNAT Subnets
prefixes:
v4: 100.64.0.0/10
v6: fd7a:115c:a1e0::/48
# Official Tailscale DERP Relay Map
derp:
server:
enabled: false
urls:
- https://controlplane.tailscale.com/derpmap/default
paths: []
auto_update_enabled: true
update_frequency: 24h
disable_check_updates: false
ephemeral_node_inactivity_timeout: 30m
node_state_clean_interval: 10m
dns:
magic_dns: true
base_domain: example.com
nameservers:
global:
- 1.1.1.1
4. Create docker-compose.yml
Create ~/headscale/docker-compose.yml:
version: "3.8"
services:
headscale:
image: docker.io/headscale/headscale:0.23.0
container_name: headscale
restart: unless-stopped
read_only: true
tmpfs:
- /var/run/headscale
ports:
- "127.0.0.1:8080:8080"
- "127.0.0.1:9090:9090"
volumes:
- ./config:/etc/headscale:ro
- ./lib:/var/lib/headscale
command: serve
healthcheck:
test: ["CMD", "headscale", "health"]
interval: 30s
timeout: 5s
retries: 3
start_period: 10s
networks:
- traefik-public
labels:
- "traefik.enable=true"
- "traefik.docker.network=traefik-public"
- "traefik.http.routers.headscale.rule=Host(`${DOMAIN}`)"
- "traefik.http.routers.headscale.entrypoints=web,websecure"
- "traefik.http.routers.headscale.tls=true"
- "traefik.http.routers.headscale.tls.certresolver=${CERT_RESOLVER}"
- "traefik.http.services.headscale.loadbalancer.server.port=8080"
networks:
traefik-public:
name: ${TRAEFIK_NETWORK}
external: true
5. Launch the Stack
Start Headscale:
docker compose up -d
Verify that Traefik successfully picks up the router and obtains the Let's Encrypt SSL certificate:
curl -v https://headscale.yourdomain.com/health
(You should see an HTTP 200 OK response returning {"status":"pass"}).
Phase 3: Configuring the VPS as an Egress Exit Node
Now we connect the host OS of the VPS to Headscale so it can serve as a full-tunnel VPN exit node.
1. Create a Headscale User
On the VPS host, create a user namespace inside Headscale:
docker exec headscale headscale users create myuser
2. Connect the VPS Tailscale Client
Install the official Tailscale binary on the VPS host:
curl -fsSL https://tailscale.com/install.sh | sh
Authenticate the VPS host against your self-hosted Headscale control server, advertising it as an exit node:
sudo tailscale up --login-server=https://headscale.yourdomain.com --advertise-exit-node --reset
3. Register and Approve the Exit Node
Copy the mkey:xxxx... registration key generated by the terminal and approve it in Headscale:
# 1. Register the node under 'myuser'
docker exec headscale headscale nodes register --user myuser --key mkey:xxxx...
# 2. Check the advertised routes
docker exec headscale headscale nodes list-routes
# 3. Approve the 0.0.0.0/0 exit node routes
docker exec headscale headscale nodes approve-routes --identifier <VPS_NODE_ID> --routes 0.0.0.0/0,::/0
Phase 4: Connecting Your Local LAN Host & MagicDNS
Now, let's link your local home server/host to the Headscale control plane.
1. Authenticate the Local LAN Server
On your local home machine, install Tailscale and connect:
curl -fsSL https://tailscale.com/install.sh | sh
sudo tailscale up --login-server=https://headscale.yourdomain.com
2. Register and Rename the LAN Node
On your OCI VPS, register the local machine's key:
docker exec headscale headscale nodes register --user myuser --key mkey:xxxx...
Give the LAN node a clean, static MagicDNS name (e.g., homeserver):
# List nodes to find the ID
docker exec headscale headscale nodes list
# Rename node ID 2 to 'homeserver'
docker exec headscale headscale nodes rename -i 2 homeserver
Test that your VPS can dynamically ping your local home machine through the encrypted tunnel:
ping homeserver
Phase 5: Exposing Local LAN Apps via Traefik Reverse Proxy
Now that the VPS can resolve homeserver dynamically over the encrypted mesh, you can expose local home services (running on your home server's port 8080, 8096, etc.) to the public internet with automatic SSL!
Add a Dynamic Configuration File to Traefik on your VPS (e.g., /etc/dokploy/traefik/dynamic/homelab.yml):
http:
routers:
my-home-app:
rule: "Host(`app.yourdomain.com`)"
service: my-home-app-service
entryPoints:
- "websecure"
tls:
certResolver: "letsencrypt"
services:
my-home-app-service:
loadBalancer:
servers:
- url: "http://homeserver:8080" # Points to MagicDNS hostname!
How Request Flow Works:
- Public User visits
https://app.yourdomain.com. - Traffic hits OCI VPS Traefik on Port 443 (Let's Encrypt SSL terminated).
- Traefik forwards the request down the WireGuard tunnel to
http://homeserver:8080. - Your Home Server responds over the tunnel back to the VPS, which responds to the user.
- Result: Zero open ports on your home router!
Phase 6: Troubleshooting & Lessons Learned
Throughout this deployment, several technical traps can occur. Here is how to avoid them:
1. The "Traefik Default Cert" (CN=TRAEFIK DEFAULT CERT) Trap
- Symptom:
curl -v https://headscale.yourdomain.comshows Traefik serving its fallback self-signed certificate instead of Let's Encrypt. - Causes & Solutions:
- Entrypoint Mismatch: If Traefik handles ACME via HTTP-01 challenge on port 80 (
web), your Headscale router labels must include both entrypoints:traefik.http.routers.headscale.entrypoints=web,websecure. If restricted only towebsecure, the HTTP-01 solver fails. - Docker Network Mismatch: If Traefik runs with
--providers.docker.network=dokploy-network, it ignores containers on other networks unless explicitly overridden with the label- "traefik.docker.network=traefik-public". - Cloudflare Proxying: If using Cloudflare DNS, the A record must be set to Gray Cloud (DNS Only) during ACME validation.
- Entrypoint Mismatch: If Traefik handles ACME via HTTP-01 challenge on port 80 (
2. NAT Hairpinning / Local Loopback Connection Hangs
- Symptom: Running
tailscale up --login-server https://headscale.yourdomain.comdirectly on the VPS host hangs indefinitely. - Cause: Cloud VPS network interface trying to connect to its own public IP through external NAT.
- Solution: Add a loopback override to the VPS
/etc/hosts:echo "127.0.0.1 headscale.yourdomain.com" | sudo tee -a /etc/hosts
3. Docker Command Override Errors
- Symptom: Logs show
Error: unknown command "headscale" for "headscale". - Cause: The official Docker image uses
ENTRYPOINT ["headscale"]. Specifyingcommand: headscale servecauses Docker to executeheadscale headscale serve. - Solution: Use
command: serveindocker-compose.yml.
Conclusion
You now own a fully sovereign, highly secure hybrid-cloud infrastructure:
- Ingress: Public web traffic seamlessly proxies through your cloud VPS straight to your home server without exposing your home IP or opening home router ports.
- Egress: Your mobile devices and laptops can enable "Exit Node" mode at any time, routing all public internet traffic out through your encrypted OCI VPS connection.
- Resilience: Because Traefik points to Headscale's MagicDNS hostnames (
homeserver), your homelab connection is 100% immune to home ISP IP changes or router reboots.
Enjoyed this article?
Discussion
No comments yet. Start the conversation!