caddy-reverse-proxy
npx machina-cli add skill ddnetters/homelab-agent-skills/caddy-reverse-proxy --openclawCaddy Reverse Proxy
Caddy is a modern web server with automatic HTTPS. This skill covers Caddyfile configuration, reverse proxy patterns, and Docker deployment for homelabs.
Caddyfile basics
# Reverse proxy to a backend service
app.example.com {
reverse_proxy localhost:8080
}
# Multiple domains, same backend
app1.example.com, app2.example.com {
reverse_proxy backend:3000
}
# Wildcard with on-demand TLS
*.example.com {
tls {
on_demand
}
reverse_proxy localhost:8080
}
Reverse proxy patterns
Basic proxy
service.example.com {
reverse_proxy service:8080
}
With path stripping
example.com {
handle /api/* {
reverse_proxy api-server:3000
}
handle {
reverse_proxy frontend:80
}
}
WebSocket support (automatic)
Caddy handles WebSocket upgrades automatically. No extra config needed.
Load balancing
app.example.com {
reverse_proxy node1:8080 node2:8080 node3:8080 {
lb_policy round_robin
health_uri /health
health_interval 10s
}
}
Headers
service.example.com {
reverse_proxy backend:8080 {
header_up X-Real-IP {remote_host}
header_up X-Forwarded-Proto {scheme}
}
}
TLS / HTTPS
Automatic (Let's Encrypt)
Caddy gets certificates automatically. Just use a domain name:
app.example.com {
reverse_proxy localhost:8080
}
Cloudflare DNS challenge
For wildcard certs or when port 80/443 isn't publicly reachable. Requires the caddy-cloudflare build.
*.example.com {
tls {
dns cloudflare {env.CLOUDFLARE_API_TOKEN}
}
@app1 host app1.example.com
handle @app1 {
reverse_proxy app1:8080
}
}
Internal / LAN-only (no public TLS)
:80 {
reverse_proxy localhost:8080
}
# Or with self-signed cert
service.lan {
tls internal
reverse_proxy localhost:8080
}
Restrict to LAN only
service.example.com {
@denied not remote_ip 192.168.0.0/16 10.0.0.0/8 172.16.0.0/12
respond @denied 403
reverse_proxy backend:8080
}
Common homelab patterns
Catch-all / landing page
*.example.com {
tls {
dns cloudflare {env.CLOUDFLARE_API_TOKEN}
}
# Named matchers for each service
@grafana host grafana.example.com
handle @grafana {
reverse_proxy grafana:3000
}
@n8n host n8n.example.com
handle @n8n {
reverse_proxy n8n:5678
}
# Fallback for unmatched subdomains
handle {
respond "Nothing here" 404
}
}
Basic auth
admin.example.com {
basicauth {
# Generate hash: caddy hash-password
admin $2a$14$HASHED_PASSWORD_HERE
}
reverse_proxy backend:8080
}
File server
files.example.com {
file_server browse {
root /srv/files
}
}
Redirect HTTP to HTTPS (automatic)
Caddy does this by default. To redirect a domain:
old.example.com {
redir https://new.example.com{uri} permanent
}
Docker deployment
docker-compose.yml
services:
caddy:
image: caddy:2-alpine
# Or for Cloudflare DNS: caddybuilds/caddy-cloudflare
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile
- caddy_data:/data
- caddy_config:/config
environment:
- CLOUDFLARE_API_TOKEN=your-token # if using DNS challenge
restart: unless-stopped
volumes:
caddy_data:
caddy_config:
Connecting to other Docker services
Services on the same Docker network are reachable by container name:
app.example.com {
reverse_proxy container-name:8080
}
For services on different networks, use the host IP or join networks:
services:
caddy:
networks:
- frontend
- backend
Management commands
# Validate config
caddy validate --config /etc/caddy/Caddyfile
# Reload without downtime (inside container)
docker exec <caddy-container> caddy reload --config /etc/caddy/Caddyfile
# Adapt Caddyfile to JSON (debugging)
caddy adapt --config /etc/caddy/Caddyfile
# View loaded config
caddy config
# Hash a password for basicauth
caddy hash-password
Environment variables
Use {env.VAR_NAME} in Caddyfile:
service.example.com {
reverse_proxy {env.BACKEND_HOST}:{env.BACKEND_PORT}
}
Logging
service.example.com {
log {
output file /var/log/caddy/access.log
format json
level INFO
}
reverse_proxy backend:8080
}
Troubleshooting
| Problem | Fix |
|---|---|
| Certificate not issuing | Check DNS points to Caddy, ports 80/443 open, or use DNS challenge |
| 502 Bad Gateway | Backend not running or wrong hostname/port. Check docker network ls |
| Config not reloading | Use caddy reload, not restart (avoids downtime). Check caddy validate first |
| Wildcard cert failing | Requires DNS challenge plugin (e.g. caddy-cloudflare), not default image |
| WebSocket disconnecting | Usually a proxy timeout. Add transport http { keepalive 30s } |
| LAN service reachable externally | Add remote_ip matcher to restrict access |
Caddy vs nginx quick reference
| nginx | Caddy |
|---|---|
proxy_pass http://backend; | reverse_proxy backend:8080 |
ssl_certificate /path/cert.pem; | Automatic (just use domain name) |
location /api { ... } | handle /api/* { ... } |
nginx -t && nginx -s reload | caddy validate && caddy reload |
| Separate config for HTTPS redirect | Automatic |
Source
git clone https://github.com/ddnetters/homelab-agent-skills/blob/main/caddy-reverse-proxy/SKILL.mdView on GitHub Overview
Caddy Reverse Proxy lets you map public domains to internal services using straightforward Caddyfile blocks. It adds automatic TLS, supports multiple reverse_proxy patterns, and integrates cleanly with Docker deployments for homelabs.
How This Skill Works
Define site blocks in a Caddyfile to declare hostnames and reverse_proxy targets. Caddy automatically handles TLS, WebSocket upgrades, and optional patterns like path stripping, load balancing, and header rewriting. For Docker-based setups, containers on the same network can be addressed by their service names, simplifying inter-service routing.
When to Use It
- Expose multiple internal services under distinct subdomains
- Add wildcard subdomains with on-demand TLS for public exposure
- Route specific paths to backends using handle blocks
- Proxy to Docker services by container name on the same network
- Operate in LAN-only mode with internal TLS or no public TLS
Quick Start
- Step 1: Write a Caddyfile snippet mapping domains to backends
- Step 2: Deploy Caddy (e.g., docker-compose) mounting the Caddyfile and data/config volumes
- Step 3: Bring up services and verify TLS and routing
Best Practices
- Use explicit reverse_proxy directives for each service
- Leverage handle blocks for path stripping and routing
- Enable TLS with automatic certificates; consider DNS or on_demand TLS for wildcards
- Balance load across multiple backends with health checks
- Limit exposure to LAN when needed; use internal tls or private domains
Example Use Cases
- app.example.com { reverse_proxy localhost:8080 }
- app1.example.com, app2.example.com { reverse_proxy backend:3000 }
- *.example.com { tls { dns cloudflare {env.CLOUDFLARE_API_TOKEN} } reverse_proxy localhost:8080 }
- service.example.com { reverse_proxy backend:8080 { header_up X-Real-IP {remote_host} X-Forwarded-Proto {scheme} } }
- admin.example.com { basicauth { admin $2a$14$HASHED_PASSWORD_HERE } reverse_proxy backend:8080 }