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.

47+
Containerized Services
12
Docker Compose Stacks
41
Public Subdomains
3
Active Hosts

01 — ARCHITECTURE

How it's connected

Internet
HTTPS · DNS-01 · *.savannahridgelabs.com
Porkbun DNS + Let's Encrypt

Caddy Reverse Proxy

41 subdomains · Auto TLS · --watch

Jupiter PROXMOX
Savannah-Ridge-Proxy
Mark-Windows
Dev Tools
Media Server
CIFS
TrueNAS
Savannah Ridge Pi
CSTAT Pi
Saturn PROXMOX
Apogee-Prod
Apogee-Integrations
All nodes connected via Tailscale mesh VPN · WireGuard · zero-config

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.

01

Git push → main

Developer pushes changes to any stack directory under Jupiter/ or cstatpi/.

02

Detect changed stacks

GitHub Actions diffs BASE_SHA..HEAD_SHA and sets boolean outputs for each of the 12 stacks based on path patterns.

03

Join Tailscale

The Actions runner authenticates to Tailscale as tag:server and joins the private mesh — no ports exposed to the internet.

04

POST Portainer webhook

For each changed stack, curl POSTs to the Portainer webhook URL over the Tailscale tunnel. Only changed stacks are triggered.

05

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

Tailscale (WireGuard mesh VPN)
Caddy (reverse proxy + TLS)
Porkbun DNS-01 ACME
Let's Encrypt (auto-renew)

Containers

Docker Engine
Docker Compose v2
Portainer (per-host UI)
GitOps webhook deploy

CI/CD

GitHub Actions
tailscale/github-action@v2
Path-based stack detection
Portainer webhook per stack

Identity

Authentik (OIDC + LDAP)
Forward auth via Caddy
Per-service SSO
LDAP bridge for apps

Media Stack

Sonarr / Radarr / Lidarr
Prowlarr / Bazarr
Jellyfin + GPU transcode
Gluetun VPN gateway

Monitoring

Prometheus
Grafana
cAdvisor + node-exporter
Uptime Kuma

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

admin / owner
all nodes · all ports
tag:server GitHub Actions runner
all nodes · all ports
member
Caddy edge + media-server All Caddy-proxied services
member
via exit node
internet
SSH · admins
any tagged device
SSH · members
own devices only
No open ports

All inter-service traffic travels over WireGuard. Zero inbound ports on any host.

Tag-based ACLs

GitHub Actions joins as tag:server and gets narrowly scoped access without credentials.

SSO on every service

Authentik forward auth protects internal dashboards — single login, per-app policies.