How Caddy's JSON API for Dynamic Configuration Works: Complete Technical Guide
Caddy's JSON API enables runtime configuration changes through an admin HTTP server that exposes REST endpoints for reading, patching, and reloading the active configuration without process restarts.
The Caddy web server supports dynamic configuration management through a built-in JSON API, allowing operators to modify TLS certificates, routes, and server blocks while the server remains active. This capability centers on the admin server configured via the Admin field in the top-level Config struct, which exposes HTTP endpoints for granular configuration manipulation. According to the caddyserver/caddy source code, the API implements optimistic concurrency control using ETags and atomic configuration swaps to ensure safe, concurrent updates.
Admin Server Architecture and Request Routing
When Caddy starts, the admin server initializes an adminHandler (see admin.go lines 221-236) that registers routes mapping to specific configuration operations. The handler receives all requests via adminHandler.ServeHTTP (lines 787-795) and dispatches them through an internal mux after performing security checks via checkHost and checkOrigin.
The API exposes the following primary routes:
/config/*– Managed byhandleConfigfor read and write operations on configuration subtrees/load– Handled byadminLoad.handleLoadfor full configuration replacement/id/*– Resolved byhandleConfigIDfor object lookup using the@idmeta-field/stop– Processed byhandleStopfor graceful server termination
Read requests execute under a read lock, while mutations acquire a write lock through rawCfgMu to protect the internal rawCfg map (see the concurrency comments at admin.go lines 45-47).
Reading Configuration with ETag Support
The GET /config/... endpoint triggers handleConfig (lines 1011-1032), which retrieves JSON subtrees through readConfig (lines 267-273 in caddy.go). This function wraps unsyncedConfigAccess to traverse the raw configuration structure and return the requested data.
For optimistic concurrency control, the handler generates an ETag header using makeEtag (lines 997-1002). The ETag value follows the format "path <hex-hash>", where the hash represents the current state of the requested subtree. Clients must include this value in subsequent If-Match headers when modifying configuration to prevent lost updates.
curl -s http://localhost:2019/config/ | jq .
The response includes an Etag header (e.g., Etag: "config 7b8e...f0") that represents the configuration's current hash state.
Mutating Configuration Dynamically
Configuration changes occur through POST, PUT, PATCH, and DELETE requests to /config/*, which delegate to changeConfig (lines 145-164 in caddy.go). This function implements a multi-stage workflow for safe configuration updates.
The changeConfig Workflow
The mutation process follows these steps:
- If-Match Validation – Parses the quoted header and computes the current hash via
unsyncedConfigAccess, rejecting requests with HTTP 412 if the hash differs (lines 172-203) - Apply Change – Calls
unsyncedConfigAccesswith the HTTP method, JSON body, and target path to traverserawCfgand perform the operation, including array handling and ellipsis support (lines 441-495 and 511-595 inadmin.go) - Detect No-Op – Compares the marshalled representation (
newCfg) against the cached copy (rawCfgJSON) and returnserrSameConfigif unchanged andforceReloadis false (lines 220-225) - Re-index @id Fields – Rebuilds the lookup table via
indexConfigObjects(lines 274-282) to maintain the/id/*resolution capability - Load New Config – Invokes
unsyncedDecodeAndRun(lines 312-324) to strip meta-fields, unmarshal into*Config, and re-initialize plugins and listeners atomically
If any step fails, Caddy executes rollback logic (lines 336-353) to restore the previous configuration, ensuring the running server remains stable.
Array Manipulation with Ellipsis Syntax
To append elements to arrays without overwriting existing items, use the trailing ... path syntax:
curl -X POST \
-H "Content-Type: application/json" \
-d '[{"handle":[{"handler":"static_response","body":"new"}]}]' \
http://localhost:2019/config/apps/http/servers/srv0/routes/...
The trailing ellipsis instructs Caddy to treat the payload as an array and append each element to the existing routes rather than replacing the entire array.
Full Configuration Replacement
The /load endpoint provides atomic full-configuration replacement through handleLoad in the adminLoad module. This endpoint reads the request body, optionally validates an If-Match header, and calls caddy.Load (lines 68-78 in caddyconfig/load.go). This function clears the previous configuration and runs the new one exactly like a fresh caddy run command, but without restarting the process.
cat new-config.json | curl -X POST \
-H "Content-Type: application/json" \
http://localhost:2019/load
If the new configuration fails to load, Caddy retains the previous active configuration, preventing service disruption.
Object Resolution Using @id
When configuration objects include the @id meta-field, indexConfigObjects (lines 274-300 in caddy.go) maintains a mapping from ID values to their JSON pointer paths. The GET /id/<id> endpoint uses this index to retrieve objects without requiring knowledge of their full path structure.
curl http://localhost:2019/id/myroute
This resolves the object tagged with "@id":"myroute" regardless of its nested location within the configuration tree.
Practical API Usage Examples
Optimistic Concurrency with ETags
To safely update a configuration field while preventing conflicts from concurrent modifications:
# Capture the current ETag
etag=$(curl -i -s http://localhost:2019/config/ | awk -F'"' '/Etag/ {print $2}')
# Submit patch with validation
curl -X PATCH \
-H "Content-Type: application/json" \
-H "If-Match: \"$etag\"" \
-d '{"listen":"0.0.0.0:2020"}' \
http://localhost:2019/config/admin
If the configuration changed between the GET and PATCH requests, Caddy returns HTTP 412 Precondition Failed, prompting the client to retry with the current state.
Deleting Configuration Subtrees
Remove specific configuration paths using DELETE requests:
curl -X DELETE http://localhost:2019/config/apps/http/servers/srv0/routes/0
This removes the first route from the specified server's routes array.
Summary
- The admin server initializes at startup via
AdminConfig, creating anadminHandlerthat routes requests to specific handlers likehandleConfigandhandleLoad - ETag-based optimistic concurrency prevents lost updates by requiring matching hashes in
If-Matchheaders for all mutation operations - The
changeConfigfunction incaddy.goorchestrates safe configuration updates through validation, application, re-indexing, and atomic loading stages - Automatic rollback occurs if configuration loading fails, ensuring the server never enters an undefined state
- The
@idmeta-field system enables stable object references via the/id/*endpoint, decoupling clients from specific JSON paths
Frequently Asked Questions
What port does Caddy's JSON API use by default?
By default, the admin server listens on localhost:2019. You can modify this through the admin section of your Caddy configuration or via the CADDY_ADMIN environment variable. The listen field accepts network addresses in the format host:port or Unix socket paths.
How does Caddy handle concurrent configuration updates from multiple clients?
Caddy implements optimistic concurrency control using ETag headers. When reading configuration, the server returns an ETag representing the current state hash. Clients must include this value in the If-Match header when submitting changes. If the configuration changed between read and write (hash mismatch), Caddy returns HTTP 412 Precondition Failed, forcing the client to fetch the current state and retry.
What happens if a dynamic configuration update fails halfway through?
Caddy maintains the previous configuration in memory during the update process. If unsyncedDecodeAndRun fails to load the new configuration at caddy.go lines 336-353, the server automatically rolls back to the previous stable configuration. This ensures that partial or invalid configurations never disrupt active services.
Can I use the JSON API to update TLS certificates dynamically?
Yes. You can POST new certificate data to /config/apps/tls/certificates or use the /id/* endpoint if your certificates have @id tags. Caddy will reload the TLS configuration without dropping existing connections, enabling zero-downtime certificate rotation through the API.
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 →