Architecture

Caddy Architecture: How It Actually Works

caddyserver/caddy Apache-2.0 1 diagram
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.

caddyserver/caddy module map: the 10 largest of 39 detected modules with call-weighted edges, generated by Symvanta
Module map of caddyserver/caddy, generated by Symvanta. Link to this diagram Open full size

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.

  1. changeConfig (caddy.go:158) takes rawCfgMu, verifies the If-Match hash against the current config, applies the mutation, and indexes any @id fields before reloading.
  2. unsyncedDecodeAndRun (caddy.go:337) strips the meta fields, decodes strictly into a Config, refuses a recursive config load, then swaps the running context and stops the old one.
  3. run (caddy.go:419) provisions the context, starts each app in turn, and rolls back the apps it already started when one fails.
  4. provisionContext (caddy.go:484) opens the loggers, resolves the storage module, replaces the local admin server, and loads every app in AppsRaw.
  5. Context.LoadModule (context.go:188) reflects over the struct field, reads the caddy: tag for its namespace and inline_key, and dispatches on the field's kind.
  6. 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.
  7. LoadModuleByID (context.go:364) looks that ID up in the registry RegisterModule filled at init, calls the module's New, unmarshals the JSON into it, and provisions it.
  8. 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.

See your own codebase mapped like this. Free for 7 days, no credit card.

Start free trial →

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

Get this for your codebase →