Profile
Back to NewsBack
GitHub Trending 10 min
Reader Mode
CaddyBuilds/caddy-cloudflare: Caddy Docker image with Cloudflare DNS module

CaddyBuilds/caddy-cloudflare: Caddy Docker image with Cloudflare DNS module

6 hours ago

Caddy Cloudflare

Build</a> License: MIT</a>

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 Challenge

Features

  • 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, and s390x.
  • 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 ensures docker-compose down does not delete it. Create it manually with docker 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-For headers.
  • 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-Ip when the connecting IP is in Cloudflare's published ranges, which caddy-cloudflare-ip keeps 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 403 instead of abort if 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.
Requires an image from 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 Requests with a Retry-After header.
  • {client_ip} resolves to the visitor's IP via trusted_proxies cloudflare. Without it, every visitor behind the same Cloudflare edge would share one limit.
  • order rate_limit before basic_auth is required because rate_limit is not a standard Caddy directive.
Older images do not include this module and will fail with 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:

  1. Log in to Cloudflare at dash.cloudflare.com.
  1. Navigate to API Tokens:
- Click your profile icon > "My Profile" > "API Tokens" in the sidebar.
  1. Create a Custom Token:
- Click "Create Token", then under "Custom Token" click "Get started".
  1. Configure Token Permissions:
- Name: 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).
  1. Specify Zone Resources:
- Include - All Zones: For access to all your zones, or - Include - Specific Zone: Select only the zone(s) you need.
  1. Create and Store the Token:
- Review your settings, click "Create Token", and copy the generated token to a secure location.
  1. Set the Environment Variable:
- Add 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 -- Alpine variants of the version tags above (e.g., X.Y.Z-alpine, 2.11-alpine, 2-alpine).
Note: Starting from v2.11.0, the image includes the caddy-cloudflare-ip and caddy-combine-ip-ranges modules 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:

  1. 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.
  1. 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.
  1. 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.
  1. 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.
  1. Fork-friendly -- Image names are derived from the GITHUB_REPOSITORY environment variable, so forks produce correctly named images without any configuration changes.
  1. 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

  1. Fork this repository to your GitHub account.
  1. Set up GitHub Secrets (optional, for Docker Hub):
- Navigate to Settings > Secrets and variables > Actions. - Add DOCKERHUB_USERNAME, DOCKERHUB_TOKEN, and optionally DOCKERHUB_REPOSITORY_NAME.
  1. Enable GitHub Actions:
- Go to the Actions tab. If prompted, click "I understand my workflows, go ahead and enable them".
  1. Trigger a build (optional):
- In the Actions tab, select Build and Push Standard Docker Image (or the Alpine variant) and click Run workflow.
  1. 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.

Chat with me