Caddy JSON #

Although most users interact with Caddy through the concise, human-friendly Caddyfile, that format is actually just a high-level abstraction. Behind the scenes, Caddy’s runtime engine doesn’t understand the Caddyfile directly. All Caddy configuration parameters are managed and executed using the native format: Caddy JSON. Understanding Caddy JSON is very important if you want to build dynamic programmatic integrations, leverage advanced features that don’t have shortcuts in the Caddyfile, or do code-based deployment automation using external systems. We’ll thoroughly dissect the Caddy JSON configuration tree architecture, the adapter compilation process, the anatomy of routes and handlers, TLS certificate automation, and a decision-making guide for when to choose JSON over the Caddyfile.


Why Understand Caddy JSON? #

The Caddyfile is designed to make manual configuration writing easy for humans. However, the Caddyfile’s simplicity sacrifices configuration freedom (expressiveness). Some very granular Caddy module features — like complex access log rotation management, very specific TLS handshake policy settings per IP address, or TCP/UDP-level routing (Layer 4 proxy) — are often not fully supported by the Caddyfile syntax.

Caddy JSON is the direct representation of the Go programming language struct objects inside Caddy’s source code. By using JSON, you gain these advantages:

  • 100% Feature Access: Every configuration option from every Caddy module (both built-in and third-party plugins) is fully exposed without restrictions.
  • Programmatic Automation: Allows external applications (like Python scripts, Node.js dashboards, Kubernetes operators, or Terraform modules) to generate configuration documents dynamically directly from databases.
  • Config API Integration: Instant configuration changes via Caddy’s REST API require the document to be sent in JSON format.
  • State Consistency: Eliminates Caddyfile parser interpretation ambiguity, because JSON has a definite data schema validated directly by Go’s standard library.

Configuration Compilation Pipeline #

Caddy uses a highly modular architecture. When you give a Caddyfile configuration file to Caddy, the file passes through the following pipeline before finally being activated in memory:

flowchart TD
    Caddyfile["1. Caddyfile File (Physical)"] --> Adapter["2. Caddyfile Adapter (Parser)"]
    Adapter --> JSON_AST["3. JSON AST Document (Native Format)"]
    JSON_AST --> Validation["4. Schema & Runtime Module Validation"]
    
    subgraph Caddy_Internal["Caddy Internal Engine"]
        Validation -->|Passes| Active_Mem["5. Active Configuration in RAM"]
        Validation -->|Fails| Rollback["6. Cancel, Keep Using the Old Config"]
    end

    style Caddyfile stroke:#0288d1,stroke-width:2px
    style Active_Mem stroke:#43a047,stroke-width:2px
    style Rollback stroke:#e53935,stroke-dasharray:5,5

The process above shows that the Caddyfile Adapter acts as a translator converting the Caddyfile’s declarative statements into a JSON Abstract Syntax Tree (AST) file. The caddy adapt command can be used to see the result of this translation directly:

# Convert a local Caddyfile to raw JSON form
caddy adapt --config /etc/caddy/Caddyfile --adapter caddyfile

# Format the JSON output using jq for human readability
caddy adapt --config /etc/caddy/Caddyfile --adapter caddyfile | jq .

# Save the conversion result to a permanent JSON configuration file
caddy adapt --config /etc/caddy/Caddyfile --adapter caddyfile | jq . > /etc/caddy/config.json

Caddy JSON Top-Level Structure #

The Caddy JSON document is organized into several main key blocks representing the server’s functional scopes:

{
  "admin": {
    // Controls the Admin API listen address, TLS, and access control
  },
  "logging": {
    // Defines system log and access log writing
  },
  "storage": {
    // Determines the TLS certificate storage location (disk, Redis, database)
  },
  "apps": {
    // The main application block where routes and server logic are defined
    "http": {
      // The HTTP web server, routes, domain handling
    },
    "tls": {
      // SSL/TLS certificate automation policies
    }
  }
}

Each of these blocks maps directly to Caddy’s internal module structure. The most crucial distinction lies in the apps block, which houses Caddy’s functional modules.


Apple-to-Apple Comparison: Caddyfile vs JSON #

To understand how Caddy translates Caddyfile syntax to JSON, let’s directly compare a virtual host configuration serving static files, data compression, a reverse proxy, and access log writing:

The Caddyfile #

# Standard Caddyfile configuration
example.com {
    encode gzip zstd
    reverse_proxy localhost:3000
    
    log {
        output file /var/log/caddy/access.log
        format json
    }
}

The Converted Caddy JSON Result #

Here’s the JSON document resulting from translating the Caddyfile above (some default parts have been simplified for clarity):

