How Caddy Config Loading and Validation Works: From Caddyfile to Runtime

Caddy loads configuration through the admin API or CLI, adapts non-JSON formats to native JSON, provisions modules via unsyncedDecodeAndRun, and validates each module's Validate() method before atomically applying changes with automatic rollback on failure.

Caddy's configuration system is designed for zero-downtime reloads and strict validation before activation. Whether you're using the Caddyfile format, JSON, or the admin API, the server follows a consistent pipeline to ensure only valid configurations reach runtime. This article examines the source code in caddyserver/caddy to explain exactly how the config loading and validation process operates.

Config Loading Pipeline

Admin API Endpoints (/load and /adapt)

The admin module registers configuration endpoints in caddyconfig/load.go. The adminLoad.Routes function sets up /load and /adapt handlers that accept configuration via HTTP【adminLoad.Routes】.

The /load endpoint (handleLoad) performs the following:

  1. Reads the request body containing raw configuration
  2. If the Content-Type is not JSON, calls adaptByContentType to execute the appropriate config adapter (e.g., converting Caddyfile to JSON)【adaptByContentType】
  3. Invokes caddy.Load(body, forceReload) where forceReload is set to true when the request includes Cache-Control: must-revalidate【handleLoad】

The /adapt endpoint simply returns the JSON result of the adaptation without applying it, useful for dry-run validation of Caddyfile syntax.

CLI Configuration Loading

The command-line entry point in cmd/main.go provides the LoadConfig helper function that reads configuration files and selects adapters based on file extensions【LoadConfig】. When you run caddy run or caddy test, the CLI calls caddy.Load with the raw bytes from the file.

Core Load Function and changeConfig

The caddy.Load function in caddy.go serves as the central entry point for both the admin API and CLI【caddy.Load】. It notifies the OS that a reload is occurring and delegates to changeConfig(http.MethodPost, "/"+rawConfigKey, cfgJSON, "", forceReload).

The changeConfig function in caddy.go acts as the atomic mutation hub for all configuration changes【changeConfig】. It performs these critical steps:

  1. If-Match handling – Validates optional preconditions using ETag headers to prevent stale updates
  2. Mutation – Calls unsyncedConfigAccess to update the in-memory raw config map
  3. JSON encoding – Serializes the entire configuration structure as newCfg
  4. Short-circuit optimization – Compares the new JSON against rawCfgJSON; if identical and forceReload is false, returns errSameConfig to skip unnecessary reloads
  5. ID indexing – Walks the config tree to build an ID-to-path index via indexConfigObjects for efficient module lookups
  6. Decode and provision – Invokes unsyncedDecodeAndRun(newCfg, true) to instantiate modules and run validation
  7. Rollback protection – If provisioning fails, unmarshals the previous raw config back into rawCfg to maintain server stability
  8. Persistence – Stores the new JSON in rawCfgJSON for future comparison

Provisioning and Validation

Module Provisioning

Inside caddy.go, the unsyncedDecodeAndRun function handles the transition from static JSON to live modules. It decodes the JSON into a Config struct and calls Provision on each module (apps, handlers, TLS managers, etc.). This phase instantiates objects and wires up dependencies before any traffic flows through them.

The Validator Interface

After provisioning, Caddy enforces semantic validation through the Validator interface defined in modules.go【Validator interface】. Any module requiring additional validation implements the Validate() method:

If any Validate() method returns an error, the error bubbles up through unsyncedDecodeAndRun, triggering the rollback mechanism in changeConfig and causing the admin API to return HTTP 400.

CLI Validation Command

The caddy validate sub-command in cmd/commandfuncs.go provides explicit validation without starting the server【cmdValidateConfig】. The handler loads the configuration file, adapts it if necessary, and calls caddy.Validate(cfg). This function is a thin wrapper around run(cfg, false) that provisions and validates all modules without starting listeners, then disposes of resources【Validate】.

Practical Configuration Examples

Loading via Admin API

Send a Caddyfile directly to the running server for hot-reloading:

curl -X POST http://localhost:2019/load \
  -H "Content-Type: application/caddyfile" \
  -d '
:80 {
    respond "Hello, Caddy!"
}
'

The endpoint automatically adapts the Caddyfile to JSON, runs provisioning and validation, and applies the configuration atomically.

Programmatic Loading in Go

Embed configuration reloading in your Go application:

import (
    "github.com/caddyserver/caddy/v2"
    "github.com/caddyserver/caddy/v2/caddyconfig"
)

func reloadFromFile(path string) error {
    // Read file and detect adapter by extension
    raw, adapter, src, err := caddyconfig.LoadConfig(path)
    if err != nil {
        return err
    }

    // Adapt non-JSON formats
    if adapter != "" {
        raw, _, err = caddyconfig.GetAdapter(adapter).Adapt(raw, nil)
        if err != nil {
            return err
        }
    }

    // Load, validate, and provision (force reload)
    return caddy.Load(raw, true)
}

CLI Validation

Validate a configuration file before deployment:

caddy validate --config ./Caddyfile

This parses the file, adapts it to JSON, provisions all modules, and runs validation without starting the server or binding to ports.

Summary

  • Caddy loads configuration through the admin API (/load, /adapt) or CLI (caddy run, caddy validate), with adapters converting Caddyfile and other formats to native JSON in caddyconfig/load.go and cmd/main.go
  • The changeConfig function in caddy.go orchestrates atomic configuration updates with If-Match preconditions, short-circuit optimization for identical configs, and automatic rollback on failure
  • Module provisioning occurs in unsyncedDecodeAndRun, which instantiates apps and handlers before they accept traffic
  • Validation happens via the Validator interface in modules.go, where modules like TLS and HTTP handlers implement Validate() to catch misconfigurations before activation
  • Rollback protection ensures that if provisioning or validation fails, the previous configuration remains active in rawCfg, maintaining server stability

Frequently Asked Questions

What happens if a Caddy configuration fails validation?

If any module's Validate() method returns an error during the unsyncedDecodeAndRun phase, changeConfig catches the error and rolls back to the previous configuration. The function unmarshals the prior raw JSON back into rawCfg, ensuring the running server remains unaffected, and returns HTTP 400 for API requests or a non-zero exit code for CLI commands.

How does Caddy handle configuration reloads without downtime?

Caddy uses an atomic two-phase commit process in changeConfig. It first provisions and validates the new configuration alongside the running one. Only after all modules pass validation does it switch the active configuration. If the new JSON matches the current rawCfgJSON and forceReload is false, it returns errSameConfig to avoid unnecessary reloads entirely.

Can I validate a Caddyfile before applying it?

Yes. Use the caddy validate --config ./Caddyfile command implemented in cmd/commandfuncs.go. This runs the full provisioning and validation pipeline—including adapter conversion and all module Validate() calls—without starting the server or binding to network ports.

What is the difference between /load and /adapt admin endpoints?

The /load endpoint in caddyconfig/load.go adapts configuration (if necessary) and immediately applies it to the running server via caddy.Load. The /adapt endpoint only performs the conversion from Caddyfile or other formats to JSON and returns the result without modifying the running configuration, making it useful for debugging or previewing changes.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →