I run a lot of Caddy. Almost everything public-facing in my homelab sits behind it, and the thing I lean on hardest is automatic TLS over the DNS-01 ACME challenge — the one challenge type that works for split-horizon and internal-only hostnames, because it never needs an inbound connection to prove ownership. The catch is that DNS-01 needs a provider plugin compiled into the Caddy binary, and the official Docker image ships without any of them.

So I maintain cmunroe/caddy-dns — prebuilt, multi-arch Caddy images with the DNS plugins already baked in, one image per provider. When upstream Caddy ships a release, I rebuild the whole matrix. Caddy 2.11.4 dropped on June 3rd, so the images are rebuilt and pushed. This is the what-changed-and-how-to-use-it.


What's in 2.11.4

This is a security-forward patch release. The Caddy maintainers landed four security-related fixes, plus a patch for an advisory (GHSA-vcc4-2c75-vc9v) and a stack of normal bugfixes. FrankenPHP collaborated on the PHP-adjacent pieces.

The security-related patches:

  • caddyhttp: normalize Windows backslashes in the path matcher — closes a path-traversal gap where \ could sneak past matchers that only expected /.
  • rewrite: prevent placeholder re-expansion in an injected query — stops a tampered request from getting its placeholders re-evaluated, a classic injection foothold.
  • templates: a more reliable stripHTML — better removal of malformed HTML tags so sanitization can't be trivially bypassed.
  • caddyhttp: ignore header fields with underscoresX_Forwarded_For and X-Forwarded-For are distinct on the wire but easily conflated downstream; underscore headers are now dropped to prevent that collision.

There's also a healthy round of non-security fixes worth knowing about:

  • TLS client-auth fix, plus a fix for TLS state races and ECH rotation retry.
  • Reverse proxy no longer closes the request body on dial errors, and wraps the body so it isn't closed if it was never read — both were sources of spurious body closed errors.
  • caddy start now works on IPv6-only hosts.
  • IDN SNI now matches in connection policies (and pure-ASCII SNI skips the IDNA round-trip, a small perf win).
  • Content negotiation now prioritizes zstd and br over gzip — you may see a different Content-Encoding on responses after upgrading.

The one caveat: read this before you `pull`

The release notes carry an explicit warning, and I'll repeat it here because it's the kind of thing that bites you at 2am:

⚠️ These security patches may be breaking if your application relies on the buggy behaviors.

Concretely: if anything in your stack depended on underscore headers surviving, on backslashes being treated as literal path characters, or on the old gzip-first negotiation order, 2.11.4 will change that behavior. None of it hit my configs, but if you proxy to an app that reads X_Forwarded_*-style headers, test before you roll it to production.

An aside the maintainers added

Buried in the notes is a candid line: the recent surge of patch releases is partly because the team is now rejecting more than 75% of "security" reports as AI slop — lazy or incorrect LLM-generated spam — and has started blocking accounts that fire them off. It's a small window into the tax that low-effort agent output is putting on open-source maintainers. Use your robots responsibly; the people on the other end are real.


How to use the images

Every provider ships two tags — a version-pinned one and a latest:

docker pull cmunroe/caddy-dns:cloudflare-2.11.4
docker pull cmunroe/caddy-dns:cloudflare-latest

Swap cloudflare for any of the 47 supported providers — the full table lives in the repo README and on Docker Hub. Every image is built for both linux/amd64 and linux/arm64, so the same tag runs on a Raspberry Pi or a beefy x86 node without thinking about it.

The Caddyfile side is the standard DNS-01 block. For Cloudflare with a scoped API token:

internal.example.com {
    tls {
        dns cloudflare {env.CF_API_TOKEN}
    }
    respond "hello over real TLS"
}

Drop that token in as an environment variable and Caddy handles the rest — provisioning the cert via a temporary _acme-challenge TXT record and renewing it on its own:

# compose snippet
services:
  caddy:
    image: cmunroe/caddy-dns:cloudflare-2.11.4
    environment:
      CF_API_TOKEN: ${CF_API_TOKEN}   # Zone:Read + DNS:Edit, scoped to the zone
    ports: ["443:443", "443:443/udp"]  # /udp for HTTP/3
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      - caddy_data:/data

Each provider folder in the repo has its own README with the exact credential setup, env var names, and provider-specific gotchas (resolvers, propagation timeouts, the works).


Why some providers aren't in the list

A quick reality check, because people ask: not every caddy-dns plugin compiles against current Caddy. The libdns library had a breaking API change — the old libdns.Record struct fields are gone — and any plugin that hasn't caught up simply won't build. So the image matrix is the set that actually compiles and runs on 2.11.x, not a wishlist.

That's why you'll see ~47 enabled and a longer list of disabled ones in the README, each annotated with why (outdated Caddy dependency, old acmez v1/v2 API, abandoned upstream, or just an empty repo). dnspod is the instructive one: its go.mod even targets the right Caddy version, but its libdns dependency still uses the removed record fields, so it can't compile. If a provider you need is on the disabled list and its upstream gets updated, open an issue — re-enabling is usually a one-line change and a rebuild.


Upgrading

If you're already on a -latest tag, it's just a re-pull and a restart. If you pin versions (I do, in production), bump 2.11.32.11.4, re-read the breaking caveat above, and roll it out. The certs and ACME account carry over untouched — the DNS plugin and challenge flow are unchanged in this release; everything new is in Caddy's core HTTP, TLS, and security layers.

Short version: same images, same usage, a meaningfully more hardened Caddy underneath.