Turn a GL.iNet GLKVM (or any always-on Tailscale node) into a wake gate for home servers. Visit a URL → server wakes up → you get redirected to it. No traffic is proxied — the GLKVM only handles the initial bounce.
Also provides valid TLS for the KVM web UI via Caddy + Cloudflare DNS challenge.
Client → on.{name}.example.com (or custom alias like watch.example.com)
→ Caddy (valid TLS, Cloudflare DNS challenge)
→ WoL gateway (Python, port 8888)
→ Server up + redirect set: 302 → server's own URL (direct connection)
→ Server up + no redirect: "server is online" page
→ Server down: WoL packet + waiting page (polls until up)
Port 443 is split by IP — Caddy binds to the Tailscale IP, nginx keeps the LAN + localhost for the KVM UI.
git clone <this-repo> ~/repos/glkvm-setup
cd ~/repos/glkvm-setup
# Create your server config from the template
cp servers.example.json servers.json
# Edit servers.json with your Tailscale IPs, MACs, domain, etc.
# Create .env with your Cloudflare API token and SSH target
echo "CLOUDFLARE_API_TOKEN=your-token-here" > .env
echo "GLKVM_SSH=root@glkvm" >> .envTo get a Cloudflare API token: Dashboard → My Profile → API Tokens → Create Token → Edit zone DNS template → scope to your domain zone.
Pull the existing KVM nginx config from your GLKVM:
scp root@<glkvm-ip>:/etc/kvmd/nginx-kvmd.conf .Edit nginx-kvmd.conf — change the 443 listener from 0.0.0.0 to your specific IPs:
# Change this:
listen 443 ssl;
listen [::]:443 ssl;
# To this (use your GLKVM's LAN IP):
listen 192.168.1.x:443 ssl;
listen 127.0.0.1:443 ssl;Also add server_name _; after the include /etc/kvmd/nginx/ssl.conf; line.
brew install go
go install github.com/caddyserver/xcaddy/cmd/xcaddy@latestssh root@<glkvm-ip> # password required this one time
tailscale set --ssh
exitIf you get a host key warning later: ssh-keygen -R <glkvm-ip>
# Build Caddy with Cloudflare DNS plugin (cross-compile for GLKVM armv7)
GOOS=linux GOARCH=arm GOARM=7 ~/go/bin/xcaddy build --with github.com/caddy-dns/cloudflare
# Deploy everything
./deploy.sh --fullThis will: generate the Caddyfile → create DNS records → upload all files → restart services → verify.
-
Edit
servers.json— add an entry:"my-server": { "name": "My Server", "host": "100.x.x.x", "port": 22, "mac": "aa:bb:cc:dd:ee:ff", "redirect": null }
host/port: Tailscale IP + port to check if the server is upmac: MAC address for Wake-on-LANredirect: URL to redirect to when online (ornullfor wake-only)alias(optional): custom subdomain, e.g."alias": "watch"→watch.example.com
-
Run
./deploy.sh
That's it. The deploy script auto-generates the Caddyfile, creates DNS records, uploads configs, and restarts services.
Each server gets on.{name}.{domain} automatically. With an alias, it also gets {alias}.{domain}.
# Deploy after any config change
./deploy.sh
# Deploy after GLKVM firmware update (rebuilds Caddy binary too)
./deploy.sh --full
# Just sync DNS records
python3 sync-dns.py
# Just verify all domains
python3 verify.py
# Wake a server from the GLKVM CLI
ssh root@glkvm wakeonlan media-server| File | Purpose |
|---|---|
servers.json |
Server definitions — the only file you edit to add servers (gitignored) |
servers.example.json |
Template with dummy values |
.env |
Cloudflare API token + SSH target (gitignored) |
deploy.sh |
One-command deploy: generate → DNS → upload → restart → verify |
generate-caddyfile.py |
Auto-generates Caddyfile from servers.json |
sync-dns.py |
Creates/updates Cloudflare A records from servers.json |
verify.py |
Checks all domains are reachable with valid TLS |
wol-gateway.py |
Multi-domain WoL gateway (reads servers.json at runtime) |
wakeonlan |
WoL CLI with hostname aliases (reads from servers.json) |
Caddyfile |
Auto-generated — don't edit manually (gitignored) |
nginx-kvmd.conf |
Modified KVM nginx config (gitignored) |
S98wol-gateway |
Init script for WoL gateway |
S99caddy |
Init script for Caddy |
| Local | Remote |
|---|---|
wol-gateway.py, servers.json |
/opt/jellyfin-gateway/ |
wakeonlan |
/usr/bin/wakeonlan |
Caddyfile, .env |
/etc/caddy/ |
nginx-kvmd.conf |
/etc/kvmd/nginx-kvmd.conf |
S98wol-gateway, S99caddy |
/etc/init.d/ |
caddy (built binary) |
/usr/bin/caddy |
- Cloudflare API token: local
.env(gitignored), deployed to/etc/caddy/.env(mode 600) - GLKVM_SSH: SSH target for deploy (e.g.
root@glkvm), in.env(gitignored). Defaults toroot@glkvmif unset S99caddysources/etc/caddy/.envat startup — no token in the init scriptservers.jsoncontains Tailscale IPs and MAC addresses (gitignored)
# Verify all domains
python3 verify.py
# Check port 443 split
ssh root@glkvm 'ss -tlnp | grep :443'
# Expected: caddy on Tailscale IP, nginx on LAN IP + 127.0.0.1
# Check services
ssh root@glkvm 'pgrep -a caddy; pgrep -a python3; pgrep -a nginx'
# WoL gateway logs
ssh root@glkvm 'cat /var/log/wol-gateway.log'
# Restart everything
ssh root@glkvm '/etc/init.d/S98wol-gateway restart && /etc/init.d/S99caddy restart && /etc/init.d/S99kvmd-nginx stop && /etc/init.d/S99kvmd-nginx start'- GL.iNet GLKVM (or any always-on Linux box with Tailscale on your LAN)
- Tailscale on all clients and servers
- Cloudflare DNS for your domain (DNS-only, grey cloud)
- Go + xcaddy on your Mac (for building Caddy with Cloudflare plugin)