Caddy Reverse Proxy & Routing
Overview
Caddy is the central traffic controller for the entire homelab. It sits behind the Cloudflare tunnel, listens on internal port
80, inspects incoming domain headers, enforces authentication through Authelia, and securely dispatches requests to individual Docker containers or host services.
1. What is a Reverse Proxy? (Beginner Mental Model)
Think of Caddy as the front-desk concierge of an office building:
flowchart TD UserReq["Incoming Request: https://hq.ganeshr.me"] --> Caddy["Caddy Reverse Proxy (Port 80)"] subgraph ForwardAuthCheck ["Security Checkpoint"] Caddy -->|1. 'Is this person authorized?'| Authelia["Authelia (:9091)"] Authelia -- "No" --> Redir["Redirect to auth.ganeshr.me"] Authelia -- "Yes (HTTP 200)" --> Pass["Allow through + Attach User Headers"] end Pass --> Backend["Forward to Homepage Container (:3000)"] Backend --> Response["Return Dashboard HTML/CSS to User"]
Without Caddy, every service would need its own dedicated public port or separate domain configuration. Caddy unifies everything behind a single, clean internal interface.
2. Key Architecture Features in Our Homelab
A. The Forward Authentication Pattern (forward_auth)
For protected apps (hq, wealth, cockpit), Caddy never serves content directly to unverified visitors.
Instead, it pauses the request and calls Authelia’s forward-auth API endpoint:
- If Authelia replies with HTTP 200 OK, Caddy attaches identity headers (
Remote-User,Remote-Groups,Remote-Email) and hands the request to the application. - If Authelia replies with HTTP 302 / 401, Caddy immediately redirects the browser to the biometric login screen at
auth.ganeshr.me.
B. Bridging Docker to Host Services (host.docker.internal)
While most apps run in Docker, Cockpit runs natively on the Ubuntu host OS on port 9090.
- In
docker-compose.yml, Caddy is given access to the host gateway:extra_hosts: - host.docker.internal:host-gateway - In
Caddyfile, Caddy routescockpit.ganeshr.metohttps://host.docker.internal:9090. tls_insecure_skip_verifyis enabled for this route because Cockpit uses an internal self-signed SSL certificate.
C. Public Custom Static Assets (/custom-assets/)
Homepage needs to render custom app icons, server wallpaper, and backup status badges. If those assets required passkey authentication, icons would break on the login screen or during widget refreshes.
- Caddy exposes
/custom-assets/*mapping directly to the local folder./assets(/var/www/assets). - This endpoint is exempted from Authelia authentication.
3. Annotated Caddyfile Walkthrough
The configuration lives at /opt/vaultwarden/Caddyfile:
# ==============================================================================
# 1. REUSABLE SNIPPETS
# ==============================================================================
# Forward-auth snippet: Checks every request with Authelia before proxying
(authelia_middleware) {
forward_auth authelia:9091 {
uri /api/authz/forward-auth
copy_headers Remote-User Remote-Groups Remote-Name Remote-Email
header_up X-Forwarded-Proto https
header_up X-Forwarded-Host {host}
}
}
# Static assets snippet: Serves icons, wallpapers, and backup-status.json
(custom_assets) {
handle_path /custom-assets/* {
root * /var/www/assets
file_server
}
}
# Catch-all for internal asset requests
http://caddy {
import custom_assets
}
# ==============================================================================
# 2. AUTHELIA AUTHENTICATION PORTAL (Zero 2FA check here to avoid loop)
# ==============================================================================
http://auth.ganeshr.me, auth.ganeshr.me {
encode gzip zstd
import custom_assets
handle {
reverse_proxy authelia:9091 {
header_up X-Real-IP {remote_host}
header_up X-Forwarded-Proto https
}
}
}
# ==============================================================================
# 3. HOMEPAGE DASHBOARD (Protected by Authelia Passkey)
# ==============================================================================
http://hq.ganeshr.me, hq.ganeshr.me {
encode gzip zstd
import custom_assets
handle {
import authelia_middleware # <-- Verifies passkey before granting access
reverse_proxy homepage:3000 {
header_up X-Real-IP {remote_host}
}
}
}
# ==============================================================================
# 4. VAULTWARDEN (Bitwarden Handles Its Own Zero-Knowledge Auth)
# ==============================================================================
http://vault.ganeshr.me, vault.ganeshr.me {
encode gzip zstd
import custom_assets
handle {
reverse_proxy vaultwarden:80 {
header_up X-Real-IP {remote_host}
}
}
}
# ==============================================================================
# 5. COCKPIT LINUX CONSOLE (Host Bridge + WebSocket Support)
# ==============================================================================
http://cockpit.ganeshr.me, cockpit.ganeshr.me {
encode gzip zstd
import custom_assets
handle {
import authelia_middleware
reverse_proxy https://host.docker.internal:9090 {
header_up Host {host}
header_up X-Forwarded-Proto https
header_up X-Real-IP {remote_host}
transport http {
tls_insecure_skip_verify
tls_server_name cockpit.ganeshr.me
}
}
}
}
# ==============================================================================
# 6. WEALTHFOLIO (Net Worth Tracker Protected by Authelia)
# ==============================================================================
http://wealth.ganeshr.me, wealth.ganeshr.me {
encode gzip zstd
import custom_assets
handle {
import authelia_middleware
reverse_proxy wealthfolio:8088 {
header_up X-Real-IP {remote_host}
}
}
}4. Operational Commands
Reload Caddy Without Dropping Connections
Whenever you modify /opt/vaultwarden/Caddyfile, reload it instantly without restarting the container:
sudo docker exec -w /etc/caddy caddy caddy reloadView Live Access & Reverse Proxy Logs
sudo docker logs -f caddyTest Caddyfile Syntax
sudo docker exec -w /etc/caddy caddy caddy validate5. Troubleshooting Common Caddy Issues
| Issue | Cause | Solution |
|---|---|---|
| Infinite Redirect Loop on Login | Missing X-Forwarded-Proto https. Authelia thinks the request is insecure HTTP and attempts to redirect to HTTPS repeatedly. | Ensure header_up X-Forwarded-Proto https is present in both the authelia_middleware snippet and the auth.ganeshr.me block. |
| HTTP 502 Bad Gateway on one domain | The specific backend container is crashed, restarting, or bound to the wrong internal port. | Run sudo docker ps to check if the target container (e.g. homepage, wealthfolio) is running. Check container logs: sudo docker logs <container_name>. |
| Cockpit Console Disconnects Immediately | Cockpit rejects the Host origin or WebSocket upgrade. | Verify /etc/cockpit/cockpit.conf includes Origins = https://cockpit.ganeshr.me wss://cockpit.ganeshr.me. |
6. Next Steps & Related Links
- 01 - Cloudflare Zero Trust & Tunnel — The tunnel that sends traffic into Caddy.
- 03 - Authelia & Biometric Passkey SSO — How Authelia handles the authentication requests from Caddy.