Caddy Cloudflare
A fully integrated Caddy Docker image featuring Cloudflare DNS-01 ACME validation
Automated, multi-platform Docker builds of Caddy with Cloudflare DNS and proxy IP trust modules baked in.
Table of Contents
- Docker Pull - Caddyfile Examples - Creating a Cloudflare API Token - ACME DNS ChallengeFeatures
- Automated Builds -- Daily checks for new Caddy releases with automatic image builds.
- Module Update Detection -- Tracks upstream module commits and rebuilds when dependencies change.
- Base Image Tracking -- Rebuilds when the official Caddy image is republished with OS or Go toolchain fixes.
- Cloudflare DNS-01 Integration -- Automatic SSL certificate management via DNS challenges.
- Cloudflare IP Trust -- Real client IPs preserved when proxying through Cloudflare.
- Multi-Platform -- Supports
amd64,arm64,arm/v7(Raspberry Pi),ppc64le, ands390x. - Alpine Variant -- Lightweight Alpine-based image for smaller footprint.
- Fork-Friendly -- Image names derived automatically from your GitHub repository.
Quick Start
Pull the image and set up a docker-compose.yml:
services:
caddy:
image: ghcr.io/caddybuilds/caddy-cloudflare:latest
restart: unless-stopped
cap_add:
- NET_ADMIN
ports:
- "80:80"
- "443:443"
- "443:443/udp"
volumes:
- $PWD/Caddyfile:/etc/caddy/Caddyfile
- $PWD/site:/srv
- caddy_data:/data
- caddy_config:/config
environment:
- CLOUDFLARE_API_TOKEN=your_cloudflare_api_token
volumes:
caddy_data:
external: true
caddy_config:
Defining the data volume as external ensuresdocker-compose downdoes not delete it. Create it manually withdocker volume create caddy_data.
Replace your_cloudflare_api_token with your actual token. See Creating a Cloudflare API Token for setup instructions, and Caddyfile Examples for configuration samples.
Included Modules
These modules are compiled into Caddy at build time using xcaddy. No additional installation is needed.
| Module | Purpose | Repository |
|--------|---------|------------|
| caddy-dns/cloudflare | DNS-01 ACME challenge provider for Cloudflare | caddy-dns/cloudflare |
| caddy-cloudflare-ip | Automatically trust Cloudflare proxy IP ranges for correct client IPs | WeidiDeng/caddy-cloudflare-ip |
| caddy-combine-ip-ranges | Combine IP ranges from multiple sources (e.g. Cloudflare plus your own) for trusted_proxies | fvbommel/caddy-combine-ip-ranges |
| caddy-ratelimit | Rate limit requests per real client IP, even behind Cloudflare's proxy | mholt/caddy-ratelimit |
Usage
Docker Pull
# Standard image
docker pull ghcr.io/caddybuilds/caddy-cloudflare:latest
docker pull caddybuilds/caddy-cloudflare:latest
Alpine variant
docker pull ghcr.io/caddybuilds/caddy-cloudflare:alpine
docker pull caddybuilds/caddy-cloudflare:alpine
See Quick Start for a full docker-compose setup.
Caddyfile Examples
Global DNS Configuration
Set the ACME DNS challenge provider globally so it applies to all sites:
{
acme_dns cloudflare {env.CLOUDFLARE_API_TOKEN}
}
example.com {
root * /usr/share/caddy
file_server
encode gzip
}
another-example.com {
reverse_proxy localhost:8080
}
Per-Site DNS Configuration
Configure DNS challenge individually per site:
example.com {
root * /usr/share/caddy
file_server
encode gzip
tls {
dns cloudflare {env.CLOUDFLARE_API_TOKEN}
}
}
another-example.com {
reverse_proxy localhost:8080
tls {
dns cloudflare {env.CLOUDFLARE_API_TOKEN}
}
}
Cloudflare IP Trust
Trust Cloudflare's proxy IPs to preserve real client IPs in logs and headers:
{
acme_dns cloudflare {env.CLOUDFLARE_API_TOKEN}
servers {
trusted_proxies cloudflare
client_ip_headers Cf-Connecting-Ip
}
}
example.com {
root * /usr/share/caddy
file_server
encode gzip
}
With this setup:
- Requests proxied through Cloudflare will correctly populate
X-Forwarded-Forheaders. - Access logs will show the real client IP instead of Cloudflare's edge server IPs.
Cloudflare-Only Access
Reject requests that reach your origin directly instead of through Cloudflare's proxy:
{
acme_dns cloudflare {env.CLOUDFLARE_API_TOKEN}
servers {
trusted_proxies cloudflare
client_ip_headers Cf-Connecting-Ip
}
}
example.com {
@notCloudflare vars {vars.client_ip} {remote_host}
abort @notCloudflare
reverse_proxy localhost:8080
}
With this setup:
- Caddy only reads
Cf-Connecting-Ipwhen the connecting IP is in Cloudflare's published ranges, whichcaddy-cloudflare-ipkeeps up to date. For any other connection the client IP equals the connecting IP, so the matcher catches it. No hardcoded IP list is needed (approach suggested on the Caddy forum). - Direct requests have their connection closed. Use
respond @notCloudflare 403instead ofabortif you prefer an explicit error. - Certificate issuance is unaffected, since the DNS-01 challenge does not use inbound HTTP.
- Not for Cloudflare Tunnel setups: tunneled traffic arrives from your local
cloudflared, not Cloudflare's IPs, so every request would be rejected.
v2.11.0 or later (see Tags). Add the matcher to each site block you want to protect. This checks the source IP only; for stronger guarantees, also restrict ports 80/443 to Cloudflare at your firewall, or use Authenticated Origin Pulls.
Rate Limiting
Limit how many requests each visitor can make, keyed on their real IP rather than Cloudflare's:
{
acme_dns cloudflare {env.CLOUDFLARE_API_TOKEN}
order rate_limit before basic_auth
servers {
trusted_proxies cloudflare
client_ip_headers Cf-Connecting-Ip
}
}
example.com {
rate_limit {
zone per_client {
key {client_ip}
events 100
window 1m
}
}
reverse_proxy localhost:8080
}
With this setup:
- Each client gets 100 requests per minute; further requests receive
429 Too Many Requestswith aRetry-Afterheader. {client_ip}resolves to the visitor's IP viatrusted_proxies cloudflare. Without it, every visitor behind the same Cloudflare edge would share one limit.order rate_limit before basic_authis required becauserate_limitis not a standard Caddy directive.
rate_limit is not a registered directive; check yours with the command in Verifying Modules. See the module documentation for multiple zones, request matchers, and distributed limits.
Configuration
Creating a Cloudflare API Token
To use the Cloudflare DNS challenge provider, create an API token with the necessary permissions:
- Log in to Cloudflare at dash.cloudflare.com.
- Navigate to API Tokens:
- Create a Custom Token:
- Configure Token Permissions:
Caddy DNS-01 Challenge (or similar)
- Permissions:
- Zone - Zone - Read: Allows reading DNS zones.
- Zone - DNS - Edit: Allows editing DNS records (required for DNS-01 challenge).
- Specify Zone Resources:
- Create and Store the Token:
- Set the Environment Variable:
CLOUDFLARE_API_TOKEN to your deployment environment. See Quick Start for the docker-compose example.
ACME DNS Challenge
To configure the ACME DNS challenge provider for all ACME transactions, add the following to your Caddyfile:
{
acme_dns cloudflare {env.CLOUDFLARE_API_TOKEN}
}
This is equivalent to specifying the provider in the tls directive's ACME issuer configuration. See Caddyfile Examples for full samples.
Tags
The image is available on GitHub Container Registry and Docker Hub with the following tags:
latest-- Most recent stable release with the Cloudflare DNS module.alpine-- Most recent stable release of the Alpine-based image.-- Full version pin (e.g.,docker pull ghcr.io/caddybuilds/caddy-cloudflare:X.Y.Z).-- Latest patch within a minor series (e.g.,. 2.11).-- Latest release within a major series (e.g.,2).,-alpine ,. -alpine -- Alpine variants of the version tags above (e.g.,-alpine X.Y.Z-alpine,2.11-alpine,2-alpine).
Note: Starting fromv2.11.0, the image includes thecaddy-cloudflare-ipandcaddy-combine-ip-rangesmodules by default.
Why did a digest change?
Images are rebuilt without a new Caddy version when a module gets new commits or the official Caddy image is republished with OS or Go fixes. Each rebuild is logged under Rebuilds in that version's GitHub Release, and the reason is stored in the image labels:
docker inspect --format '{{ index .Config.Labels "caddy-build.reason" }}: {{ index .Config.Labels "caddy-build.details" }}' ghcr.io/caddybuilds/caddy-cloudflare:latest
Platform Support
The image is built for the following platforms:
- linux/amd64 -- Standard x86_64 (desktops, servers)
- linux/arm64 -- ARM 64-bit (modern ARM servers, Apple Silicon via emulation)
- linux/arm/v7 -- ARM 32-bit (Raspberry Pi 3+)
- linux/ppc64le -- 64-bit PowerPC Little Endian
- linux/s390x -- 64-bit IBM System z
Alpine
The Alpine-based image uses Alpine Linux as its base, resulting in a significantly smaller image. Alpine uses musl libc instead of glibc, which may cause compatibility issues with some software. Additional tools (git, bash, etc.) are not included by default.
Raspberry Pi
The linux/arm/v7 build supports Raspberry Pi 3 and later. No special configuration is needed beyond having Docker installed on a compatible OS (such as Raspberry Pi OS).
Troubleshooting
DNS Resolution Errors
If you encounter solving challenges: presenting for challenge: adding temporary record for zone xyz.: got error status: HTTP 403, try setting custom DNS resolvers:
tls {
dns cloudflare {env.CLOUDFLARE_API_TOKEN}
resolvers 1.1.1.1
}
Verifying Modules
To confirm the bundled modules are loaded:
docker run --rm ghcr.io/caddybuilds/caddy-cloudflare:latest caddy list-modules | grep -E "cloudflare|combine|rate_limit"
Official Resources
How It Works
The build pipeline is fully automated via GitHub Actions:
- Daily release check -- A scheduled workflow queries the GitHub API for new Caddy releases and verifies the official Docker image is available with all required platforms before triggering a build.
- Module update detection -- The pipeline tracks the latest commit SHA of each included module in
module-versions.json. If any module has new commits, a rebuild is triggered even without a new Caddy release.
- Base image tracking -- Docker Official Images republish existing Caddy versions for base OS and Go toolchain fixes. The check compares when the official image was last pushed with when ours was built, and rebuilds if upstream is newer.
- Multi-platform build -- A reusable workflow handles both standard and Alpine builds, producing images for all supported architectures. Images are pushed to GitHub Container Registry (primary) and optionally Docker Hub.
- Fork-friendly -- Image names are derived from the
GITHUB_REPOSITORYenvironment variable, so forks produce correctly named images without any configuration changes.
- Failure notifications -- If a build fails, a GitHub issue is automatically created for visibility.
Building Your Own Image
Prerequisites
- GitHub account
- Docker Hub account (optional, for pushing to Docker Hub)
- GitHub Secrets configured:
GITHUB_TOKEN (automatically available in GitHub Actions)
- DOCKERHUB_USERNAME (optional)
- DOCKERHUB_TOKEN (optional)
- DOCKERHUB_REPOSITORY_NAME (optional, to customize the Docker Hub repository name)
Setup Instructions
- Fork this repository to your GitHub account.
- Set up GitHub Secrets (optional, for Docker Hub):
Settings > Secrets and variables > Actions.
- Add DOCKERHUB_USERNAME, DOCKERHUB_TOKEN, and optionally DOCKERHUB_REPOSITORY_NAME.
- Enable GitHub Actions:
Actions tab. If prompted, click "I understand my workflows, go ahead and enable them".
- Trigger a build (optional):
Actions tab, select Build and Push Standard Docker Image (or the Alpine variant) and click Run workflow.
- Pull your image:
docker pull ghcr.io/YOUR_GITHUB_USERNAME/YOUR_REPOSITORY_NAME:latest
Use the image in your docker-compose setup as shown in the Quick Start, replacing the image name with your own.
Contributing
Feel free to open issues or submit pull requests if you have any improvements or bug fixes.
License
This project is licensed under the MIT License. See the LICENSE file for details.