Skip to content

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.

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).

Settings → Caddy BuildNothing saves here, so the build stays current and Rebuild is never offered.DemoNothing you change here is saved
Plugins are compiled into Caddy
Turning a module off stops this app from generating config for it right away. Removing it from the binary - and adding a new one - needs a rebuild, which restarts the proxy.

Proxying

2/2
TCP/UDP proxying. Required by L4 Proxy Hosts and by the agent that binds their ports.
Runs Tailscale inside Caddy. Required to serve a proxy host on your tailnet, to gate one on Tailscale identity, and to reach an upstream over the tailnet.

HTTP caching

0/6
A shared HTTP cache (Souin) in front of upstreams. Enables the Caddy cache mode of a proxy host's Cache assets option.
An in-memory store for the HTTP cache, bounded by entry count and faster than the built-in one. Entries are lost on restart.
Keeps the HTTP cache on disk in a Badger database on the Caddy data volume, so it survives a restart.
Keeps the HTTP cache as plain files on the Caddy data volume, so it survives a restart.
Keeps the HTTP cache in Redis or Valkey, shared by every Caddy that points at the same server.
Keeps the HTTP cache in an etcd cluster, shared by every Caddy that points at it. No authentication.

Security

2/4
Country, continent, ASN, and CIDR blocking. Required by global geoblocking and by per-host geoblock rules.
ModSecurity-compatible web application firewall with the OWASP Core Rule Set. Required by the WAF settings and the WAF events page.
Sliding-window request limits per client. Required by a proxy host's Rate limiting option.
A CrowdSec bouncer: refuses clients your CrowdSec Local API has banned, on proxy hosts and L4 hosts, with optional AppSec. Required by the CrowdSec settings.

ACME DNS-01 providers

22/22
Cloudflare DNS API
github.com/caddy-dns/route53
AWS Route 53 DNS API (supports IAM roles when fields are empty)
DigitalOcean DNS API
Duck DNS dynamic DNS service
Hetzner DNS API
Vultr DNS API
Porkbun DNS API
GoDaddy DNS API
Namecheap DNS API
OVH DNS API
IONOS DNS API
github.com/caddy-dns/linode
Linode/Akamai DNS API
Njalla DNS API
Spaceship DNS API
deSEC DNS API
Dynu DNS API
acme-dns delegated DNS-01 validation (dedicated ACME challenge records only)
Infomaniak DNS API
INWX DNS API
netcup CCP DNS API
ClouDNS DNS API
github.com/caddy-dns/rfc2136
RFC 2136 dynamic DNS updates via a TSIG key (BIND 9 and compatible servers)

Custom modules

0/0
Any Caddy plugin published as a Go module. Compiled from source at build time, so an unreachable or non-building module fails the rebuild - the running container is left untouched when that happens. Tag, branch, or commitNo custom modules.
26 modules selected. The agent builds this image when you rebuild. To build it yourself instead, set CADDY_BUILD_MODE=external on the agent, which then needs no Docker build access.

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.

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.

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 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.

  1. Run the command, on the agent’s host or anywhere else. Push the image to a registry if it was built elsewhere.
  2. The first time, set CADDY_IMAGE to its tag in .env and run docker compose up -d agent. Use a tag of your own, not the shipped image’s name.
  3. 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.

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.

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_access and waf_rules logs, 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.