{
  "apps": {
    "http": {
      "servers": {
        "srv0": {
          "listen": [":443", ":80"],
          "routes": [
            {
              "match": [{"host": ["example.com"]}],
              "handle": [
                {
                  "handler": "subroute",
                  "routes": [
                    {
                      "handle": [
                        {
                          "handler": "encode",
                          "encodings": {
                            "gzip": {},
                            "zstd": {}
                          }
                        }
                      ]
                    },
                    {
                      "handle": [
                        {
                          "handler": "reverse_proxy",
                          "upstreams": [{"dial": "localhost:3000"}]
                        }
                      ]
                    }
                  ]
                }
              ],
              "terminal": true
            }
          ],
          "logs": {
            "logger_names": {
              "example.com": "log0"
            }
          }
        }
      }
    },
    "tls": {
      "automation": {
        "policies": [
          {
            "subjects": ["example.com"],
            "issuers": [
              {
                "module": "acme",
                "ca": "https://acme-v02.api.letsencrypt.org/directory"
              }
            ]
          }
        ]
      }
    }
  },
  "logging": {
    "logs": {
      "log0": {
        "writer": {
          "output": "file",
          "filename": "/var/log/caddy/access.log"
        },
        "encoder": {"format": "json"}
      }
    }
  }
}

Looking at the conversion result above:

  1. The example.com domain block is translated into a host matcher ("match": [{"host": ["example.com"]}]).
  2. Ports :443 and :80 are automatically defined in the "listen" array because Caddy enables automatic HTTPS by default.
  3. The log writing structure is separated from the main HTTP configuration and placed on the top-level "logging" object with a unique logger name (log0) referenced by the virtual host.

Route and Matcher Anatomy in JSON #

A route is the smallest processing unit in Caddy’s HTTP module that determines how a user request is processed. The structure of a route in Caddy JSON consists of three main properties:

{
  "routes": [
    {
      "@id": "optional-marker-id",
      "match": [
        // Request filtering criteria (Matchers)
      ],
      "handle": [
        // Request processing chain (Handlers)
      ],
      "terminal": true
    }
  ]
}

1. Matchers (Filtering Conditions) #

Matchers are condition blocks that determine whether this route should process an incoming request or ignore it. Caddy provides various very flexible built-in matchers:

"match": [
  {
    "host": ["example.com", "api.example.com"],
    "path": ["/api/v1/*", "/assets/*.png"],
    "method": ["GET", "POST"],
    "header": {
      "Content-Type": ["application/json"],
      "X-Requested-With": ["XMLHttpRequest"]
    },
    "remote_ip": {
      "ranges": ["192.168.1.0/24", "10.0.0.0/8"]
    },
    "query": {
      "format": ["json"]
    }
  }
]

2. Handlers (Processing Chains) #

If the criteria in match are met, Caddy executes the handle array sequentially (handler chain). Each handler is identified by its module name in the "handler" property.

Here are example JSON configurations for the most frequently used handler types:

A. The file_server Handler (Static File Serving) #

This handler is used to serve physical files from the server’s disk storage:

{
  "handler": "file_server",
  "root": "/var/www/html",
  "index_names": ["index.html", "index.htm"],
  "browse": {} // Enables the interactive directory browser module
}

B. The reverse_proxy Handler (Traffic Forwarding) #

This handler routes requests to backend servers, complete with load balancing configuration, plus active and passive health checks:

{
  "handler": "reverse_proxy",
  "upstreams": [
    {"dial": "10.0.1.10:3000"},
    {"dial": "10.0.1.11:3000"}
  ],
  "load_balancing": {
    "selection_policy": {
      "policy": "least_conn"
    }
  },
  "health_checks": {
    "active": {
      "uri": "/healthz",
      "interval": "10s",
      "timeout": "3s",
      "expect_status": 200
    },
    "passive": {
      "fail_duration": "30s",
      "max_fails": 3
    }
  }
}

C. The static_response Handler (Static Response / Redirect) #

Used to return static text or do URL redirection:

{
  "handler": "static_response",
  "status_code": 301,
  "headers": {
    "Location": ["https://example.com{http.request.uri}"],
    "Content-Type": ["text/html; charset=utf-8"]
  }
}

D. The encode Handler (Data Compression) #

Used to compress response payloads to save network bandwidth:

{
  "handler": "encode",
  "encodings": {
    "gzip": {},
    "zstd": {}
  },
  "minimum_length": 1024 // Only compress responses larger than 1KB
}

TLS Configuration and Certificate Automation in JSON #

Caddy’s TLS module is configured in JSON under the apps.tls path. This structure lets you define certificate authentication policies very dynamically for various domain scenarios.

Here’s a complete TLS configuration defining two different certificate automation policies:

