Caddy — Architecture#
Architectural style#
Microkernel / Plugin-based
Caddy is a textbook microkernel: a small, stable core hosts a dynamic registry of independently loadable plugins (called “modules”). The core (github.com/caddyserver/caddy/v2) provides:
- A typed module registry (
RegisterModule/GetModule) - A context-scoped module lifecycle (
Provision → Validate → Start → Cleanup) - A JSON config engine that maps JSON fields to module namespaces
- An HTTP admin API for live config mutation
Everything else — HTTP server, TLS automation, reverse proxy, logging, PKI, filesystem abstraction — is a registered module that the core loads and runs. Third-party plugins integrate by calling RegisterModule() in their init() function and are compiled into a custom binary via xcaddy. There is no dynamic linking; the plugin boundary is a Go interface.
Component diagram (textual)#
┌──────────────────────────────────────────────────────────────┐
│ cmd/caddy (binary) │
│ main() → caddycmd.Main() → cobra command dispatch │
└───────────────────────┬──────────────────────────────────────┘
│ caddy.Load(cfgJSON)
┌───────────────────────▼──────────────────────────────────────┐
│ caddyconfig layer │
│ GetAdapter(name) → Adapter.Adapt() → JSON bytes │
│ Adapters: caddyfile, caddyfile/httpcaddyfile, (3rd party) │
└───────────────────────┬──────────────────────────────────────┘
│ JSON bytes
┌───────────────────────▼──────────────────────────────────────┐
│ Core: package caddy (root) │
│ │
│ Config ──► changeConfig() ──► unsyncedDecodeAndRun() │
│ │ │ │
│ │ ▼ │
│ │ provisionContext() │
│ │ ├─ openLogs() │
│ │ ├─ loadStorage module │
│ │ └─ loadModule for each app │
│ │ │ │
│ │ ▼ │
│ │ run() → App.Start() for each app │
│ │ │ │
│ │ ▼ │
│ │ finishSettingUp() │
│ │ ├─ remote admin endpoint │
│ │ └─ config loader/watcher │
│ ▼ │
│ Admin API (localhost:2019) │
│ PATCH /config/… → changeConfig() [live reload] │
│ │
│ Module Registry (global map[string]ModuleInfo) │
│ Populated by init() calls during program startup │
└──────────────┬───────────────────────────────────────────────┘
│ loaded & provisioned at runtime
┌──────────────▼───────────────────────────────────────────────┐
│ modules/ │
│ │
│ caddyhttp (App) │
│ └─ Servers → Routes → MatcherSets → Handler chain │
│ ├─ reverseproxy, fileserver, rewrite, headers… │
│ └─ encode, templates, tracing, caddyauth… │
│ │
│ caddytls (App) │
│ └─ CertMagic integration, ACME, STEK rotation │
│ │
│ caddypki (App) │
│ └─ local CA + embedded ACME server │
│ │
│ caddyevents (App) │
│ └─ event bus for inter-module notifications │
│ │
│ logging, metrics (App) │
│ └─ log sinks, Prometheus exposition │
└──────────────────────────────────────────────────────────────┘Core components#
Module Registry#
- Package:
github.com/caddyserver/caddy/v2(modules.go) - Responsibility: Global, concurrency-safe registry mapping module IDs (dotted namespace strings like
http.handlers.file_server) toModuleInfostructs.RegisterModule()is called from each module’sinit().GetModule()andGetModules()look up registered modules by ID or namespace prefix. - Key types:
Module(interface:CaddyModule() ModuleInfo),ModuleInfo(ID + New constructor),ModuleID(string type with namespace/name methods) - Dependencies: none (pure stdlib)
Config Engine#
- Package:
github.com/caddyserver/caddy/v2(caddy.go) - Responsibility: Owns the single authoritative config (
rawCfg map[string]any).Load()/changeConfig()/unsyncedDecodeAndRun()form the reload pipeline: JSON mutation → JSON marshal → strict unmarshal into*Config→provisionContext()→run()→ swap current context. Implements optimistic concurrency control (If-Match header + config hash for atomic partial updates). - Key types:
Config(top-level JSON struct),App(interface:Start() / Stop()),Context(wrapscontext.Context, owns module instances, manages cleanup) - Dependencies:
context,sync,encoding/json,notify(systemd),certmagic
Admin API#
- Package:
github.com/caddyserver/caddy/v2(admin.go) - Responsibility: Embedded HTTP/1.1 server on
localhost:2019(default). RoutesGET/POST/PUT/PATCH/DELETE /config/…tochangeConfig()with JSON body, enabling surgical live config edits at any path within the config tree. Also routes/id/…(by@idfield),/metrics(Prometheus),/pprof/,/debug/vars, and any paths registered by modules viaAdminRouterinterface. Supports mutual TLS for a second (“remote”) admin endpoint. - Key types:
AdminConfig,AdminRouter(interface modules implement to add admin routes),APIError(structured HTTP error) - Dependencies:
net/http,crypto/tls,certmagic,prometheus
Config Adapter System#
- Package:
github.com/caddyserver/caddy/v2/caddyconfig - Responsibility: Registry of named adapters (same init/RegisterModule pattern) that translate non-JSON formats into Caddy JSON.
GetAdapter(name)retrieves an adapter;Adapter.Adapt(input, opts)returns(jsonBytes, warnings, error). - Key types:
Adapter(interface),Warning(struct with file/line/directive) - Subpackages:
caddyfile/— lexer, parser,Dispenser(token cursor), formatter for the Caddyfile DSLhttpcaddyfile/— HTTP-specific Caddyfile-to-JSON translation: global options, site blocks, routes, matchers, handlers viaRegisterDirective()/RegisterHandlerDirective()
HTTP App (caddyhttp)#
- Package:
github.com/caddyserver/caddy/v2/modules/caddyhttp - Responsibility: Implements
caddy.App. Manages a set ofServerinstances, each bound to listener addresses. Each server has aRouteslist; each route hasMatcherSets(AND/OR logic) andHandlersRaw(middleware chain). At request time, the server walks routes in order, evaluates matchers, and builds a per-request handler stack viawrapRoute(). Implements theMiddlewareHandlerinterface chain terminating in a final handler. - Key types:
App,Server,Route,Middleware(interface: wrapsMiddlewareHandler),MiddlewareHandler(interface:ServeHTTP(ResponseWriter, *Request) error),Handler(interface),Matcher(interface),ResponseRecorder - Dependencies: core
caddy,certmagic(for TLS listener),quic-go(HTTP/3),zap(access logs)
TLS App (caddytls)#
- Package:
github.com/caddyserver/caddy/v2/modules/caddytls - Responsibility: Implements
caddy.App. Wrapscertmagicto provide automatic TLS certificate management: ACME challenges, OCSP, certificate caching, session ticket key (STEK) rotation (both localstandardstekand clustereddistributedstek). ExposesTLSConfig()to the HTTP server for building*tls.Configfrom policy objects. - Key types:
TLS(app),CertificateLoader(interface),ConnectionPolicies,SessionTicketService(interface) - Dependencies:
certmagic,golang.org/x/crypto
PKI App (caddypki)#
- Package:
github.com/caddyserver/caddy/v2/modules/caddypki - Responsibility: Manages one or more internal CAs using
smallstep/certificates. Issues TLS certificates for internal names (.localhost, RFC 1918). Containsacmeserver/subpackage — a full embedded ACME server so internal services can obtain certs via standard ACME protocol from Caddy itself. - Key types:
PKI(app),CA(struct with root/intermediate keypairs) - Dependencies:
smallstep/certificates,smallstep/nosql, corecaddy
Event System (caddyevents)#
- Package:
github.com/caddyserver/caddy/v2/modules/caddyevents - Responsibility: App-level event bus. Modules can emit typed events; handler modules subscribe via the
eventsconfigsubpackage. Decouples modules that need to react to each other’s state changes without direct imports. - Key types:
App(event dispatcher),EventHandler(interface),Event(struct with name, origin, data)
Data flow#
Startup / config load#
1. cmd/caddy/main.go: init() registers all standard modules via blank imports
2. caddycmd.Main() → cobra dispatch → cmdRun()
3. LoadConfig(file, adapter) → reads file → if non-JSON: Adapter.Adapt() → JSON bytes
4. caddy.Load(cfgJSON, forceReload=true)
5. changeConfig(POST, "/config", cfgJSON)
6. rawCfgMu.Lock() → mutate rawCfg map
7. unsyncedDecodeAndRun(newCfgJSON)
8. StrictUnmarshalJSON → *Config
9. provisionContext(newCfg)
10. NewContextWithCause → caddy.Context
11. newCfg.Logging.openLogs(ctx) // set up log sinks
12. ctx.LoadModule(newCfg, "StorageRaw") // provision storage backend
13. for each app in AppsRaw:
14. ctx.LoadModule(newCfg, "AppsRaw") → module.New() + JSON unmarshal + Provision() + Validate()
15. run(newCfg, start=true)
16. ctx.cfg.Admin.provisionAdminRouters(ctx)
17. for each app: app.Start() // starts listeners, goroutines
18. finishSettingUp() // remote admin, config watcher
19. swap currentCtx (atomic pointer swap under currentCtxMu)
20. unsyncedStop(oldCtx) // Stop + Cleanup old apps
21. autosave newCfgJSON to diskLive reload (Admin API)#
PATCH localhost:2019/config/apps/http/servers/myserver/routes/0 {"body": ...}
→ admin HTTP handler → changeConfig(PATCH, path, body)
→ mutate rawCfg at path → re-marshal → unsyncedDecodeAndRun
→ new Context provisioned, new apps started, old apps stopped
→ response: 200 OKHTTP request handling#
net.Listener.Accept()
→ caddytls: wrap with *tls.Config (ALPN negotiates h1/h2/h3)
→ caddyhttp.Server.ServeHTTP()
→ walk Route list:
for each Route:
evaluate MatcherSets (AND within a set, OR across sets)
if match: build handler chain from HandlersRaw
chain = h1(h2(h3(... finalHandler)))
chain.ServeHTTP(w, r)
(each middleware calls next.ServeHTTP or short-circuits)
if no route matched: fallback (404 or pass-through)Initialization / Bootstrap#
Bootstrap follows a strict layered sequence:
Module registration (init phase): All
init()functions run.modules/standard/imports.goblank-imports every standard module package; each package’sinit()callscaddy.RegisterModule(). This populates the globalmodules map[string]ModuleInfobeforemain()is reached.CLI setup:
caddycmd.Main()builds a cobra root command with registered subcommands. Therunsubcommand is the primary path.Config loading:
LoadConfig()resolves the config file, selects an adapter, and produces canonical JSON.Module provisioning:
ctx.LoadModule()is the key function: it callsModuleInfo.New()to construct an empty module value, unmarshals the JSON config fragment into it (usingjson.Unmarshal), then callsProvision(ctx)(if the module implementsProvisioner) andValidate()(if it implementsValidator). Modules record their instance inctx.moduleInstancesfor cleanup tracking.Start: Each app’s
Start()is called sequentially. Apps that open listeners (HTTP, admin) do so here and start goroutines.Context swap: The new
Context(and its*Config) atomically replacescurrentCtx. The old context’scancelFuncis invoked, triggeringCleanerUpper.Cleanup()for each old module instance.
Dependency injection pattern: Manual / context-passing. There is no wire, dig, or fx. Modules receive a caddy.Context in Provision() and use ctx.App("http") (type assertion) or ctx.LoadModule() to access sibling modules or load sub-modules. This is explicit, readable, and avoids reflection-heavy DI frameworks.
Configuration#
Caddy uses a three-layer config system:
Native JSON: The canonical format. The
Configstruct and all module structs are direct JSON targets; Caddy’sjson:"..."struct tags plus thecaddy:"namespace=... inline_key=..."struct tag describe the module namespace to load when decodingjson.RawMessagefields.StrictUnmarshalJSONdisallows unknown fields.Config adapters: Any non-JSON format goes through
caddyconfig.Adapter.Adapt()which outputs JSON. The Caddyfile adapter is the most important: it parses the Caddyfile DSL and maps directives to handler/matcher module configs via a registered directive system (httpcaddyfile.RegisterDirective()). Third-party adapters (YAML, NGINX, TOML) exist as separate plugins.Admin API (live):
PATCH/PUT/POST /config/<path>allows surgical mutations to any subtree of the running config without a full restart. Config is stored asmap[string]any(the “raw” config) so arbitrary JSON paths can be targeted.
Config autosave: After each successful reload, the full JSON config is written to ConfigAutosavePath (OS data dir). On next startup, --resume loads this file automatically.
Environment variables: CADDY_ADMIN overrides the admin listen address. XDG_DATA_HOME, HOME, APPDATA are used to derive data/config directories.
Env file: --envfile flag loads KEY=VALUE pairs before config parsing, allowing secrets to be injected without baking them into config files.
Key design decisions#
JSON as the single internal config representation. All adapters (Caddyfile, YAML, NGINX) are one-way translators that produce JSON. The admin API, autosave, and config diffing all operate on the same JSON representation. This eliminates format impedance and makes the running state fully inspectable and round-trippable. The cost is that JSON is verbose; the Caddyfile adapter exists precisely to hide that verbosity from users.
init()-driven module registration with namespace-scoped dispatch. Modules self-register with dotted IDs (e.g.http.handlers.reverse_proxy). When Caddy decodes ajson.RawMessagefield taggedcaddy:"namespace=http.handlers inline_key=handler", it reads thehandlerkey from the JSON, appends it to the namespace, and looks up the result in the registry. This makes adding a new handler as simple as: implementModule+MiddlewareHandler, callRegisterModule()ininit(), import the package. No central switch statement, no code-gen.Atomic live reload with rollback. Config changes are applied transactionally: new modules are provisioned in a new
Context; only if all provisioning and allStart()calls succeed does the context swap happen. If anything fails, the new context is canceled (cleaning up provisioned modules) and the old config keeps running. This is a strong operational guarantee — a bad config push never takes down a live server.caddy.Contextas the module lifetime boundary. Rather than global singletons or dependency injection containers, Caddy uses a context that owns all module instances for a given config generation. When the context is canceled (on config swap or shutdown), it iteratesmoduleInstancesand callsCleanup()on eachCleanerUpper. This achieves deterministic resource cleanup withoutdeferchains or manual teardown code in each module.cmd/caddy/main.goas a public template. The entry point binary is deliberately minimal (43 lines, 3 imports) and documented as a copy-paste template for custom builds. Combined withxcaddy, this is Caddy’s distribution model for third-party plugins: no plugin API server, no dynamic loading, no ABI stability concerns — just recompile with the desired imports.