Caddy as the Front Door of My Homelab
By Xin Lu profile image Xin Lu
1 min read

Caddy as the Front Door of My Homelab

Once you run more than a couple of services at home, a reverse proxy stops being optional.

My setup is simple: everything sits behind Caddy.
Only ports 80 / 443 are exposed. Services stay internal and move freely.

Caddy handles the boring but critical parts:

  • a single public entry point
  • automatic TLS
  • clean separation between domains and backend services

Below is the out-of-box Docker Compose configuration I use.

services:
  caddy:
    image: caddy:latest
    container_name: caddy
    restart: unless-stopped
    cap_add:
      - NET_ADMIN
    ports:
      - "80:80"
      - "443:443"
      - "443:443/udp"   # HTTP/3
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile
      - ./site:/srv
      - caddy_data:/data
      - caddy_config:/config

volumes:
  caddy_data:
  caddy_config:

With this in place, adding or removing services is just editing the Caddyfile.
No port juggling, no mental overhead.

Once a reverse proxy is in front, the homelab becomes something you can grow without friction.

Some of my Caddyfile configurations (masked):

# =============================
# Git server
# =============================

git.example.org {
    reverse_proxy 192.168.10.22:8001
}


# =============================
# well-known redirects + proxy
# =============================

cloud.example.org {
    redir /.well-known/carddav /remote.php/dav/ 301
    redir /.well-known/caldav  /remote.php/dav/ 301
    reverse_proxy 192.168.10.23:11000
}


# =============================
# Upstream HTTPS (internal cert)
# =============================

vpn.example.org {
    reverse_proxy https://192.168.10.6:943 {
        transport http {
            tls_insecure_skip_verify
        }
    }
}


# =============================
# Subpath rewrite (Guacamole)
# =============================

remote.example.org {
    rewrite * /guacamole{uri}
    reverse_proxy 192.168.10.24:8080
}


# =============================
# Model APIs: auth gate + long timeouts
# =============================

(mymodel_auth) {
    @unauth {
        not header Authorization "Bearer <REDACTED>"
    }
    respond @unauth "Unauthorized" 401
}

m0.example.org {
    import mymodel_auth
    reverse_proxy 192.168.10.9:8080 {
        transport http {
            read_timeout  1h
            write_timeout 1h
        }
    }
}

# m1.example.org / m2.example.org / ollama.example.org
# follow the same pattern with different ports
By Xin Lu profile image Xin Lu
Updated on
HomeLab Docker