{
  "apps": {
    "tls": {
      "automation": {
        "policies": [
          {
            # Policy 1: Public domains using ACME (Let's Encrypt & ZeroSSL)
            "subjects": ["example.com", "*.example.com"],
            "issuers": [
              {
                "module": "acme",
                "ca": "https://acme-v02.api.letsencrypt.org/directory",
                "email": "[email protected]",
                "challenges": {
                  "dns": {
                    "provider": {
                      "name": "cloudflare",
                      "api_token": "{env.CF_API_TOKEN}"
                    }
                  }
                }
              },
              {
                "module": "acme",
                "ca": "https://acme.zerossl.com/v2/DV90",
                "external_account": {
                  "key_id": "{env.ZEROSSL_KEY_ID}",
                  "mac_key": "{env.ZEROSSL_MAC_KEY}"
                }
              }
            ]
          },
          {
            # Policy 2: Internal local network domains using the internal CA
            "subjects": ["app.local", "database.local"],
            "issuers": [
              {
                "module": "internal"
              }
            ]
          }
        ]
      }
    }
  }
}

In the configuration above:

  • The first policy defines the wildcard domain *.example.com using the Cloudflare DNS-01 challenge for validation, with the API token read directly from the system environment variable ({env.CF_API_TOKEN}). Caddy also sets up ZeroSSL with External Account Binding (EAB) as an automatic fallback.
  • The second policy configures local domains (app.local) to use Caddy’s internal PKI module to create local certificates signed by a standalone internal Root CA.

Global Logging Configuration and Log Rotation #

One of the Caddy JSON features that can’t be freely configured in the Caddyfile is precise log rotation parameter customization.

In Caddy JSON, you can define an access log logger that writes to a physical file, limits the maximum file size, sets retention days, and the encoder format in a structured way:

{
  "logging": {
    "logs": {
      "access_logs": {
        "writer": {
          "output": "file",
          "filename": "/var/log/caddy/access.log",
          "roll_size_mb": 100,      // Rotate the file after reaching 100MB
          "roll_gzip": true,         // Compress old log files to gzip format
          "roll_keep": 10,           // Only keep a maximum of 10 archive log files
          "roll_keep_days": 90       // Keep archive log files for up to 90 days
        },
        "encoder": {
          "format": "json"           // Logs are written in a unified JSON structure
        },
        "include": ["http.log.access"],
        "level": "INFO"
      }
    }
  }
}

Running Caddy with a JSON File #

Once your JSON configuration document is ready, you can run it directly on the server using several methods:

1. Running the Server Directly (Command Line) #

The caddy run command reads the JSON file natively if you omit the --adapter parameter:

# Run Caddy using a JSON configuration natively
caddy run --config /etc/caddy/config.json

# Run in the background (background daemon)
caddy start --config /etc/caddy/config.json

2. Loading the JSON Configuration via the Admin API (Zero-Downtime Reload) #

If Caddy is already running on the server, you can send the new JSON file directly to the /load endpoint to trigger an instant zero-downtime reload:

# Send the JSON file to the local Admin API
curl -s -X POST http://localhost:2019/load \
  -H "Content-Type: application/json" \
  --data-binary @/etc/caddy/config.json

When to Choose JSON vs the Caddyfile? #

To help you make the right tactical decision when designing your Caddy web server infrastructure architecture, use the following comparison criteria guidelines:

KEEP USING the CADDYFILE if:
  ✓ The configuration is written, read, and maintained manually by humans.
  ✓ The operations team isn't familiar with very verbose JSON structures.
  ✓ The deployment needs are standard (like static file hosting, basic HTTP proxying).
  ✓ There's no need for programmatic automation integration from external applications.

CONSIDER SWITCHING to JSON if:
  ✓ The configuration is produced automatically by application code (dynamic generation).
  ✓ Managing multi-tenant SaaS platforms with thousands of dynamic domains via the REST API.
  ✓ Needing advanced granular configuration options not supported by the Caddyfile.
  ✓ Building Kubernetes infrastructure using the Caddy Ingress Controller.
  ✓ Needing a log system with very strict file rotation rules.

Summary #

  • The Server’s Native Format: Caddy JSON is the native configuration format and the single source of truth for all Caddy server functionality.
  • Compilation Process: Use the caddy adapt command to see the JSON representation of the Caddyfile you create, making it easier to learn the object tree structure.
  • Main Tree Structure: The Caddy JSON document is organized into four main blocks: apps (applications), admin (API), logging (log recording), and storage (TLS storage).
  • Route Flexibility: The HTTP route block consists of match (filtering criteria) and handle (handler processing chains) evaluated sequentially.
  • Consistent Identification: Always insert @id tags on dynamic route objects so they can be safely manipulated via the Config API without worrying about array index shifts.
  • Granular Configuration: JSON provides full control over advanced parameters like automatic log file rotation, distributed cluster SSL/TLS storage, and Layer 4 proxying.

← Previous: Reload Config   Next: Security →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact