Directly exposing application container ports (e.g. :8080, :3000) to the local network or public internet introduces severe security risks, prevents centralized TLS termination, and complicates domain routing.
Modern production homelabs and VPS environments replace complex, label-heavy reverse proxies with Caddy Server and Cloudflare Tunnels. This combination delivers human-readable configuration, automated HTTPS via Cloudflare DNS-01 challenges, and completely eliminates the need to open inbound firewall ports on your router.
⚡ Quick Runbook: 60-Second Implementation
TL;DR: Deploy a high-performance Caddy reverse proxy stack with automated Cloudflare DNS-01 certificate renewal:
# /etc/caddy/Caddyfile
{
email [email protected]
}
# Global Wildcard Ingress with Cloudflare DNS-01 ACME
*.homelab.example.com {
tls {
dns cloudflare {env.CLOUDFLARE_API_TOKEN}
}
# Forward git traffic to Forgejo container
@git host git.homelab.example.com
handle @git {
reverse_proxy forgejo:3000
}
# Forward telemetry to Netdata container
@metrics host monitor.homelab.example.com
handle @metrics {
reverse_proxy netdata:19999
}
# Fallback default
handle {
respond "Access Denied" 403
}
}
1. Why Caddy and Cloudflare Outperform Other Proxies
Many traditional reverse proxies require massive configuration files, complex XML/JSON syntax, or dozens of Docker container labels scattered across multiple Compose files. Caddy and Cloudflare solve these headaches through simplicity and security:
- Human-Readable Caddyfile: Caddy’s configuration syntax is concise, elegant, and declarative. A complete reverse proxy setup with automatic TLS takes only a few lines.
- Automated TLS by Default: Caddy manages certificate issuance, OCSP stapling, and renewals completely automatically via Let’s Encrypt and ZeroSSL.
- Zero Inbound Ports with Cloudflare Tunnels: When paired with
cloudflared, your server establishes an outbound-only connection to Cloudflare’s edge network. You never have to forward port 80 or 443 on your home router or expose public IP addresses. - Structured JSON Logging: Caddy natively emits high-performance structured JSON logs, allowing tools like CrowdSec and Vector/Loki to analyze access events with minimal CPU overhead.
2. Production Caddy Docker Compose Deployment
To use Cloudflare DNS-01 challenges (allowing wildcard SSL certificates for internal-only domains), run a Caddy image built with the caddy-dns/cloudflare module:
# /opt/stacks/caddy/compose.yaml
services:
caddy:
image: caddy:2.8-alpine
container_name: caddy
restart: unless-stopped
ports:
- "80:80"
- "443:443"
- "443:443/udp" # HTTP/3 QUIC
environment:
- CLOUDFLARE_API_TOKEN_FILE=/run/secrets/cf_api_token
secrets:
- cf_api_token
volumes:
- /opt/stacks/caddy/Caddyfile:/etc/caddy/Caddyfile:ro
- /opt/stacks/caddy/data:/data
- /opt/stacks/caddy/config:/config
- /opt/stacks/caddy/logs:/var/log/caddy
networks:
- proxy-tier
secrets:
cf_api_token:
file: ./secrets/cf_api_token.txt
networks:
proxy-tier:
name: proxy-tier
external: true
3. Hardened Caddyfile Configuration
Create /opt/stacks/caddy/Caddyfile with production security headers, compression, and structured logging:
# Global Options Block
{
email [email protected]
log {
output file /var/log/caddy/access.log {
roll_size 10mb
roll_keep 5
}
format json
}
}
# Reusable Security Snippet
(security_headers) {
header {
Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"
X-Content-Type-Options "nosniff"
X-Frame-Options "DENY"
Referrer-Policy "strict-origin-when-cross-origin"
-Server
}
}
# Wildcard Homelab Ingress
*.homelab.example.com {
import security_headers
encode zstd gzip
tls {
dns cloudflare {env.CLOUDFLARE_API_TOKEN}
}
@jellyfin host stream.homelab.example.com
handle @jellyfin {
reverse_proxy jellyfin:8096
}
@git host git.homelab.example.com
handle @git {
reverse_proxy forgejo:3000
}
handle {
respond "Not Found" 404
}
}
4. Zero-Trust Cloudflare Tunnel Integration
For public-facing applications where you do not wish to expose any home router ports, deploy a cloudflared sidecar alongside Caddy:
# Add cloudflared tunnel sidecar
cloudflared:
image: cloudflare/cloudflared:latest
container_name: cloudflared
restart: unless-stopped
command: tunnel run
environment:
- TUNNEL_TOKEN_FILE=/run/secrets/cf_tunnel_token
secrets:
- cf_tunnel_token
networks:
- proxy-tier
In the Cloudflare Zero Trust dashboard, point the tunnel public hostname (e.g. app.example.com) to http://caddy:80, letting Caddy manage internal routing while Cloudflare protects your edge.
Related Runbooks in This Series
- Base Linux OS Hardening: SSH Ed25519, UFW/nftables & Sysctl Baseline
- Fleet-Wide Intrusion Prevention: Deploying CrowdSec with Docker, Caddy & Firewall Bouncers
- Deploying a Secure WireGuard & Tailscale Exit Node on a VPS for Private Mobile Egress
- Zero-Trust Edge Ingress: Deploying Cloudflare Tunnels for Private Services
- Modern Reverse Proxy Architecture: Caddy Server & Cloudflare Tunnels for Container Ingress