Homelab Gateway Skill
The homelab-gateway skill automates the setup, configuration, and maintenance of a secure, production-grade reverse proxy and remote access gateway for Linux home servers (Ubuntu/Debian) connecting to client devices (macOS, iOS, Windows, Android).
1. When to Activate This Skill
Activate this skill when:
- The user wants to expose local home server services (Portainer, Cockpit, Jellyfin, Home Assistant, Nextcloud, Plex, Ollama) over secure HTTPS without security warnings.
- Setting up Caddy as a reverse proxy via Docker Compose.
- Interconnecting home servers and laptops/Macs using Tailscale and MagicDNS with official Let's Encrypt TLS certificates.
- Resolving home network issues like shifting DHCP IP addresses, Chrome Secure DNS (
ERR_ADDRESS_UNREACHABLE), or browser certificate authority trust. - Eliminating port numbers (
:8443,:8080,:9000) in favor of clean custom domain names or subpaths.
2. Interactive Semi-Automated Workflow
When this skill is activated, follow this 7-phase execution pipeline:
flowchart TD
Discovery["Phase 1: Discovery (detect_services.sh)"] --> Network["Phase 2: Network Hardening (Static IP)"]
Network --> Caddy["Phase 3: Deploy Caddy (Docker Compose)"]
Caddy --> Tailscale["Phase 4: Tailscale Mesh & Public TLS"]
Tailscale --> Routing["Phase 5: Port-Free URL Routing"]
Routing --> Client["Phase 6: Client Device Integration (Mac)"]
Client --> Verification["Phase 7: End-to-End Verification"]
Phase 1: Environment & Service Discovery
- Execute the packaged discovery script:
bash ~/.agents/skills/homelab-gateway/scripts/detect_services.sh - Inspect the output to determine:
- Primary Network Interface: (e.g.
eth0orwlo1). - Current LAN IP: (e.g.
192.168.1.100). - Default Gateway: (e.g.
192.168.1.1). - NetworkManager Profile: (e.g.
Home-WiFiorWired-Connection). - Tailscale Status: Whether installed, logged in, and current tailnet suffix.
- Active Docker Containers: Existing services (Portainer, etc.) and listening ports.
- Primary Network Interface: (e.g.
- Present the discovered environment to the user and confirm the proposed plan:
- Server LAN IP to lock.
- Services to expose (e.g. Portainer, Cockpit, Hub).
- Domain naming scheme.
Phase 2: Host Network Hardening (Static IP)
[!IMPORTANT] Home routers frequently shift IP addresses over DHCP (e.g. lease expiry or reboot). This immediately breaks
/etc/hostsand DNS records. Always lock the server to a static IP in NetworkManager.
- Lock the IP permanently on the active connection:
sudo nmcli con mod "<CONNECTION_NAME>" \ ipv4.addresses <CURRENT_IP>/24 \ ipv4.gateway <DEFAULT_GATEWAY> \ ipv4.dns "<DEFAULT_GATEWAY>,1.1.1.1" \ ipv4.method manual - Verify:
nmcli -f ipv4.method,ipv4.addresses,ipv4.gateway,ipv4.dns con show "<CONNECTION_NAME>" ping -4 -c 2 1.1.1.1
Phase 3: Caddy Reverse Proxy Deployment
- Create directory layout:
mkdir -p ~/caddy/site ~/caddy/data ~/caddy/config - Deploy
docker-compose.ymlfrom~/.agents/skills/homelab-gateway/templates/docker-compose.yml:- Uses
network_mode: hostto eliminate NAT overhead and directly reach127.0.0.1:<PORT>services. - Mounts persistent
./dataand./configto protect against ACME rate limits. - Mounts
/var/run/tailscale/tailscaled.sockfor native Tailscale certificate fetching.
- Uses
- Copy and populate
templates/index.htmlto~/caddy/site/index.html. - Launch Caddy:
cd ~/caddy && docker compose up -d
Phase 4: Tailscale Mesh & Public Let's Encrypt TLS
- If Tailscale is not installed:
Provide the user with the authentication link (curl -fsSL https://tailscale.com/install.sh | sudo sh sudo tailscale up --operator=$USERhttps://login.tailscale.com/a/...). - Rename machine to a clean, memorable hostname (e.g.
home):tailscale set --hostname=home - Prompt user to enable HTTPS certificates in Tailscale admin panel:
- URL:
https://login.tailscale.com/admin/dns→ Click "Enable HTTPS Certificates".
- URL:
- Verify certificate generation:
tailscale cert <HOSTNAME>.<TAILNET_SUFFIX>.ts.net
Phase 5: Port-Free URL Routing (Standard HTTPS Port 443)
Configure ~/caddy/Caddyfile using templates/Caddyfile as the baseline:
- Local Access Block (
.home/.local): Usestls internalto generate local self-signed certificates with root CA download at/root.crt. - Tailscale Access Block (
*.ts.net): Usestls { get_certificate tailscale }to request official Let's Encrypt certificates from Tailscale. - Subpath Routing Rules:
- Portainer:
redir /portainer /portainer/ handle_path /portainer/* { reverse_proxy 127.0.0.1:9000 } - Cockpit:
In
/etc/cockpit/cockpit.conf:
In Caddyfile:[WebService] Origins = https://<TAILSCALE_DOMAIN> wss://<TAILSCALE_DOMAIN> ProtocolHeader = X-Forwarded-Proto UrlRoot = /systemredir /cockpit /system/ redir /cockpit/ /system/ handle /system* { reverse_proxy 127.0.0.1:9090 }
- Portainer:
- Reload Caddy:
cd ~/caddy && docker compose exec caddy caddy reload --config /etc/caddy/Caddyfile
Phase 6: Client Device Integration (macOS)
Provide the user with the exact client commands for their Mac:
Method A: Tailscale (Recommended — Zero Config)
- Install Tailscale on Mac:
brew install --cask tailscale. - Log into the same Tailscale account.
- Open
https://<HOSTNAME>.<TAILNET_SUFFIX>.ts.net/directly in Chrome or Safari.- Works everywhere (home Wi-Fi, cellular data, office).
- 100% valid Let's Encrypt green padlock.
- Zero
/etc/hostschanges. - Chrome "Use secure DNS" can remain ON.
Method B: Local LAN Fallback (.home)
If connecting strictly over home Wi-Fi without Tailscale:
- Map hostnames in Mac
/etc/hosts:echo "<LAN_IP> portainer.home cockpit.home server.home" | sudo tee -a /etc/hosts sudo dscacheutil -flushcache && sudo killall -HUP mDNSResponder - Trust Caddy's Root CA in macOS System Keychain:
curl -k https://<LAN_IP>/root.crt -o /tmp/caddy-root.crt && sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain /tmp/caddy-root.crt - If Google Chrome shows
ERR_ADDRESS_UNREACHABLE:- Chrome's internal Secure DNS (DoH) bypasses
/etc/hosts. Toggle "Use secure DNS" OFF atchrome://settings/security, or use Safari.
- Chrome's internal Secure DNS (DoH) bypasses
Phase 7: End-to-End Verification
Verify every endpoint using curl:
# Verify Hub Landing Page
curl -k -I --resolve <DOMAIN>:443:<TAILSCALE_IP> https://<DOMAIN>/
# Verify Portainer Subpath
curl -k -I --resolve <DOMAIN>:443:<TAILSCALE_IP> https://<DOMAIN>/portainer/
# Verify Cockpit Subpath
curl -k -I --resolve <DOMAIN>:443:<TAILSCALE_IP> https://<DOMAIN>/cockpit
# Verify TLS Certificate Issuer
echo | openssl s_client -connect <TAILSCALE_IP>:443 -servername <DOMAIN> 2>/dev/null | openssl x509 -noout -issuer -subject
Expected Issuer for Tailscale: O=Let's Encrypt
3. Operational Maintenance Cheatsheet
Caddy
# Zero-downtime config reload
cd ~/caddy && docker compose exec caddy caddy reload --config /etc/caddy/Caddyfile
# Check live logs
docker logs -f --tail 50 caddy
# Restart container
cd ~/caddy && docker compose restart
Adding a New Homelab Service
- Open
~/caddy/Caddyfile. - Add a subpath block under the Tailscale site definition:
redir /<APP> /<APP>/ handle_path /<APP>/* { reverse_proxy 127.0.0.1:<PORT> } - Reload Caddy:
cd ~/caddy && docker compose exec caddy caddy reload --config /etc/caddy/Caddyfile. - Access at
https://<TAILSCALE_DOMAIN>/<APP>/on port 443!
Tailscale
# Check peer status
tailscale status
# View daemon logs
journalctl -u tailscaled -n 50 --no-pager