Homelab Gateway Stack
Architecture Overview
| Service | Type | Role |
|---|---|---|
| Caddy | Reverse Proxy | Automatic SSL, Traffic Routing, Geo-Blocking (Layer 1). |
| CrowdSec | IDS / IPS | "The Brain." Detects attacks and bans IPs via Caddy. |
| Tailscale | Mesh VPN | Zero-trust remote access & Subnet Routing (Kernel Mode). |
| VoidAuth | Identity (SSO) | Centralized authentication for internal apps. |
Service Structure
Each service is containerized with its own compose.yml for modular management.
gateway-stack/
├── caddy/
│ ├── compose.yml # Custom Caddy Build (CrowdSec + Plugins)
│ ├── .env
| └── Caddyfile # Main routing logic & snippets
├── crowdsec/
│ ├── compose.yml
| ├── .env
│ └── acquis.yaml # Log acquisition rules (Caddy's Auth Log)
├── tailscale/
│ ├── compose.yml # Host-mode networking
| ├── .env
│ └── state/ # Persisted machine identity
├── voidauth/
├── compose.yml
├── .env
└── config/ # Branding & Database configs
Prerequisites
- Domain (standby for subdomain A records for publicly-exposed services)
- Cloudflare API Token (for DDNS with Caddy for auto-change ISP's public IP address in Cloudflare when/if happens). More details here at Cloudflare Setup page.
- Tailscale Auth Key (if deploying fresh).
- Maxmind Account sign-up
Directory Preparation
We use Bind Mounts to persist data. To prevent permission errors (Docker creating root-owned folders), we must manually create the folder structure before starting the containers.
If we let Docker auto-create a missing folder (e.g., ./state) during startup, the Docker Daemon (which runs as root) will create that folder owned by root:root.
We therefore manually create the folders before deploying. Since we created them, they are owned by our user (1000:1000), and no permissions issues.
Creating the required folders/files:
# Create service directories
mkdir -p caddy/{config,data,logs,maxmind}
mkdir -p crowdsec/{config,data}
mkdir -p tailscale/state
mkdir -p voidauth/{config,db}
# Create empty .env files (to be filled)
touch caddy/.env crowdsec/.env tailscale/.env voidauth/.env
# Create a Caddyfile to populate configs
touch caddy/Caddyfile
Expand to view the environment variables template
```text
Generic
BACKUP_USER=
CADDY
CF DNS Zone API Token for domain.com
CLOUDFLARE_API_TOKEN=
Crowdsec Bouncer
CROWDSEC_API_KEY=
Root Domain
ROOT_DOMAIN=
For Maxmind GeoIP
MAXMIND_ACCOUNT_ID= MAXMIND_LICENSE_KEY=
For VOIDAUTH
VOIDAUTH_STORAGE_KEY= VOIDAUTH_DB_PASS= VOIDAUTH_POSTGRES_PASS= PASS_STRENGTH=2 APP_TITLE='Title_Goes_Here'
FOR JELLYFIN
CLOUDFLARED - CF TUNNEL IF USING THIS INSTEAD OF CADDY REVERSE PROXY
CLOUDFLARE_TUNNEL_TOKEN=
FOR KOPIA
KOPIA_SERVER_USERNAME_HOST=admin@hostname KOPIA_SERVER_PASSWORD= KOPIA_REPO_PASSWORD=
FOR WUD
Set the check-in seconds (86400 = 1 day)
WUD_POLL_CYCLE=21600
ADD THESE LINES FOR LINUXSERVER.IO
GITHUB_USER= GITHUB_TOKEN=
ADD THESE LINES FOR DOCKERHUB
DOCKERHUB_USER= DOCKERHUB_TOKEN=
For Gotify Trigger
GOTIFY_TOKEN=
Basic Auth
WUD_NAME_USER= WUD_NAME_HASH=
FOR HOMEPAGE
HOMEPAGE_VAR_JELLYFIN_KEY= HOMEPAGE_VAR_JELLYSEERR_KEY= HOMEPAGE_VAR_RADARR_KEY= HOMEPAGE_VAR_SONARR_KEY= HOMEPAGE_VAR_BAZARR_KEY= HOMEPAGE_VAR_PROWLARR_KEY= HOMEPAGE_VAR_GOTIFY_KEY= HOMEPAGE_VAR_TRANS_USER= HOMEPAGE_VAR_TRANS_PASS= HOMEPAGE_VAR_QBIT_USER= HOMEPAGE_VAR_QBIT_PASS= HOMEPAGE_VAR_CROWDSEC_USER= HOMEPAGE_VAR_CROWDSEC_PASS= HOMEPAGE_VAR_GLUETUN_KEY= HOMEPAGE_VAR_BESZEL_USER= HOMEPAGE_VAR_BESZEL_PASS= HOMEPAGE_VAR_BESZEL_SYSID= HOMEPAGE_VAR_KOPIA_USERNAME_HOST=admin@host HOMEPAGE_VAR_KOPIA_PASS= HOMEPAGE_VAR_AUTHENTIK_API_TOKEN= HOMEPAGE_VAR_ROOT_DOMAIN= HOMEPAGE_VAR_SPEEDTEST_KEY= HOMEPAGE_VAR_TAILSCALE_DEVICE_ID= HOMEPAGE_VAR_TAILSCALE_KEY="tskey-api-xxxx"
FOR BESZEL
MEDIASVR_PUBLIC_KEY="ssh-ed25519 xxxx" MEDIASVR_TOKEN=
FOR ARCANE
ARCANE_KEY= ARCANE_JWT_SECRET=
FOR GLUETUN
AIRVPN - WIREGUARD SETTINGS
Go to AirVPN Client Area -> "WireGuard"
VPN_TYPE=wireguard WIREGUARD_PRIVATE_KEY== WIREGUARD_PRESHARED_KEY== WIREGUARD_ADDRESSES= SERVER_COUNTRIES=Singapore,Netherlands
WIREGUARD_MTU=1300
AIRVPN PORT FORWARDING (CRITICAL)
AirVPN Client Area -> "Port Forwarding"
Request Ports and paste the 5-digit number here
AIRVPN_PORT_QBIT= AIRVPN_PORT_TRANS=
TRANSMISSION CREDENTIALS
TRANSMISSION_USER= TRANSMISSION_PASS=''
GLUETUN CONTROL SERVER SECURITY
Enable logging for the control server
HTTP_CONTROL_SERVER_LOG=off
For SPEED TRACKER SERVICE
SPEED_KEY=
SONARR_API_KEY= RADARR_API_KEY=
Security Features
The "Double Wall" Defense
- Layer 1 (GeoIP): Caddy immediately drops connections from countries not on the whitelist (Powered by MaxMind).
- Layer 2 (Reputation): If GeoIP passes, the IP is checked against CrowdSec's local database. If the IP is known for brute-forcing, it is dropped.
Authentication
Exposed public urls like Jellyfin (Web-UI only), Seerr and Gotify (Web-UI only) are protected by VoidAuth.
- Flow: Request -> Caddy -> VoidAuth Check -> Service
Maintenance & Common Commands
Reload Caddy Config (Zero Downtime):
docker exec caddy caddy reload --config /etc/caddy/Caddyfile
Check Tailscale Status:
docker exec tailscale tailscale status