Caddy Build
Caddy is a single static binary. A plugin either was compiled in with xcaddy or it does not exist at runtime - there is no loading one later. Settings → Caddy Build makes that list editable from the dashboard.
The default image ships with every supported module except the opt-in ones - Rate Limit, CrowdSec and the HTTP caching modules - so an existing install behaves exactly as it did before this page existed.
Choosing modules
Section titled “Choosing modules”Each supported plugin has a toggle: Layer 4 Proxy, Tailscale, Request Blocker, Coraza WAF, and every DNS provider. Turning one off does two things:
- The app stops generating config that uses it, immediately. The handler simply stops being emitted, which is what lets you remove a plugin without Caddy rejecting the stored config on the way out.
- Every setting that depends on it is disabled, with a tooltip naming the module. Geo blocking follows Request Blocker, the WAF pages follow Coraza, L4 hosts follow caddy-l4, tailnet options follow caddy-tailscale, and each DNS provider follows its own module - so disabling Cloudflare leaves Route 53 alone.
Rate Limit (caddy-ratelimit), CrowdSec (caddy-crowdsec-bouncer) and the HTTP caching modules are the ones the shipped image leaves out: each is off until you turn it on and rebuild. Rate Limit unlocks a host’s Rate limiting. CrowdSec unlocks CrowdSec; it is one module for proxy hosts, AppSec and L4 hosts alike, and because its L4 part is built on caddy-l4, selecting it compiles caddy-l4 in even with Layer 4 Proxy switched off. HTTP Cache unlocks the Caddy cache mode of a host’s Cache assets; the storage modules beside it give that cache somewhere to live other than memory (see HTTP cache storage).
Proxying
2/2HTTP caching
0/6Security
2/4ACME DNS-01 providers
22/22Custom modules
0/0You cannot turn off something in use
Section titled “You cannot turn off something in use”A module still being used cannot be switched off. The save is refused and names what is using it - “3 enabled L4 proxy hosts need the Layer 4 Proxy module”. That check covers global and per-host WAF and geo blocking, CrowdSec while it is switched on, enabled L4 hosts, hosts served on the tailnet, and every DNS provider with credentials on file. Turn the feature off first.
Rate Limit and the HTTP caching modules are the exception: turning one off is allowed. Hosts with rate limiting are served without limits, hosts in Caddy cache mode fall back to browser caching, and a storage falls back to memory.
Per-host custom Caddyfile snippets cannot be checked the same way: they are free-form text, and only Caddy’s adapter knows what a directive resolves to - for the binary running now rather than the one a rebuild would produce. Saving a module change while any host has a snippet adds an advisory note listing those hosts so you can review them first.
Your own modules
Section titled “Your own modules”Any Go module xcaddy can fetch can be added by import path, optionally pinned to a tag, branch or commit. It is compiled from source at build time.
Rebuilding
Section titled “Rebuilding”Saving a changed selection starts the rebuild - in Settings and over PUT /api/v1/caddy/modules
alike. The agent runs the build and recreates the Caddy container when it finishes,
reporting progress as it goes. The running Caddy keeps serving until the new binary is ready.
Rebuild retries a build that failed; an agent that was offline builds when it next connects.
Building the image yourself
Section titled “Building the image yourself”Building needs BuildKit access through the Docker socket proxy. To keep that away from the agent,
set CADDY_BUILD_MODE=external on it and GRPC: 0 and SESSION: 0 on docker-socket-proxy. The
agent then never builds: the page’s Build command is a docker build for your selection, and
Load built image takes the Rebuild button’s place. The command builds from this release’s tag
on GitHub, so it runs anywhere Docker can build, without a checkout. In a fleet where only some
agents are external, Rebuild leaves those alone; pick one under Module selection for to load
its image.
- Run the command, on the agent’s host or anywhere else. Push the image to a registry if it was built elsewhere.
- The first time, set
CADDY_IMAGEto its tag in.envand rundocker compose up -d agent. Use a tag of your own, not the shipped image’s name. - Click Load built image. The agent pulls the tag if it is in a registry and reads the new image’s module list first. If Caddy has a module the image lacks, the controller pushes a config without it before anything restarts, since Caddy would not start on a saved config naming it. Then the agent recreates Caddy if the tag names a different image, and waits for it to report healthy.
The agent learns which modules the image has from /etc/caddy/caddy-modules.txt, which the
Dockerfile writes. An image built another way has no such file and is treated as having no
plugins, so the config stays loadable and every feature needing a module stays off.
HTTP cache storage
Section titled “HTTP cache storage”Settings → Caddy Build → HTTP Cache picks where the Caddy cache keeps entries. Memory is built in; each other storage is its own module, and can be chosen once that module is selected above:
| Storage | Kept | Shared between Caddys |
|---|---|---|
| Memory | In memory, lost on restart | No |
| Otter | In memory, bounded by entry count | No |
| Badger | On the Caddy data volume | No |
| SimpleFS | Files on the Caddy data volume | No |
| Redis | A Redis or Valkey server, with optional ACL user, password and database | Yes |
| etcd | An etcd cluster that does not require authentication | Yes |
Redis and etcd servers are dialed by Caddy, so they have to be reachable from the Caddy container. A storage Caddy cannot reach, or one whose module is not compiled in yet, leaves the cache in memory rather than taking hosts down; Caddy’s log says why.
NATS, Olric and NutsDB have Souin storages too but are not offered: with the current cache handler the first two are silently replaced by memory, and NutsDB stalls on reads.
CDN purging tags cached responses for a CDN in front of Caddy and purges it when Souin invalidates an entry. Cloudflare takes the account’s Global API Key, its email and the zone ID; Fastly takes an API token, the service ID and a soft or hard purge. Both keys are stored encrypted.
A cache block in the global Caddyfile still works while this is left at memory with no CDN.
Once either is set, CPM writes the cache app itself and the global one is refused by name.
Global Caddyfile
Section titled “Global Caddyfile”Settings → Caddy Build → Global Caddyfile takes raw Caddyfile text - a global options block, site blocks, or both - and adds it to every agent’s config. Each agent’s Caddy adapts it, so a directive from a module that agent was built with works there.
It can only add to what CPM generates. CPM rebuilds the whole config on every change, so anything that would replace part of it is refused by name rather than merged:
- the admin API and storage - the agent drives the first and reads certificates from the second;
- TLS certificate automation, and any other setting CPM already writes;
- ports 80 and 443, and every other port a CPM server listens on;
- the
http_accessandwaf_ruleslogs, and the layer 4 and Tailscale apps.
A site block on a port of its own gets that port inside the Caddy container; publish it in
docker-compose.yml to reach it from outside. A save is checked by every connected agent’s Caddy,
with caddy validate where the agent supports it, and refused with Caddy’s own message. If a saved
one later stops adapting - after a rebuild drops a module, say - it is skipped with a warning in the
controller log and the rest of the config still loads.