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 routes cockpit.ganeshr.me to https://host.docker.internal:9090.
  • tls_insecure_skip_verify is 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 reload

View Live Access & Reverse Proxy Logs

sudo docker logs -f caddy

Test Caddyfile Syntax

sudo docker exec -w /etc/caddy caddy caddy validate

5. Troubleshooting Common Caddy Issues

IssueCauseSolution
Infinite Redirect Loop on LoginMissing 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 domainThe 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 ImmediatelyCockpit rejects the Host origin or WebSocket upgrade.Verify /etc/cockpit/cockpit.conf includes Origins = https://cockpit.ganeshr.me wss://cockpit.ganeshr.me.