Using Caddy as a Reverse Proxy with SSL Termination and Cloudflare in Docker Compose

Reverse Proxy Nov 2, 2024

Caddy is a modern, lightweight web server with automatic HTTPs, easy configuration and built-in support for reverse proxying. In this guide, we'll show you how to:

  1. Building a custom Caddy Docker image with the cloudflare plugin.
  2. Configuring Docker Compose to use our built custom image.
  3. Automatically obtain and renew TLS certificates from Let's encrypt using DNS-01 via cloud flare.
  4. Reverse proxy multiple backend services based on URL paths or subdomains.
  5. Block direct IP access on ports 80 & 443 with a custom "Access Denied"

Prerequisites

  • Docker & Docker Compose installed
  • A domain managed in Cloudflare
  • Cloudflare API token with Zone.DNS permissions

Create a file named .env in your project root

# .env
# Your domain and Cloudflare DNS token
DOMAIN=example.com
CF_API_TOKEN=your_cloudflare_api_token
EMAIL=admin@example.com

Build a Custom Caddy Image with Cloudflare DNS Plugin

Create a Dockerfile.caddy alongside your project

# Stage 1: build Caddy with the Cloudflare DNS plugin
FROM caddy:2-builder AS builder

# Use xcaddy (bundled in caddy-builder image)
RUN xcaddy build \
    --with github.com/caddy-dns/cloudflare \
    --output /usr/bin/caddy-custom

# Stage 2: create the final lightweight image
FROM caddy:2-alpine

# copy the custom binary
COPY --from=builder /usr/bin/caddy-custom /usr/bin/caddy

# copy default Caddyfile (can be overridden by volume)
COPY Caddyfile /etc/caddy/Caddyfile

# Expose HTTP/S
EXPOSE 80 443

# Run Caddy
CMD ["caddy", "run", "--config", "/etc/caddy/Caddyfile", "--adapter", "caddyfile"]

Docker Compose Configuration

version: '3.7'
services:
  caddy:
    build:
      context: .
      dockerfile: Dockerfile.caddy
    container_name: caddy
    env_file: .env
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      - caddy_data:/data
      - caddy_config:/config
    ports:
      - '80:80'
      - '443:443'
    restart: unless-stopped

  api:
    image: your-api-image
    container_name: api
    expose:
      - '8080'
    restart: unless-stopped

  web:
    image: your-web-image
    container_name: web
    expose:
      - '3000'
    restart: unless-stopped

volumes:
  caddy_data:
  caddy_config:
  • caddy_data: stores TLS certificates and ACME data.
  • caddy_config: stores Caddy's runtime configuration

Writing the Caddyfile

Create a Caddyfile in the project root

{
  # Global options
  email {$EMAIL}
  # Use Cloudflare DNS provider for DNS-01 challenges
  acme_dns cloudflare
}

# Block direct IP access on HTTP/HTTPS
:80, :443 {
  respond @blockIP "Access Denied" 403
}

# Main site configuration
{$DOMAIN} {
  # Use DNS-01 challenge via Cloudflare
  tls {
    dns cloudflare {env.CF_API_TOKEN}
  }

  # API service at /api*
  handle_path /api/* {
    reverse_proxy api:8080
  }

  # Web UI at /web*
  handle_path /web/* {
    reverse_proxy web:3000
  }

  # Fallback: other paths
  handle {
    respond "Not Found" 404
  }
}

Notes

  • Global block: the first server block listens on ports 80 and 443 on the IP address of your host and returns 403.
  • DNS-01 Via Cloudflare: The tls { dns cloud flare } block requires the Cloudflare plugin we bundled.
  • Path-based routing: handle_path directs requests based on the URL prefix

Alternatively, requests can be routed to different services using sub-domains

{
  # Global options
  email {$EMAIL}
  # Use Cloudflare DNS provider for DNS-01 challenges
  acme_dns cloudflare
}

# Block direct IP access on HTTP/HTTPS
:80, :443 {
  respond @blockIP "Access Denied" 403
}

api.example.com {
  # Use DNS-01 challenge via Cloudflare
  tls {
    dns cloudflare {env.CF_API_TOKEN}
  }

  reverse_proxy api:8080

}

web.example.com {
  # Use DNS-01 challenge via Cloudflare
  tls {
    dns cloudflare {env.CF_API_TOKEN}
  }

  reverse_proxy web:3000

}

Launching & Testing

Bring up the stack

docker-compose up -d --build

Verify:

  • Certificates: Check Caddy logs for ACME issuance.

docker-compose logs -f caddy

  • API: curl https://example.com/api/health or curl https://api.example.com/health if you're routing by subdomain
  • Web: curl https://example.com/web/status or curl https://web.example.com/status (for subdomain routing)
  • Direct IP: curl -v https://xxx.x.xxx.xx (your host IP) should return HTTP 403 "Access Denied"

Conclusion

By building a custom Caddy image, you enable Cloudflare DNS integration for Let's Encrypt certificates. Combined with Docker Compose, path-based/subdomain routing and IP blocking, this setup provides a secure, automated reverse proxy solution for your microsevices.

Tags

Views: Loading...