Caddy Architecture: How It Actually Works
How to read this page
Symvanta parsed this repository into a code graph: every function, class, and method is a node, and every call or import between them is an edge. Everything on this page is computed from that graph at the commit shown above. The terms:
- Module (or cluster)
- A group of symbols that call each other far more than they call anything else. An algorithm called Louvain community detection finds these groups from the call traffic alone; nobody draws them by hand.
- Modularity (the Q number)
- A 0-to-1 score of how cleanly those groups separate. Higher means more call traffic stays inside its own group; scores around 0.7 and above read as clean boundaries.
- Hub
- The most depended-upon symbol inside one module.
- Load-bearing symbols
- PageRank, the algorithm Google originally used to rank web pages, run over the call graph instead: it surfaces the functions the rest of the codebase leans on hardest.
- Arrows and their numbers
- How many calls cross from one module into another. A heavier arrow means tighter coupling between those two parts.
- Dependency cycle
- File A imports B, which imports A again, sometimes through a longer loop. Cycles are not bugs, but a change inside one tends to ripple around the whole loop.
- Mutually recursive symbols
- Functions that call each other, usually the natural shape of parsers and tree-walking code.
Caddy is a web server built as a module system: a small core that parses
config, loads modules, and swaps configurations at runtime, with the HTTP
server, reverse proxy, and TLS automation all plugged in as modules. The
graph at 45ba327 shows that split directly. Symvanta detects 39 functional
modules (modularity Q=0.81), and the biggest clusters are the config
machinery itself: the root caddy package around the Log accessor (467
symbols), the Caddyfile token dispenser (428), and the module loader that
provisions everything a config names (417).
That is the structural signal: in most servers the request path dominates the
map, and in Caddy the config path does. Next, LoadModule, and
RegisterModule all rank in the PageRank top 10 because every plugin in the
repo goes through them. The mutually recursive symbols tell the same story:
the largest group is the config-swap cycle in caddy.go (changeConfig,
run, provisionContext, finishSettingUp).
Module map
The diagram shows the 10 largest of the 39 detected modules, with edges
weighted by how many calls cross between them. The biggest holds 467 symbols
and its hub is Log, the global logger accessor in logging.go that every
other cluster reaches for. The caddytest harness (204 symbols, hub
NewTester in caddytest/caddytest.go) ranks eighth by size and is left out
as scaffolding. Module names are checked by hand against the files their
members live in; the symbol counts, hubs, and edge weights are what the graph
computed.
Where to start reading
These are the load-bearing entry points: the PageRank ranking over the call
graph, blended with the hub symbol of each of the largest clusters. Five of
those entries are left out below. Name is a struct field the graph groups
across a dozen types (httpcaddyfile.App, caddypki.CA, cmd.Command),
key is a private map helper in internal/filesystems/map.go,
resetDynamicHosts is defined in
modules/caddyhttp/reverseproxy/dynamic_upstreams_test.go and only the
reverse proxy suite calls it, and filtered and Filter are internals of the
log filter modules in modules/logging. They rank high without pointing
anywhere useful.
Key subsystems
Core logging and network addresses
The root caddy package surface: the Log global logger accessor in
logging.go, network address parsing and joining in listeners.go
(JoinNetworkAddress, IsUnixNetwork), and the config adapter registry in
caddyconfig/configadapters.go. 467 symbols, the largest cluster in the repo,
and its heaviest edge runs into the module loader (24 calls).
Caddyfile dispenser
The token reader in caddyconfig/caddyfile/dispenser.go. Next, Val,
NextArg, and ArgErr are how every Caddyfile directive walks its own
arguments. 428 symbols, and its edges fan out to the httpcaddyfile adapter (24
calls), the core, and the directive registry.
Module loading and TLS provisioning
Context.LoadModule, LoadModuleByID, and loadModuleInline in context.go:
the reflection walk that turns raw JSON config into provisioned module
instances. The graph clusters it with the caddytls automation code it
provisions, 417 symbols in all, and its heaviest edge runs into the core
cluster (44 calls).
Placeholder replacer
Replacer.replace and NewReplacer in replacer.go, the {placeholder}
substitution engine every other layer reaches for. 348 symbols. Placeholders
resolve inside request handling, so the cluster's heaviest edge points at the
HTTP server internals (28 calls).
HTTP server internals
modules/caddyhttp/server.go: request serving, trusted proxy resolution
(determineTrustedProxy), and the header alias provisioning a server runs at
startup. 297 symbols, hub provisionHeaderAliasAllowlist.
Module descriptors and plugin types
The ModuleInfo descriptor in modules.go plus the plugin config structs that
declare one, from Argon2idHash in caddyauth to LeafFileLoader in caddytls.
222 symbols. This is the shape of a plugin catalog: types that exist to be
registered and unmarshaled into.
httpcaddyfile adapter
caddyconfig/httpcaddyfile: the adapter that turns a parsed Caddyfile into
JSON config, including the automation policy consolidation in tlsapp.go
(subjectQualifiesForPublicCert, automationPolicyIsSubset). 220 symbols,
and it reads its input through the dispenser (29 calls).
Context and metrics
NewContext in context.go with the metrics registry and the instrumented
route wrappers in modules/caddyhttp/metrics.go. 178 symbols.
Shared infrastructure
Two smaller clusters round out the diagram: the reverse proxy upstreams (167
symbols, whose hub resetDynamicHosts is defined in
dynamic_upstreams_test.go, which is what a hub looks like when the test
suite drives every dynamic-upstream path) and response encoding (162 symbols,
the Accept-Encoding negotiation and compressing response writer in
modules/caddyhttp/encode).
Canonical request flow
The sequence worth reading first is a config apply: what happens when a new configuration reaches the admin API. Take the config lock, check the If-Match hash, decode the JSON, provision a fresh Context, load every module the config references, start the apps, then finish setting up the admin endpoint and config loaders. Every step below is a call edge in the config-swap cycle, in the order the work happens.
changeConfig(caddy.go:158) takesrawCfgMu, verifies the If-Match hash against the current config, applies the mutation, and indexes any@idfields before reloading.unsyncedDecodeAndRun(caddy.go:337) strips the meta fields, decodes strictly into aConfig, refuses a recursive config load, then swaps the running context and stops the old one.run(caddy.go:419) provisions the context, starts each app in turn, and rolls back the apps it already started when one fails.provisionContext(caddy.go:484) opens the loggers, resolves the storage module, replaces the local admin server, and loads every app inAppsRaw.Context.LoadModule(context.go:188) reflects over the struct field, reads thecaddy:tag for its namespace andinline_key, and dispatches on the field's kind.loadModuleInline(context.go:478) pulls the module name out of the raw JSON object and joins it to the namespace to form a module ID.LoadModuleByID(context.go:364) looks that ID up in the registryRegisterModulefilled at init, calls the module'sNew, unmarshals the JSON into it, and provisions it.finishSettingUp(caddy.go:594) establishes the server identity, replaces the remote admin endpoint, and starts the dynamic config loader.
Health signals
Symvanta detected 0 dependency cycles across 39 modules (modularity Q=0.81). 6 sets of mutually recursive symbols were also detected, the largest being caddy (9 symbols). The recursion that exists sits in the config path: finishSettingUp loads a config through LoadConfig and hands it back to changeConfig, which closes the loop through run and provisionContext.
Auto-generated by Symvanta from the public repo caddyserver/caddy at commit 45ba327 , licensed Apache-2.0 .
Machine-readable companion: data.json (module counts, subsystems, load-bearing symbols, health signals).