Enjoying the caffeine boost? If this repo saves you some time, buy me a coffee!

Table of Contents
- Now built on a minimal distroless base image.
- Expanded plugin set including rate limiting, Cloudflare IP handling, geolocation, Coraza WAF and more.
- Updated CI workflows and security docs.
This docker image enhances the work from @lucaslorentz by bundling several useful plugins:
- caddy-docker-proxy – auto-configure Caddy from container labels.
- caddy-dynamicdns – updates DNS records when your IP changes.
- sablier – start workloads on demand and stop them when idle.
- CrowdSec bouncer – block malicious traffic via CrowdSec (HTTP/AppSec/Layer4).
- caddy-admin-ui – experimental web UI for administration.
- caddy-storage-redis – store certificates in Redis for clustered setups.
- Cloudflare DNS – handle ACME DNS challenges through Cloudflare.
- transform-encoder – additional compression encoders.
- caddy-ratelimit – simple request rate limiting.
- caddy-l4 – layer‑4 (TCP/UDP) features.
- caddy-cloudflare-ip – log real client IPs when behind Cloudflare.
- caddy-maxmind-geolocation – MaxMind GeoIP lookups.
- Coraza WAF – integrate the Coraza web application firewall.
- caddy-security – authentication portals and security helpers.
- caddy-websockify – proxy and translate WebSockets.
The image uses a distroless base for a smaller footprint and improved security. Caddy and its plugins are refreshed automatically by GitHub Actions, so you always get the latest stable versions.
📔 For detailed guidance on using the base caddy-docker-proxy functionality, refer to the original documentation.
This image is ideal for using ™️ Caddy as a reverse proxy with Let's Encrypt and Cloudflare DNS.
GitHub Actions automatically update the Docker image weekly, including Caddy and all plugins.
It also supports dynamic IP address updates via Caddy DynamicDNS.
🔰 This image supports linux/amd64, linux/arm, and linux/arm64 architectures, making it suitable for standard Linux servers and various ARM-based devices, including Raspberry Pi.
You will need to have:
- 🐳 Docker
- 🐋 docker-compose
- Domain name -> you can get from Name Cheap
- Cloudflare DNS Zone
You will tell ™️ Caddy where it has to route traffic in docker network, as ™️ Caddy is ingress on this case.
⬇️ A simple docker-compose.yml:
services:
caddy:
container_name: caddy
image: homeall/caddy-reverse-proxy-cloudflare:latest
restart: unless-stopped
environment:
TZ: 'Europe/London'
volumes:
- "/var/run/docker.sock:/var/run/docker.sock" # needs socket to read events
- "./caddy-data:/data" # persist certificates via XDG_DATA_HOME
ports:
- "80:80"
- "443:443"
- "443:443/udp" # Enable HTTP/3
labels: # Global options
caddy.email: [email protected] # needs for acme CERT registration account
caddy.acme_dns: "cloudflare $API_TOKEN" # When set here, you don't need to set it for each service individually
# Optional: Enable Admin UI (experimental) - see section below for more details
# caddy.admin: "0.0.0.0:2019"
# caddy.admin.origins: "your.admin.domain.com" # Or use specific IP/host if not exposing publicly
whoami0:
container_name: whoam
image: traefik/whoami # Using traefik/whoami as jwilder/whoami is a bit old
hostname: TheDocker #----->>Expected result using curl
restart: unless-stopped
labels:
caddy: your.example.com # Caddy will route traffic for this domain
# caddy.tls.ca: "https://acme.zerossl.com/v2/DV90" # Uncomment if you prefer ZeroSSL. Default is Let's Encrypt.
caddy.reverse_proxy: "{{upstreams 80}}" # Forward traffic to port 80 of this container (traefik/whoami listens on 80)
caddy.tls.protocols: "tls1.3" # Optional: Enforce TLS 1.3. Default is tls1.2 and tls1.3.
caddy.tls.ca: "https://acme-staging-v02.api.letsencrypt.org/directory" # For testing. Remove for production.
caddy.tls.dns: "cloudflare $API_TOKEN" # (Optional when using global setting) Replace $API_TOKEN with your Cloudflare scoped API token.
Please get your scoped API-Token from here.
For quick tests without a compose file:
docker run -d --name caddy \
-v /var/run/docker.sock:/var/run/docker.sock \
-v $(pwd)/caddy-data:/data \
-e TZ="Europe/London" \
-p 80:80 -p 443:443 -p 443:443/udp \
homeall/caddy-reverse-proxy-cloudflare:latestLabel your other containers as in the compose example so Caddy can route traffic.
By default, this image uses caddy-docker-proxy to generate Caddy's configuration from Docker labels. However, you can also provide your own complete Caddyfile.
How Caddy Loads Configuration:
Caddy itself loads its primary configuration from /etc/caddy/Caddyfile by default.
Role of caddy-docker-proxy and Labels:
The caddy-docker-proxy service (which is part of this image's entrypoint logic) monitors Docker events and generates a Caddyfile based on the labels you define on your services. By default, caddy-docker-proxy writes this generated Caddyfile to /etc/caddy/Caddyfile.
Providing Your Own Caddyfile (Most Common Method):
If you want to use your own complete Caddyfile and bypass the label-based generation for the main configuration, mount your custom Caddyfile to /etc/caddy/Caddyfile.
Example docker-compose.yml snippet:
services:
caddy:
# ... other caddy service config ...
image: homeall/caddy-reverse-proxy-cloudflare:latest
volumes:
- "/var/run/docker.sock:/var/run/docker.sock" # Still needed if you import label-generated snippets or for other proxy features
- "./caddy-data:/data" # persist certificates via XDG_DATA_HOME
- "./my-custom-caddyfile:/etc/caddy/Caddyfile" # Mount your custom Caddyfile here
# environment:
# CADDY_DOCKER_CADDYFILE_PATH: '/etc/caddy/Caddyfile' # Default path for label-generated config.
# If you mount to /etc/caddy/Caddyfile, this var is implicitly handled.
# ...When you mount your own file to /etc/caddy/Caddyfile, it takes precedence over the file caddy-docker-proxy would generate at that same default location. The image's entrypoint is designed to detect a user-provided Caddyfile at this path and will use it directly.
Below is a minimal Caddyfile that configures a single domain using the Cloudflare DNS plugin. The Cloudflare API token is read from the CLOUDFLARE_API_TOKEN environment variable.
{
email [email protected]
acme_dns cloudflare {env.CLOUDFLARE_API_TOKEN}
}
example.com {
respond "Hello from Caddy"
}Before running Caddy, ensure the CLOUDFLARE_API_TOKEN environment variable is set with a token that has permission to manage your domain's DNS records.
Advanced: Label-Generated Config to a Different Path (CADDY_DOCKER_CADDYFILE_PATH)
The CADDY_DOCKER_CADDYFILE_PATH environment variable tells caddy-docker-proxy where it should write the Caddyfile it generates from Docker labels.
- If you do not set
CADDY_DOCKER_CADDYFILE_PATH, it defaults to/etc/caddy/Caddyfile. - If you mount your custom Caddyfile to
/etc/caddy/Caddyfile,caddy-docker-proxywill still attempt to write to this path, but your mounted file will be what Caddy uses.
Important Considerations:
- If you provide a custom Caddyfile to
/etc/caddy/Caddyfile, you are fully responsible for its content, including global options, TLS settings, and defining your sites. - Plugins like
caddy-storage-redisrequire their configuration to be in the global options block of the Caddyfile that Caddy loads (i.e., your custom/etc/caddy/Caddyfile). - The
caddy.emailandcaddy.acme_dnslabels on the Caddy service itself are typically used bycaddy-docker-proxyto generate global options. If you provide a full custom Caddyfile, ensure these global options (likeemailfor ACME andacme_dnsfor DNS challenges) are correctly defined in your Caddyfile's global block{...}.
The caddy-admin-ui plugin provides a web interface for managing Caddy.
To enable it, you can add the following global labels to your Caddy service in docker-compose.yml:
labels:
# ... other global labels ...
caddy.admin: "0.0.0.0:2019" # Listen address for the admin API & UI
caddy.admin.origins: "your.admin.domain.com" # Allowed Host header for accessing the UI (replace with your domain or IP)
# caddy.admin.enforce_origin: "true" # Optional: Enforce origin check
# caddy.admin.instance_id: "my-caddy-instance" # Optional: Custom instance IDThe caddy-storage-redis plugin allows Caddy to use Redis for storing certificates and other state. This is particularly useful in a distributed setup where multiple Caddy instances need to share this information.
Configuration for caddy-storage-redis is done within the global options of your Caddyfile (typically /etc/caddy/Caddyfile), specifically in the storage block. Environment variables are not directly used for configuring the Redis storage parameters themselves.
Here is an example Caddyfile snippet showing Redis storage configuration:
{
# All values are optional, below are the defaults
storage redis {
host 127.0.0.1
port 6379
address 127.0.0.1:6379 // derived from host and port values if not explicitly set
username ""
password ""
db 0
timeout 5
key_prefix "caddy"
encryption_key "" // default no encryption; enable by specifying a secret key containing 32 characters (longer keys will be truncated)
compression false // default no compression; if set to true, stored values are compressed using "compress/flate"
tls_enabled false
tls_insecure true
}
}
:443 {
# Your site configuration
# e.g., reverse_proxy / your-app:port
}ℹ️ Note: The example above shows the default values for the Redis storage module. If your Redis instance is running on a different server or requires authentication, you will need to update the host, port, address (if not using default host/port), username, password, and tls_enabled fields accordingly.
You'll also need a Redis instance running and accessible by Caddy. Here's a simple example of adding a Redis service to your docker-compose.yml if you don't have one already:
services:
# ... your caddy service ...
redis:
image: redis:alpine
container_name: redis
restart: unless-stopped
volumes:
- "./redis-data:/data" # Persist Redis data
# For production, set a password:
# command: redis-server --requirepass your-strong-passwordIf you set a password for Redis, ensure you configure it in your Caddyfile's storage redis block.
To use a custom Caddyfile (e.g., for configuring Redis storage or other specific settings not covered by labels), mount it to /etc/caddy/Caddyfile. See the "Using a Custom Caddyfile" section above for more details.
Example docker-compose.yml for Caddy service using a custom Caddyfile for Redis storage:
services:
caddy:
container_name: caddy
image: homeall/caddy-reverse-proxy-cloudflare:latest
restart: unless-stopped
environment:
TZ: 'Europe/London'
# CADDY_DOCKER_CADDYFILE_PATH: '/etc/caddy/Caddyfile' # Default path for label-generated config.
# When mounting to /etc/caddy/Caddyfile, this is implicitly handled.
volumes:
- "/var/run/docker.sock:/var/run/docker.sock" # For caddy-docker-proxy to read service labels
- "./caddy-data:/data" # persist certificates via XDG_DATA_HOME
- "./my-caddyfile-with-redis-config:/etc/caddy/Caddyfile" # Mount your Caddyfile here
ports:
- "80:80"
- "443:443"
- "443:443/udp"
# Docker labels for caddy-docker-proxy (e.g., for other services) can still be used
# in conjunction with a custom Caddyfile if your custom Caddyfile imports label-generated snippets.
# However, global options like 'storage' must be in the primary /etc/caddy/Caddyfile.
# labels:
# caddy.email: [email protected]
# caddy.acme_dns: "cloudflare $API_TOKEN"The caddy-storage-redis configuration (like the storage redis { ... } block) must be in the global options of the Caddyfile that Caddy loads (i.e., /etc/caddy/Caddyfile if you've mounted your own).
⬆️ Go on TOP ☝️
⬇️ Your can run the following command to see that is working:
$ curl --insecure -vvI https://your.example.com 2>&1 | awk 'BEGIN { cert=0 } /^\* Server certificate:/ { cert=1 } /^\*/ { if (cert) print }'
* Server certificate:
* subject: CN=your.example.com
* start date: <Date specific to your test>
* expire date: <Date specific to your test>
* issuer: CN=Fake LE Intermediate X1 # This indicates staging/test certificate
* SSL certificate verify result: unable to get local issuer certificate (20), continuing anyway.
* Using HTTP2, server supports multi-use
* Connection state changed (HTTP/2 confirmed)
* Copying HTTP/2 data in stream buffer to connection buffer after upgrade: len=0
* Using Stream ID: 1 (easy handle <some_hex_value>)
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
* Connection state changed (MAX_CONCURRENT_STREAMS == 250)!
$ curl -k https://your.example.com
I'm TheDocker
Make sure to replace your.example.com with the domain you configured in the whoami service labels. The output I'm TheDocker comes from the hostname set in the whoami service. If you used traefik/whoami on port 80, it will output its own identifying information.
🗞️ Check the LICENSE for more information.
🔴 Open an issue on GitHub if you run into problems.
If you find this image useful, you can buy me a coffee to help keep development going.
