Self-Hosted K8s Anywhere: Rancher + WireGuard Behind ISP NAT

Self-Hosted K8s Anywhere: Rancher + WireGuard Behind ISP NAT
Kubernetes WireGuard Rancher 12 min read

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.

Overall architecture overview Traffic flows from public users through a VPS ingress gateway over WireGuard to distributed cluster nodes, all managed by Rancher on the home network. Public user app.domain.com HTTPS VPS Gateway ~$5/mo · stable public IP Caddy reverse proxy Auto TLS · Let's Encrypt wg-easy hub 10.100.0.1 · UDP 51820 WireGuard UDP outbound Home Network ISP blocks inbound ✗ Rancher Server cluster management K8s Nodes anywhere in the world Node · 10.100.1.11 Node · 10.100.1.12 Node · 10.100.1.13 WireGuard tunnel (outbound only) Public HTTPS traffic
Complete architecture: one VPS is the only machine needing a public IP. Everything else connects outbound.

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.

What you need before starting A home server (Rancher), one or more remote Linux VMs, a cheap VPS ($5–6/mo from Hetzner/Vultr/DigitalOcean), and a domain name. The VPS is the only machine that needs open inbound ports.

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:

  1. WireGuard overlay — gives every node a stable private IP (10.100.x.x) regardless of physical location
  2. Rancher agent — connects outbound over HTTPS/WSS to Rancher, also via the tunnel
  3. 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

1

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:

PortProtocolPurpose
80TCPHTTP (redirects to HTTPS via Caddy)
443TCPHTTPS public traffic
51820UDPWireGuard — 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
wg-easy hub and spoke diagram wg-easy running on the VPS acts as a hub. All nodes — home Rancher server, AWS nodes, and remote nodes — connect as spokes with outbound WireGuard tunnels. wg-easy (VPS) 10.100.0.1 · UDP 51820 Rancher (home) 10.100.0.2 AWS Node 10.100.1.11 EU Node 10.100.1.12 APAC Node 10.100.1.13 Any Node 10.100.1.14 outbound UDP
wg-easy runs on the VPS as the hub. Every node — including Rancher at home — dials out to it. No inbound ports needed anywhere else.

Step 2 — Deploy wg-easy + Caddy on the VPS

2

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

3

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

4

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 is critical Set 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
Multiple clusters on separate subnets Three separate wg-easy instances run on the VPS on different UDP ports, each with its own subnet, giving hard network isolation between test clusters. VPS wg-easy :51820 cluster-1 · 10.101.0.0/24 wg-easy :51822 cluster-2 · 10.102.0.0/24 wg-easy :51824 cluster-3 · 10.103.0.0/24 Cluster 1 nodes 10.101.0.x Cluster 2 nodes 10.102.0.x Cluster 3 nodes 10.103.0.x Isolation Separate subnets Separate key spaces Separate wg-easy UI Nodes in cluster-1 cannot reach cluster-2 nodes
Run one wg-easy container per cluster on different UDP ports. Each gets its own subnet and web UI — full network isolation with zero iptables complexity.

Step 5 — Register nodes with Rancher

5

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
Why both flags? --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

6

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

End to end traffic flow Request flows from browser through DNS to Cloudflare, to VPS Caddy, over WireGuard tunnel to k8s ingress controller, then to the pod serving the application. Browser app.domain.com DNS Cloudflare optional CDN HTTPS VPS · Caddy TLS termination WireGuard k8s Ingress nginx · :30080 ClusterIP Your Pod :8080 TLS terminates at Caddy — internal traffic is over encrypted WireGuard tunnel
Complete request path. The WireGuard tunnel segment is encrypted at the network layer — your pods receive plain HTTP from the ingress controller.

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-ip and --node-external-ip
  • nginx-ingress installed with NodePort 30080
  • Caddyfile pointing *.yourdomain.com to ingress node WireGuard IP
  • DNS A record pointing to VPS public IP

What you end up with

ComponentWhere it runsNeeds public IP?
wg-easy hubVPSYes — all peers connect here
Caddy reverse proxyVPSYes — public traffic entry
Rancher serverHome networkNo — outbound WireGuard only
K8s cluster nodesAnywhereNo — outbound WireGuard only
Your applicationsK8s podsNo — 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.