Skip to content

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= PUID=1000 PGID=1000 TZ=Asia/Singapore

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