Skip to main content
Caddy is built on a powerful, extensible architecture that emphasizes modularity, configuration flexibility, and automatic HTTPS. This page explains how Caddy’s core components work together to deliver a production-ready web server.

Core Architecture

Caddy’s architecture revolves around several key concepts:
1

Config Structure

At the heart of Caddy is the Config struct, which represents the entire configuration:
caddy.go:68-95
type Config struct {
    Admin   *AdminConfig `json:"admin,omitempty"`
    Logging *Logging     `json:"logging,omitempty"`

    // StorageRaw is a storage module that defines how/where Caddy
    // stores assets (such as TLS certificates)
    StorageRaw json.RawMessage `json:"storage,omitempty" caddy:"namespace=caddy.storage inline_key=module"`

    // AppsRaw are the apps that Caddy will load and run
    AppsRaw ModuleMap `json:"apps,omitempty" caddy:"namespace="`

    apps         map[string]App
    failedApps   map[string]error
    storage      certmagic.Storage
    eventEmitter eventEmitter
    cancelFunc   context.CancelFunc
    fileSystems  FileSystems
}
The configuration is natively expressed as JSON, but config adapters can convert other formats (like Caddyfile) into Caddy JSON.
2

App Module System

Caddy runs multiple apps, each responsible for a specific area of functionality:
caddy.go:98-101
// App is a thing that Caddy runs.
type App interface {
    Start() error
    Stop() error
}
Common apps include:
  • http - HTTP server and routing
  • tls - Certificate management and TLS configuration
  • pki - Internal PKI for local certificates
  • Custom apps via third-party modules
3

Context and Lifecycle

Each configuration has a Context that manages module lifecycles:
context.go:46-55
type Context struct {
    context.Context

    moduleInstances map[string][]Module
    cfg             *Config
    ancestry        []Module
    cleanupFuncs    []func()
    exitFuncs       []func(context.Context)
    metricsRegistry *prometheus.Registry
}
The Context ensures proper provisioning, validation, and cleanup of all modules.

Configuration Lifecycle

Understanding the configuration lifecycle helps you debug issues and write better modules.
When Caddy loads a configuration, it follows this precise sequence:

Detailed Flow

From caddy.go:389-468, the run() function orchestrates the entire process:
// 1. Create context and provision modules
ctx, err := provisionContext(newCfg, start)

// 2. Provision admin routers which may need access to other apps
err = ctx.cfg.Admin.provisionAdminRouters(ctx)

// 3. Start all apps
started := make([]string, 0, len(ctx.cfg.apps))
for name, a := range ctx.cfg.apps {
    err := a.Start()
    if err != nil {
        // Stop already-started apps on failure
        for _, otherAppName := range started {
            ctx.cfg.apps[otherAppName].Stop()
        }
        return fmt.Errorf("%s app module: start: %v", name, err)
    }
    started = append(started, name)
}
If any app fails to start, Caddy stops all previously-started apps and cleans up provisioned modules to prevent resource leaks.

Admin API

The Admin API provides runtime control over Caddy’s configuration:
admin.go:67-86
type AdminConfig struct {
    // If true, the admin endpoint will be completely disabled
    Disabled bool `json:"disabled,omitempty"`

    // The address to which the admin endpoint's listener should bind itself
    // Default: localhost:2019
    Listen string `json:"listen,omitempty"`

    // If true, CORS headers will be emitted
    EnforceOrigin bool `json:"enforce_origin,omitempty"`

    // The list of allowed origins/hosts for API requests
    Origins []string `json:"origins,omitempty"`

    // Options pertaining to configuration management
    Config *ConfigSettings `json:"config,omitempty"`
}

Key Admin Endpoints

EndpointMethodPurpose
/config/GETRead current configuration
/config/POST/PUT/PATCHUpdate configuration
/config/DELETERemove config elements
/stopPOSTGracefully stop Caddy
/loadPOSTLoad new config
The admin API uses ETags for optimistic concurrency control, preventing race conditions during config updates.

