How Caddy Dynamic Config Loading Works: Runtime Configuration Without Restarts
Caddy dynamic config loading enables the server to replace or augment its running configuration without process restarts through a combination of the admin API, the ConfigLoader abstraction, and configurable load delays that fetch external configurations at runtime.
Caddy dynamic config loading is a core feature of the caddyserver/caddy repository that allows the web server to update its behavior on the fly. This capability eliminates downtime during configuration changes by leveraging a modular architecture built around the admin API and pluggable configuration loaders. The system is designed to be thread-safe and extensible, supporting everything from simple HTTP POST requests to complex remote configuration polling.
The Admin API: /load and /adapt Endpoints
The admin API provides the primary interface for Caddy dynamic config loading through two key endpoints registered in [caddyconfig/load.go](https://github.com/caddyserver/caddy/blob/master/caddyconfig/load.go). The adminLoad module routes requests to handleLoad and handleAdapt handlers that process new configuration payloads.
The handleLoad function (lines 73‑84) reads the request body and optionally adapts it based on the Content-Type header via adaptByContentType. It then invokes caddy.Load(body, forceReload), which forwards the JSON to changeConfig as implemented in [caddy.go](https://github.com/caddyserver/caddy/blob/master/caddy.go) (lines 15‑23). This triggers unsyncedDecodeAndRun, which validates, marshals, and activates the new configuration.
To force a reload even when the configuration is identical, include the header Cache-Control: must-revalidate in your request. The /adapt endpoint performs the same content negotiation without applying the configuration, returning the adapted JSON for inspection.
The ConfigLoader Interface
Dynamic loading is driven by the ConfigLoader interface defined in [caddy.go](https://github.com/caddyserver/caddy/blob/master/caddy.go) at lines 671‑676:
type ConfigLoader interface {
LoadConfig(Context) ([]byte, error)
}
Modules implementing this interface can fetch configurations from any external source. The built-in HTTP loader resides in [caddyconfig/httploader.go](https://github.com/caddyserver/caddy/blob/master/caddyconfig/httploader.go) and is registered via caddy.RegisterModule(HTTPLoader{}) at line 29. Its LoadConfig method constructs HTTP requests, handles retries, reads response bodies, and adapts payloads according to the Content-Type header.
You reference loaders in your configuration using the admin.config.load path:
{
"admin": {
"config": {
"load": {
"module": "caddy.config_loaders.http",
"url": "https://example.com/caddy.json"
}
}
}
}
ConfigSettings: Controlling When and What to Load
The AdminConfig structure contains a nested ConfigSettings type defined in [admin.go](https://github.com/caddyserver/caddy/blob/master/admin.go) at lines 27‑45. Two fields orchestrate the loading behavior:
load(LoadRaw) – Raw JSON defining aConfigLoadermodule instance.load_delay(LoadDelay) – ADurationspecifying when to execute the loader.
When Caddy finishes provisioning the core in finishSettingUp (lines 601‑667 of [caddy.go](https://github.com/caddyserver/caddy/blob/master/caddy.go)), it checks for configured loaders:
if cfg != nil && cfg.Admin != nil && cfg.Admin.Config != nil && cfg.Admin.Config.LoadRaw != nil {
val, err := ctx.LoadModule(cfg.Admin.Config, "LoadRaw")
// …
}
The execution path diverges based on the delay value (lines 626‑656):
LoadDelay > 0: A goroutine starts atime.Timer. After the delay, it callsval.(ConfigLoader).LoadConfig(ctx). Success triggersrunLoadedConfig; failures trigger retries after the same delay.LoadDelay == 0: The loader runs synchronously during startup, with the configuration applied in a separate goroutine to prevent deadlocks with the admin server.
This delay mechanism prevents tight infinite loops by ensuring that dynamically loaded configurations requesting further loads must specify a positive delay.
How Configuration Changes Propagate
Once a loader fetches raw bytes via LoadConfig, the propagation follows this sequence:
runLoadedConfigreceives the bytes and invokesunsyncedDecodeAndRun.- Parsing converts the JSON into a
*Configstructure. - Provisioning initializes modules within the new configuration.
- Context swapping updates
currentCtxunder the protection ofrawCfgMuandcurrentCtxMu. - Lifecycle management stops old applications and starts new ones while the admin API continues serving requests.
All state transitions occur under mutex protection to guarantee thread-safety during concurrent configuration updates.
Practical Examples
Loading Configuration via the Admin API
Push a new configuration directly to the running server:
curl -X POST http://localhost:2019/load \
-H "Content-Type: application/json" \
-H "Cache-Control: must-revalidate" \
-d @new-config.json
For non-JSON payloads like Caddyfiles, set the Content-Type to text/caddyfile or specify an adapter in the request body.
Configuring Remote HTTP Loading with Delay
Configure Caddy to fetch and apply a remote configuration 10 seconds after startup:
{
"admin": {
"config": {
"load": {
"module": "caddy.config_loaders.http",
"url": "https://config.example.com/caddy.json",
"method": "GET",
"timeout": "30s"
},
"load_delay": "10s"
}
}
}
Caddy waits the specified duration, fetches the remote JSON, and replaces its running configuration. Failed loads retry after each delay interval until successful or until the process terminates.
Implementing a Custom Loader
Create a Go module that implements the ConfigLoader interface, register it with caddy.RegisterModule, and reference it in admin.config.load using your module's registered name.
Summary
- Caddy dynamic config loading eliminates restart requirements by applying configurations at runtime through the admin API or automated loaders.
- The
ConfigLoaderinterface abstracts configuration sources, with built-in HTTP support incaddyconfig/httploader.go. ConfigSettingsfields (loadandload_delay) control loader execution timing and prevent recursive load loops.- The
/loadand/adaptendpoints incaddyconfig/load.goprovide the HTTP interface for manual configuration pushes. - Thread-safe propagation relies on mutex-protected context swapping in
unsyncedDecodeAndRunto ensure zero-downtime transitions.
Frequently Asked Questions
What is the difference between the /load and /adapt admin API endpoints?
The /load endpoint accepts a configuration payload, adapts it if necessary, and immediately applies it to the running server via changeConfig. The /adapt endpoint performs the same content-type negotiation and adaptation but returns the resulting JSON without applying it, allowing you to preview or validate configurations before activation.
How does Caddy prevent infinite loops when dynamically loading configurations?
The load_delay field in ConfigSettings prevents tight loops by requiring a positive delay duration for any configuration that triggers subsequent loads. When load_delay is zero, the loader runs synchronously during startup. When positive, a timer goroutine manages the load, and any nested load requests must specify their own positive delays, breaking potential infinite recursion.
Can I use Caddy dynamic config loading with a custom configuration source?
Yes. Implement the ConfigLoader interface with a LoadConfig(Context) ([]byte, error) method to fetch from any source—databases, message queues, or proprietary APIs. Register your module using caddy.RegisterModule and reference it in the admin.config.load configuration using your module's registered name and any custom parameters your loader requires.
What happens to existing connections when Caddy reloads its configuration?
Existing connections complete gracefully while Caddy swaps the active context. The unsyncedDecodeAndRun function provisions the new configuration in the background, then atomically updates currentCtx under mutex protection. Old applications stop only after new applications start, ensuring continuous request handling during the transition.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →