Caddy’s JSON configuration is a powerful way to manage your server, but it’s often misunderstood as just a "different syntax" for the Caddyfile. The reality is, JSON config is Caddy’s native, programmatic interface, and it unlocks a level of dynamic control the Caddyfile can’t touch.
Let’s see Caddy running with a JSON config. Imagine you have a simple API that needs to be exposed over HTTPS with a specific TLS certificate.
{
"apps": {
"http": {
"servers": {
"my_api_server": {
"listen": [":443"],
"routes": [
{
"match": [{"host": ["api.example.com"]}],
"handle": [
{
"handler": "reverse_proxy",
"upstreams": [{"dial": "localhost:8080"}]
}
],
"terminal": true
}
],
"tls_connection_policies": [
{
"server_name": "api.example.com",
"tls": {
"certificates": {
"load": ["/etc/ssl/certs/api.example.com.crt", "/etc/ssl/private/api.example.com.key"]
}
}
}
]
}
}
}
}
}
This JSON config defines one application (http), within that, one server (my_api_server) listening on port 443. It routes requests for api.example.com to a backend running on localhost:8080. Crucially, it specifies a custom TLS certificate to be used for api.example.com.
The primary problem Caddy’s JSON config solves is the inability to programmatically control Caddy’s configuration. With the Caddyfile, you’re editing a static file. With JSON, you can generate this configuration on the fly based on external data, orchestrate it with other systems, or even update it without restarting Caddy (though this requires more advanced techniques like the Admin API).
Internally, Caddy parses this JSON and builds an internal representation of its configuration. The apps key is the top-level entrypoint for all Caddy modules. The http app is responsible for handling HTTP and HTTPS traffic. Within http, servers define distinct network endpoints. Each server has listen directives (ports or network addresses) and routes that dictate how requests are handled. routes are evaluated in order, and the first one that matches a request is executed. handle specifies the action to take, such as reverse_proxying to an upstream. The tls_connection_policies section allows granular control over TLS settings, including specifying custom certificates.
The terminal: true directive on the route is important. It means that once this route matches, no further routes within this server will be evaluated. This is crucial for ensuring that your specific API route is handled exclusively and doesn’t fall through to a more general catch-all route.
When you use Caddy’s Admin API to push a new configuration, you’re sending this JSON payload. Caddy then validates it, merges it with the existing configuration (or replaces it, depending on the API call), and dynamically reconfigures itself. This is how you achieve zero-downtime reloads and dynamic updates.
Most people don’t realize that the tls block within tls_connection_policies is actually a separate configuration structure that mirrors the top-level tls app configuration. This means you can specify automation blocks (for ACME challenges), protocols, and other TLS-specific settings directly within a server’s TLS policy, giving you fine-grained control over how TLS is negotiated for specific hostnames or SNI values served by that server.
The next logical step is to explore how to dynamically reload this JSON configuration using Caddy’s Admin API without restarting the process.