Storage System

Caddy uses a pluggable storage system for persisting TLS certificates and other assets:
caddy.go:530-550
// Set up global storage and make it CertMagic's default
if newCfg.StorageRaw != nil {
    val, err := ctx.LoadModule(newCfg, "StorageRaw")
    if err != nil {
        return fmt.Errorf("loading storage module: %v", err)
    }
    stor, err := val.(StorageConverter).CertMagicStorage()
    if err != nil {
        return fmt.Errorf("creating storage value: %v", err)
    }
    newCfg.storage = stor
}

if newCfg.storage == nil {
    newCfg.storage = DefaultStorage
}
certmagic.Default.Storage = newCfg.storage
The default storage is the file system, but you can use:
  • Consul
  • DynamoDB
  • Redis
  • Custom storage implementations

Event System

Caddy has an event system for monitoring and reacting to changes:
caddy.go:1070-1092
type Event struct {
    // If non-nil, the event has been aborted
    Aborted error

    // The data associated with the event
    Data map[string]any

    id     uuid.UUID
    ts     time.Time
    name   string
    origin Module
}
Events are emitted for:
  • Certificate renewals
  • Server starts/stops
  • Config changes
  • Custom module events
Events are experimental and follow the CloudEvents specification for compatibility with external systems.

Configuration Autosave

Caddy automatically persists configurations that are pushed via the API:
caddy.go:362-384
if allowPersist &&
    newCfg != nil &&
    (newCfg.Admin == nil ||
        newCfg.Admin.Config == nil ||
        newCfg.Admin.Config.Persist == nil ||
        *newCfg.Admin.Config.Persist) {
    dir := filepath.Dir(ConfigAutosavePath)
    err := os.MkdirAll(dir, 0o700)
    if err != nil {
        Log().Error("unable to create folder for config autosave")
    } else {
        err := os.WriteFile(ConfigAutosavePath, cfgJSON, 0o600)
        if err == nil {
            Log().Info("autosaved config (load with --resume flag)")
        }
    }
}
This allows you to resume from the last working configuration after a restart.

Graceful Reloads

One of Caddy’s most powerful features is zero-downtime config reloads:
1

New Config Validation

The new configuration is fully validated before being applied.
2

Atomic Swap

The old and new contexts are swapped atomically:
caddy.go:353-356
currentCtxMu.Lock()
oldCtx := currentCtx
currentCtx = ctx
currentCtxMu.Unlock()
3

Graceful Shutdown

The old context is stopped asynchronously, allowing in-flight requests to complete.
If the new config fails to start, Caddy restores the old configuration state to maintain availability.

Instance Identity

Each Caddy instance has a unique UUID stored persistently:
caddy.go:913-932
func InstanceID() (uuid.UUID, error) {
    appDataDir := AppDataDir()
    uuidFilePath := filepath.Join(appDataDir, "instance.uuid")
    uuidFileBytes, err := os.ReadFile(uuidFilePath)
    if errors.Is(err, fs.ErrNotExist) {
        uuid, err := uuid.NewRandom()
        if err != nil {
            return uuid, err
        }
        err = os.MkdirAll(appDataDir, 0o700)
        if err != nil {
            return uuid, err
        }
        err = os.WriteFile(uuidFilePath, []byte(uuid.String()), 0o600)
        return uuid, err
    }
    return uuid.ParseBytes(uuidFileBytes)
}
This UUID is used for:
  • Cluster coordination
  • Storage locking
  • Distributed certificate management

Best Practices

1

Use the Admin API

Prefer using the admin API for config changes rather than restarting the process.
2

Test Configs

Use caddy validate to test configurations before deploying them.
3

Monitor Events

Subscribe to Caddy events to track certificate renewals and config changes.
4

Backup Configs

Regularly backup your configuration and the autosave file.

Build docs developers (and LLMs) love