Self-Hosted K8s Anywhere: Rancher + WireGuard Behind ISP NAT
Run distributed Kubernetes clusters across the globe — connecting remote nodes from AWS, home labs, and anywhere else — without needing a single open inbound port on your ISP connection.
This guide walks through connecting distributed Kubernetes nodes to a Rancher server running on a home network — even when your ISP blocks all inbound connections. The solution uses WireGuard VPN tunnels initiated outbound from every node, with a small VPS acting as the public-facing gateway.
How it works
The key insight is that WireGuard tunnels are initiated outbound — every node dials out to the VPS hub on UDP port 51820. Your ISP never needs to allow anything in. From there, three layers handle traffic:
- WireGuard overlay — gives every node a stable private IP (
10.100.x.x) regardless of physical location - Rancher agent — connects outbound over HTTPS/WSS to Rancher, also via the tunnel
- Caddy on the VPS — terminates public TLS and proxies inbound traffic into the tunnel toward your k8s ingress controller
Step 1 — Spin up the VPS gateway
Provision a small VPS
Hetzner CX11, Vultr $6 Cloud Compute, or DigitalOcean Droplet — 1 vCPU / 1GB RAM is enough. The VPS is purely a traffic relay, not a workload runner.
Install Docker and Docker Compose on it, then open these ports in the VPS firewall:
| Port | Protocol | Purpose |
|---|---|---|
80 | TCP | HTTP (redirects to HTTPS via Caddy) |
443 | TCP | HTTPS public traffic |
51820 | UDP | WireGuard — nodes connect here |
# On the VPS — install Docker curl -fsSL https://get.docker.com | sh apt install docker-compose-plugin -y # Open firewall ports ufw allow 80/tcp ufw allow 443/tcp ufw allow 51820/udp ufw enable
Step 2 — Deploy wg-easy + Caddy on the VPS
Create the Docker Compose stack
Both the WireGuard hub and the reverse proxy run as containers on the VPS. Caddy uses network_mode: host so it can reach WireGuard peer IPs directly.
# /opt/gateway/docker-compose.yml on the VPS services: wg-easy: image: ghcr.io/wg-easy/wg-easy restart: unless-stopped ports: - "51820:51820/udp" - "51821:51821/tcp" # web UI — keep off public internet volumes: - wg-data:/etc/wireguard environment: - WG_HOST=<VPS_PUBLIC_IP> - PASSWORD=<STRONG_PASSWORD> - WG_DEFAULT_ADDRESS=10.100.0.x - WG_MTU=1420 - WG_PERSISTENT_KEEPALIVE=25 cap_add: [NET_ADMIN, SYS_MODULE] sysctls: - net.ipv4.ip_forward=1 - net.ipv4.conf.all.src_valid_mark=1 caddy: image: caddy:alpine restart: unless-stopped network_mode: host volumes: - ./Caddyfile:/etc/caddy/Caddyfile - caddy-data:/data volumes: wg-data: caddy-data:
# /opt/gateway/Caddyfile # Wildcard cert via Cloudflare DNS challenge { acme_dns cloudflare {env.CF_API_TOKEN} } # All subdomains → k8s ingress controller (WireGuard IP) *.yourdomain.com { reverse_proxy 10.100.1.11:30080 } # Rancher UI rancher.yourdomain.com { reverse_proxy 10.100.0.2:443 { transport http { tls_insecure_skip_verify } } }
# Start the stack
cd /opt/gateway
CF_API_TOKEN=<your_token> docker compose up -d
Step 3 — Connect your Rancher server as a WireGuard peer
Add Rancher server as a peer in wg-easy
Open the wg-easy UI at http://<VPS_IP>:51821. Click + New Client, name it rancher-home. Download the .conf file and copy it to your home server.
# On your home Rancher server apt install wireguard -y # Paste the downloaded config cp rancher-home.conf /etc/wireguard/wg0.conf wg-quick up wg0 systemctl enable wg-quick@wg0 # Verify tunnel to VPS ping 10.100.0.1
Step 4 — Add cluster nodes as WireGuard peers
One peer per node via wg-easy UI
For each node you want in the cluster: open wg-easy, click + New Client, name it descriptively (cluster1-node-eu), download the config, and drop it on the remote machine. wg-easy auto-assigns the next IP.
MTU = 1420 in the [Interface] section of every node's WireGuard config. Without this, double-encapsulation (WireGuard + CNI VXLAN) causes silent packet fragmentation and intermittent pod networking failures.
# On each remote node — install WireGuard and bring up the tunnel apt install wireguard -y cp cluster1-node-eu.conf /etc/wireguard/wg0.conf # Verify MTU is set correctly in the config before starting grep MTU /etc/wireguard/wg0.conf # should show MTU = 1420 wg-quick up wg0 systemctl enable wg-quick@wg0 # Verify inter-node connectivity via tunnel ping 10.100.0.1 # VPS hub ping 10.100.0.2 # Rancher server
Step 5 — Register nodes with Rancher
Force nodes to advertise their WireGuard IP
This is the most important step. By default RKE2 auto-detects the physical NIC IP — which other nodes can't reach across the internet. You must override it to the WireGuard IP before running the registration command.
# On EACH node — write RKE2 config BEFORE registering mkdir -p /etc/rancher/rke2 cat > /etc/rancher/rke2/config.yaml << EOF # Replace with THIS node's WireGuard IP node-ip: "10.100.1.11" node-external-ip: "10.100.1.11" EOF
# Now run the Rancher registration command # Copy from Rancher UI → Cluster Management → Create → Custom # but add --node-ip and --node-external-ip flags: curl -fL https://rancher.yourdomain.com/system-agent-install.sh | \ sudo sh -s - \ --server https://rancher.yourdomain.com \ --token <CLUSTER_TOKEN> \ --node-ip 10.100.1.11 \ --node-external-ip 10.100.1.11 \ --worker
--node-ip tells kubelet which IP to bind to. --node-external-ip prevents RKE2 from falling back to the physical NIC IP for inter-node communication. Without both, nodes register successfully but stay NotReady because they can't reach each other.
Step 6 — Deploy ingress controller inside the cluster
Install nginx-ingress via Helm
The ingress controller receives traffic forwarded from Caddy on the VPS and routes it to the right service inside the cluster.
# Add the ingress-nginx Helm chart helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx helm repo update # Install — NodePort so Caddy can reach it on a known port helm install ingress-nginx ingress-nginx/ingress-nginx \ --namespace ingress-nginx \ --create-namespace \ --set controller.service.type=NodePort \ --set controller.service.nodePorts.http=30080 \ --set controller.service.nodePorts.https=30443
# Example Ingress for your app apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: myapp annotations: kubernetes.io/ingress.class: "nginx" spec: rules: - host: app.yourdomain.com http: paths: - path: / pathType: Prefix backend: service: name: myapp-svc port: number: 80
Traffic flow end to end
Pre-flight checklist
- VPS provisioned with Docker, ports 80/443/51820 open
- wg-easy + Caddy running on VPS via Docker Compose
- Rancher server connected to VPS as a WireGuard peer
- Each node has WireGuard up and can ping
10.100.0.1 - MTU = 1420 set in every node's
wg0.conf - RKE2 config written with
node-ip= WireGuard IP before registration - Rancher registration command includes
--node-ipand--node-external-ip - nginx-ingress installed with NodePort 30080
- Caddyfile pointing
*.yourdomain.comto ingress node WireGuard IP - DNS A record pointing to VPS public IP
What you end up with
| Component | Where it runs | Needs public IP? |
|---|---|---|
| wg-easy hub | VPS | Yes — all peers connect here |
| Caddy reverse proxy | VPS | Yes — public traffic entry |
| Rancher server | Home network | No — outbound WireGuard only |
| K8s cluster nodes | Anywhere | No — outbound WireGuard only |
| Your applications | K8s pods | No — served via VPS proxy |
The VPS is the only machine that needs a public IP and open inbound ports. Adding a new node to any cluster takes under two minutes: create a peer in wg-easy, drop the config on the machine, and run the Rancher registration command with the WireGuard IP flags. The cluster grows without touching your home router or ISP at all.