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 reliablestripHTML— better removal of malformed HTML tags so sanitization can't be trivially bypassed.caddyhttp: ignore header fields with underscores —X_Forwarded_ForandX-Forwarded-Forare 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 closederrors. caddy startnow 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
zstdandbrovergzip— you may see a differentContent-Encodingon 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.3 → 2.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.