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) to ModuleInfo structs. RegisterModule() is called from each module’s init(). GetModule() and GetModules() 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 *ConfigprovisionContext()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 (wraps context.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). Routes GET/POST/PUT/PATCH/DELETE /config/… to changeConfig() with JSON body, enabling surgical live config edits at any path within the config tree. Also routes /id/… (by @id field), /metrics (Prometheus), /pprof/, /debug/vars, and any paths registered by modules via AdminRouter interface. 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 DSL
    • httpcaddyfile/ — HTTP-specific Caddyfile-to-JSON translation: global options, site blocks, routes, matchers, handlers via RegisterDirective()/RegisterHandlerDirective()

HTTP App (caddyhttp)#

  • Package: github.com/caddyserver/caddy/v2/modules/caddyhttp
  • Responsibility: Implements caddy.App. Manages a set of Server instances, each bound to listener addresses. Each server has a Routes list; each route has MatcherSets (AND/OR logic) and HandlersRaw (middleware chain). At request time, the server walks routes in order, evaluates matchers, and builds a per-request handler stack via wrapRoute(). Implements the MiddlewareHandler interface chain terminating in a final handler.
  • Key types: App, Server, Route, Middleware (interface: wraps MiddlewareHandler), 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. Wraps certmagic to provide automatic TLS certificate management: ACME challenges, OCSP, certificate caching, session ticket key (STEK) rotation (both local standardstek and clustered distributedstek). Exposes TLSConfig() to the HTTP server for building *tls.Config from 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). Contains acmeserver/ 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, core caddy

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 eventsconfig subpackage. 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 disk

Live 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 OK

HTTP 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:

  1. Module registration (init phase): All init() functions run. modules/standard/imports.go blank-imports every standard module package; each package’s init() calls caddy.RegisterModule(). This populates the global modules map[string]ModuleInfo before main() is reached.

  2. CLI setup: caddycmd.Main() builds a cobra root command with registered subcommands. The run subcommand is the primary path.

  3. Config loading: LoadConfig() resolves the config file, selects an adapter, and produces canonical JSON.

  4. Module provisioning: ctx.LoadModule() is the key function: it calls ModuleInfo.New() to construct an empty module value, unmarshals the JSON config fragment into it (using json.Unmarshal), then calls Provision(ctx) (if the module implements Provisioner) and Validate() (if it implements Validator). Modules record their instance in ctx.moduleInstances for cleanup tracking.

  5. Start: Each app’s Start() is called sequentially. Apps that open listeners (HTTP, admin) do so here and start goroutines.

  6. Context swap: The new Context (and its *Config) atomically replaces currentCtx. The old context’s cancelFunc is invoked, triggering CleanerUpper.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:

  1. Native JSON: The canonical format. The Config struct and all module structs are direct JSON targets; Caddy’s json:"..." struct tags plus the caddy:"namespace=... inline_key=..." struct tag describe the module namespace to load when decoding json.RawMessage fields. StrictUnmarshalJSON disallows unknown fields.

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

  3. 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 as map[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#

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

  2. init()-driven module registration with namespace-scoped dispatch. Modules self-register with dotted IDs (e.g. http.handlers.reverse_proxy). When Caddy decodes a json.RawMessage field tagged caddy:"namespace=http.handlers inline_key=handler", it reads the handler key 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: implement Module + MiddlewareHandler, call RegisterModule() in init(), import the package. No central switch statement, no code-gen.

  3. Atomic live reload with rollback. Config changes are applied transactionally: new modules are provisioned in a new Context; only if all provisioning and all Start() 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.

  4. caddy.Context as 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 iterates moduleInstances and calls Cleanup() on each CleanerUpper. This achieves deterministic resource cleanup without defer chains or manual teardown code in each module.

  5. cmd/caddy/main.go as 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 with xcaddy, 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.