Modern Reverse Proxy Architecture: Caddy Server & Cloudflare Tunnels for Container Ingress

4 min read

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

Tags:

← Previous:
Next: →