A fully automated, secure reverse proxy stack in a single Docker image.
caddy-plus integrates four components into one binary:
- Caddy: The ultimate server with automatic HTTPS.
- Caddy Docker Proxy: Auto-generates Caddy configuration from Docker labels (no manual Caddyfile editing).
- CrowdSec Bouncer: Adds IP blocking and a Web Application Firewall (WAF) to every site you host.
- Cloudflare DNS: Enables DNS-01 challenges for Wildcard SSL certificates and internal servers.
The image is automatically rebuilt and updated on GHCR whenever there is a new release of Caddy or any of its plugins.
This setup provides a fully automated, secure reverse proxy stack:
- Dynamic Config (
caddy-docker-proxy): Caddy connects to the Docker socket. When you launch a new container with specific labels, Caddy automatically provisions SSL certificates (via HTTP or Cloudflare DNS) and routes traffic to it. - IP Blocker (
crowdsec): Acts like a front-desk security guard. It checks the IP of every visitor against CrowdSec's global blocklist before allowing access. - WAF (
appsec): Acts like a security team inside the building. It inspects the content of requests to block SQL injection, XSS, and known CVE exploits.
Follow these steps to integrate this Caddy image into your Docker setup.
Create the network externally first. This ensures the network name is exactly caddy_net and prevents Docker Compose from adding random prefixes (like myproject_caddy_net) that break the proxy discovery.
docker network create caddy_netIn your docker-compose.yml, use the image ghcr.io/buildplan/caddy-plus:latest.
Critical Requirement: You must mount the Docker socket so the proxy can detect your containers. You also need a shared volume for logs so CrowdSec can read Caddy's access logs.
Note: You do not need to mount a
Caddyfile. We configure global settings (like API keys) using labels on the Caddy container itself.
services:
caddy:
image: ghcr.io/buildplan/caddy-plus:latest
container_name: caddy
restart: unless-stopped
ports:
- "80:80"
- "443:443"
- "443:443/udp" # HTTP/3 Support
environment:
# EXACT MATCH: Must match the external network name from Step 1
- CADDY_INGRESS_NETWORKS=caddy_net
# Cloudflare Token for DNS challenges & Real IP resolution
- CF_API_TOKEN=your_cloudflare_token
networks:
- caddy_net
volumes:
- /var/run/docker.sock:/var/run/docker.sock # REQUIRED for auto-discovery
- caddy_data:/data
# Mount a volume for logs so CrowdSec can read them
- ./caddy_logs:/var/log/caddy
# GLOBAL CONFIGURATION VIA LABELS
labels:
caddy.email: "you@example.com"
# 1. Global Logging Configuration
# We tell Caddy to write logs to a file that CrowdSec can see
caddy.log.output: "file /var/log/caddy/access.log"
caddy.log.format: "json"
caddy.log.level: "INFO"
# 2. CrowdSec Configuration
# This creates the global { crowdsec { ... } } block
caddy.crowdsec.api_url: "http://crowdsec:8080"
caddy.crowdsec.api_key: "YOUR_BOUNCER_KEY_HERE" # See Step 3
caddy.crowdsec.appsec_url: "http://crowdsec:7422"
# 3. Cloudflare Trusted Proxies (Global Option)
# This ensures CrowdSec sees real IPs, not Cloudflare's
caddy.servers.trusted_proxies: "cloudflare"
# 4. Define Reusable Snippet: (cloudflare_tls)
# Other containers can import this to get DNS-01 SSL certs
caddy_0: "(cloudflare_tls)"
caddy_0.tls.dns: "cloudflare {env.CF_API_TOKEN}"
caddy_0.tls.resolvers: "1.1.1.1"
crowdsec:
image: crowdsecurity/crowdsec:latest
container_name: crowdsec
environment:
- COLLECTIONS=crowdsecurity/caddy crowdsecurity/appsec-virtual-patching crowdsecurity/appsec-generic-rules
# Listen on all interfaces so Caddy can reach it
- CROWDSEC_LAPI_LISTEN_URI=0.0.0.0:8080
networks:
- caddy_net
volumes:
- ./crowdsec-db:/var/lib/crowdsec/data
- ./crowdsec-config:/etc/crowdsec
# Mount the custom acquisition file (Created in Step 2)
- ./crowdsec-config/acquis.yaml:/etc/crowdsec/acquis.yaml
# Shared logs volume
- ./caddy_logs:/var/log/caddy
networks:
caddy_net:
external: true # <--- This prevents Docker from renaming the network
volumes:
caddy_data:
crowdsec-db:You need to tell CrowdSec to read the file that Caddy is writing to.
Create a file named acquis.yaml inside your ./crowdsec-config/ directory:
# ./crowdsec-config/acquis.yaml
filenames:
- /var/log/caddy/access.log
labels:
type: caddyNote: You also need to create the log file on the host initially to ensure permissions are correct:
mkdir -p caddy_logs
touch caddy_logs/access.log
chmod 666 caddy_logs/access.logYour Caddy bouncer needs a key to talk to the CrowdSec agent. Start the CrowdSec container, then run:
docker exec crowdsec cscli bouncers add caddy-bouncerCopy the API key generated and paste it into the caddy.crowdsec.api_key label in your docker-compose.yml (Step 1).
To use the WAF features, enable the AppSec engine in CrowdSec.
- Create the AppSec config: Inside your mounted CrowdSec config folder (e.g.,
./crowdsec-config/acquis.d/), create a file namedappsec.yaml:
# ./crowdsec-config/acquis.d/appsec.yaml
listen_addr: 0.0.0.0:7422
appsec_config: crowdsecurity/appsec-default
name: caddy-appsec-listener
source: appsec
labels:
type: appsec- Restart CrowdSec:
docker restart crowdsecWith caddy-docker-proxy, you add labels to the containers you want to expose.
Crucial: You must add caddy.log.output to your service labels. This tells Caddy to write the access logs for this specific site to the default log file we configured in Step 1.
DNS Tip: To avoid manually creating DNS records for every new service, add a wildcard A record (*) in Cloudflare pointing to your server IP.
Here is an example whoami service using the Cloudflare DNS Challenge:
services:
whoami:
image: traefik/whoami
networks:
- caddy_net
labels:
# 1. Define the domain
caddy: "whoami.example.com"
# 2. Use Cloudflare DNS Challenge
# This imports the snippet we defined on the main Caddy container
caddy.import: "cloudflare_tls"
# 3. Enable Logging (REQUIRED for CrowdSec)
caddy.log.output: "file /var/log/caddy/access.log"
caddy.log.format: "json"
# 4. Enable Security (CrowdSec + AppSec)
caddy.route.0_crowdsec: ""
caddy.route.1_appsec: ""
# 5. Security Headers
caddy.header.Strict-Transport-Security: "max-age=31536000; includeSubDomains"
caddy.header.X-Frame-Options: "SAMEORIGIN"
caddy.header.X-Content-Type-Options: "nosniff"
# 6. Reverse Proxy
caddy.route.2_reverse_proxy: "{{upstreams 80}}"
networks:
caddy_net:
external: true
Explanation of Labels:
caddy.servers.trusted_proxies: (Step 1) Tells Caddy to trust Cloudflare IPs so it can see the real client IP.caddy_0: (cloudflare_tls): (Step 1) Defines a reusable snippet for DNS configuration.caddy.import: (Step 5) Applies that snippet to your specific container.caddy.log.output: Enables access logging for this site.
- Start the stack:
docker compose up -d- Generate Traffic: Visit your site to generate some logs.
curl -I [https://whoami.example.com](https://whoami.example.com)- Check CrowdSec Metrics: Verify that CrowdSec is reading the logs and AppSec is receiving data.
docker exec crowdsec cscli metrics- Look for Acquisition Metrics: Should show
file:/var/log/caddy/access.logwith "Lines read" > 0. - Look for Parser Metrics: Should show
crowdsecurity/caddy-logs.
Since the configuration is generated in-memory, you can't open a file to check it. Use this command to see what Caddy is actually using:
docker logs caddy 2>&1 | grep "New Caddyfile" | tail -n 1 | sed 's/.*"caddyfile":"//' | sed 's/"}$//' | sed 's/\\n/\n/g' | sed 's/\\t/\t/g'The Caddy binary includes the CrowdSec CLI for health checks.
# Check if an IP is currently banned
docker exec caddy caddy crowdsec check 1.2.3.4
# Check connection health
docker exec caddy caddy crowdsec health$ docker exec caddy caddy crowdsec --help
Commands related to the CrowdSec integration (experimental)
The subcommands can help assessing the status of the CrowdSec integration.
Output of the commands can change, so shouldn't be relied upon (yet).
Usage:
caddy crowdsec [command]
Available Commands:
check Checks an IP to be banned or not
health Checks CrowdSec integration health
info Shows CrowdSec runtime information
ping Pings the CrowdSec LAPI endpoint
Flags:
-a, --adapter string Name of config adapter to apply (when --config is used)
--address string The address to use to reach the admin API endpoint, if not the default
-c, --config string Configuration file to use to parse the admin address, if --address is not used
-h, --help help for crowdsec
-v, --version version for crowdsec
Use "caddy crowdsec [command] --help" for more information about a command.
For documentation on Docker Proxy labels, visit: https://github.com/lucaslorentz/caddy-docker-proxy
caddy-docker-proxy extends caddy's CLI with the command caddy docker-proxy.
$ docker exec caddy caddy help docker-proxy
Usage:
caddy docker-proxy <command> [flags]
Flags:
--caddyfile-path string Path to a base Caddyfile that will be extended with docker sites
--controller-network string Network allowed to configure caddy server in CIDR notation. Ex: 10.200.200.0/24
--docker-apis-version string Docker socket apis version comma separate
--docker-certs-path string Docker socket certs path comma separate
--docker-sockets string Docker sockets comma separate
--envfile string Environment file with environment variables in the KEY=VALUE format
--event-throttle-interval duration Interval to throttle caddyfile updates triggered by docker events (default 100ms)
-h, --help help for docker-proxy
--ingress-networks string Comma separated name of ingress networks connecting caddy servers to containers.
When not defined, networks attached to controller container are considered ingress networks
--label-prefix string Prefix for Docker labels (default "caddy")
--mode string Which mode this instance should run: standalone | controller | server (default "standalone")
--polling-interval duration Interval caddy should manually check docker for a new caddyfile (default 30s)
--process-caddyfile Process Caddyfile before loading it, removing invalid servers (default true)
--proxy-service-tasks Proxy to service tasks instead of service load balancer (default true)
--scan-stopped-containers Scan stopped containers and use its labels for caddyfile generation
Those flags can also be set via environment variables:
CADDY_DOCKER_CADDYFILE_PATH=<string>
CADDY_DOCKER_ENVFILE=<string>
CADDY_CONTROLLER_NETWORK=<string>
CADDY_INGRESS_NETWORKS=<string>
CADDY_DOCKER_SOCKETS=<string>
CADDY_DOCKER_CERTS_PATH=<string>
CADDY_DOCKER_APIS_VERSION=<string>
CADDY_DOCKER_LABEL_PREFIX=<string>
CADDY_DOCKER_MODE=<string>
CADDY_DOCKER_POLLING_INTERVAL=<duration>
CADDY_DOCKER_PROCESS_CADDYFILE=<bool>
CADDY_DOCKER_PROXY_SERVICE_TASKS=<bool>
CADDY_DOCKER_SCAN_STOPPED_CONTAINERS=<bool>
CADDY_DOCKER_NO_SCOPE=<bool, default scope used>- Caddy Docker Proxy: Dynamic configuration using Docker labels.
- CrowdSec Bouncer: Security module for Caddy.
- Cloudflare DNS: DNS provider for solving ACME challenges.
- Cloudflare IP: Real visitor IP restoration when behind Cloudflare Proxy.