The agent
The agent runs beside Caddy on each host. It exists because the controller has no Docker socket, and some things need the Caddy container recreated rather than its configuration reloaded.
Why it is a separate container
Section titled “Why it is a separate container”Two things are fixed when a container is created and cannot be changed by reloading config:
- Published ports, which L4 hosts need.
- Compiled-in plugins, which Caddy Build changes - by building the image,
or with
CADDY_BUILD_MODE=externalonly loading one you built.
The agent also starts and stops the optional ClickHouse container for analytics and, in managed mode, the CrowdSec one, so each becomes a toggle in Settings rather than a compose profile you edit by hand.
It is also how the controller sees what only the Caddy host has:
- the access, WAF and Caddy logs for the log viewer;
- counts of the access log’s 502, 503 and 504 answers per host, for the upstream error notification, sent with analytics off too;
- Caddy’s certificate storage, for each host’s expiry and the downloads on the Certificates page, read from a throwaway container mounting it read-only;
- PEM files under
CERT_FILES_HOST_DIR, when set, for certificates from files; caddy validateagainst the real binary, in a throwaway container with no network, which checks a WAF save or a global Caddyfile without loading anything.
An agent tells the controller which of these it supports, and an older one is simply not asked. Update it to get them.
The agent dials out
Section titled “The agent dials out”The controller never connects to the agent. The agent connects out and holds an event stream open, and the controller pushes work down it. A Caddy host can therefore sit behind NAT with no inbound port and no port forwarding.
If the controller is unreachable, the agent retries with backoff and keeps Caddy serving whatever it already had. Only a 401 - the controller having forgotten this agent - ends the loop.
Same host
Section titled “Same host”The bundled compose file runs an agent beside the controller. It pairs itself using a token the controller leaves on its data volume, which the agent mounts read-only, so there is nothing to enter. Reach the dashboard on port 3000 to finish setup and Caddy starts on its own.
Its own user
Section titled “Its own user”The agent does not run as root. It keeps its database and its copy of the GeoIP databases on a volume of its own, reaches Docker through a socket proxy, and reads Caddy’s logs through Caddy’s group. If a log turns out unreadable, the WAF audit log cannot be truncated, or Caddy cannot list its log directory to prune old files, the agent’s card on the Agents page shows a warning with the command that fixes it.
A different host
Section titled “A different host”An agent elsewhere cannot read that volume, so it pairs with a code you carry. Generate one under Settings → Agent - six letters, valid five minutes, single use, at most five wrong guesses a minute per address - then run on the agent’s host:
docker exec -it caddy-proxy-manager-agent cpm-agent --pair --host https://cpm.example.com --code ABCDEFThe agent first asks the controller its name - without spending the code - and waits for you to confirm it:
This agent is about to pair with "Caddy Proxy Manager" (controller 3f9a1c2e) at https://cpm.example.com:443Confirm pairing? [y/N]The name is the controller’s Application name - the one its sidebar shows. Answering no changes
nothing and the code stays valid. The prompt needs a terminal, which is what -it is for; a script
can pass --yes instead.
A bare host means https://. Plain http:// works towards a private address with a warning and is
refused towards a public one unless CONTROLLER_ALLOW_INSECURE_HTTP=true, because the secret
travels over this link. The two exchange a secret - encrypted at rest on the controller, kept in the agent’s own state
volume - and the code is never used again. An agent that is already paired re-pairs only with a code from Re-pair on its
row. CONTROLLER_URL and PAIRING_CODE do the same without a terminal, on an agent that is not
yet paired; a stored pairing wins over them.
Fleets
Section titled “Fleets”Any number of Caddy hosts can serve one configuration. Every apply goes to all of them, and if any host refuses the config or cannot be reached the apply fails and names it - the change is not recorded as applied until every host has it, so the disagreement is visible rather than silent.
Hosts can also be pinned to specific agents, so one machine serves a subset rather than everything.
- bundledConnected
- edge-fraConnectedOwn Caddy build
- edge-sydNot connected
Unpairing
Section titled “Unpairing”Unpairing revokes the secret. The agent’s next call is refused, it drops back to idle, and it stops Caddy - so unpairing takes that host out of service. Pair it again with a fresh code to bring it back. A stream that goes silent for three keepalives (60 seconds) is reconnected, so an agent whose stream stayed open after the controller closed it still notices within a minute.
Stopping the agent
Section titled “Stopping the agent”The agent owns Caddy on its host, so stopping the agent stops Caddy first - docker compose stop agent, a host shutdown, anything that sends it SIGTERM. It gives Caddy 40 seconds, inside
the minute the bundled compose file allows the agent to exit. When the agent starts again with a
pairing it starts Caddy straight away, without waiting for the controller, so a host that reboots
while its controller is unreachable still serves; the controller’s own setting still wins once it
answers. A restart the controller asks for, after a migration, keeps Caddy running.