Using Caddy as a Reverse Proxy with SSL Termination and Cloudflare in Docker Compose
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:
- Building a custom Caddy Docker image with the cloudflare plugin.
- Configuring Docker Compose to use our built custom image.
- Automatically obtain and renew TLS certificates from Let's encrypt using DNS-01 via cloud flare.
- Reverse proxy multiple backend services based on URL paths or subdomains.
- 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
orcurl https://api.example.com/health
if you're routing by subdomain - Web:
curl https://example.com/web/status
orcurl 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.