SAVANNAH RIDGE LABS
Homelab
Self-hosted infrastructure running on personal hardware — a Proxmox hypervisor, two Ubuntu VMs, and a Raspberry Pi. Fully containerized, GitOps-deployed, and connected through a private Tailscale mesh.
01 — ARCHITECTURE
How it's connected
02 — NODES
The machines
jupiter-dev-tools
Edge / Auth / Public Services
- Caddy (reverse proxy)
- Authentik SSO
- Homarr dashboard
- BookStack wiki
- KaraKeep bookmarks
- Landing + Portfolio
media-server
Media Automation + AI
- Jellyfin (GPU transcode)
- Sonarr / Radarr / Lidarr
- qBittorrent via Gluetun VPN
- Ollama + Open WebUI
- FileFlows (GPU transcode)
- Prowlarr / Bazarr / Jellyseerr
cstatpi
Monitoring + Home Automation
- Prometheus + Grafana
- Home Assistant
- Uptime Kuma
- Upsnap Wake-on-LAN
- Homebridge (Apple HomeKit)
- cAdvisor + node-exporter
truenas-jupiter
NAS Storage
- CIFS media share
- Nextcloud instance
- Mounted by media-server at /media
03 — GITOPS
Automated deploys
Push to main and the changed stack re-deploys automatically — no SSH, no manual docker commands.
Git push → main
Developer pushes changes to any stack directory under Jupiter/ or cstatpi/.
Detect changed stacks
GitHub Actions diffs BASE_SHA..HEAD_SHA and sets boolean outputs for each of the 12 stacks based on path patterns.
Join Tailscale
The Actions runner authenticates to Tailscale as tag:server and joins the private mesh — no ports exposed to the internet.
POST Portainer webhook
For each changed stack, curl POSTs to the Portainer webhook URL over the Tailscale tunnel. Only changed stacks are triggered.
Portainer re-deploys
Portainer pulls the latest compose file from Git, runs docker compose pull and docker compose up -d --remove-orphans.
04 — TECHNOLOGY
The stack
Networking
Containers
CI/CD
Identity
Media Stack
Monitoring
05 — DECISIONS
Why these choices?
Why Tailscale over a traditional VPN?
Zero-config mesh networking — every node authenticates via OAuth without managing certs or firewall rules. GitHub Actions joins the mesh as a tagged node to reach Portainer webhooks, so no ports are ever exposed to the public internet.
Why Caddy over Nginx Proxy Manager?
Caddy's native Let's Encrypt DNS-01 integration (via Porkbun plugin) automatically issues and renews certificates for all 41 subdomains — including internal-only ones with no public inbound port. Config is plain text, version-controlled, and reloads live via --watch.
Why Portainer webhooks instead of SSH deploys?
Portainer's Git-backed stacks track the remote compose file and run docker compose up -d atomically on each deploy. The CI runner never needs SSH access — it just POSTs to a webhook URL over the Tailscale tunnel.
Why a dedicated Gluetun VPN container?
Download clients share Gluetun's network namespace via network_mode: service:gluetun. If the VPN drops, kernel-level routing blocks all traffic — no IP leaks. A port-updater sidecar automatically syncs Gluetun's forwarded port into qBittorrent.
06 — SECURITY
Network access model
Tailscale ACL Grants
All inter-service traffic travels over WireGuard. Zero inbound ports on any host.
GitHub Actions joins as tag:server and gets narrowly scoped access without credentials.
Authentik forward auth protects internal dashboards — single login, per-app policies.