How the Caddy Context System Works: Module Lifecycle and Dependency Injection Explained

The Caddy context system is a thin wrapper around Go's context.Context that manages module lifecycles, dependency injection, configuration access, and graceful cleanup across the Caddy server architecture.

The context system in caddyserver/caddy serves as the backbone for the web server’s modular architecture. It ties together configuration loading, module provisioning, logging, and resource management into a cohesive framework. Understanding how this system operates is essential for developing custom Caddy modules or debugging server behavior.

Understanding the Context Structure

The Context type defined in context.go extends Go’s standard context with Caddy-specific state management capabilities.

Core Fields and Their Purposes

The struct definition at lines 46-55 of context.go reveals the internal architecture:

type Context struct {
    context.Context
    moduleInstances map[string][]Module
    cfg             *Config
    ancestry        []Module
    cleanupFuncs    []func()
    exitFuncs       []func(context.Context)
    metricsRegistry *prometheus.Registry
}

Key components include:

  • Embedded context.Context – Provides standard cancellation signals and value storage.
  • moduleInstances – Tracks every module instantiated within this context for deterministic cleanup.
  • cfg – Holds the active *Config containing apps, storage, and logging configuration.
  • ancestry – Maintains a stack of provisioned modules, enabling ctx.Module() to identify the current module.

Creating and Managing Context Lifecycles

Context creation follows a strict lifecycle protocol that ensures proper initialization and guaranteed cleanup.

The NewContext Function

The NewContext function at lines 57-89 of context.go constructs child contexts:

func NewContext(ctx Context) (Context, context.CancelFunc)

The initialization process:

  1. Copies the parent’s configuration (ctx.cfg) into the new context.
  2. Creates a fresh moduleInstances map to isolate module tracking.
  3. Wraps the parent’s ctx.Context with context.WithCancel.
  4. Returns a wrappedCancel function that orchestrates shutdown sequences.

Cancellation and Cleanup Hooks

When wrappedCancel executes, it triggers a deterministic cleanup sequence:

  1. Calls the underlying context.CancelFunc.
  2. Executes all functions registered via ctx.OnCancel.
  3. Iterates through moduleInstances and invokes Cleanup() on any module implementing the CleanerUpper interface.

Register cleanup functions using:

func (ctx Context) OnCancel(f func())

For graceful shutdown scenarios, use the experimental exit hook:

func (ctx Context) OnExit(f func(context.Context))

Loading and Provisioning Modules

The context system serves as the primary entry point for Caddy’s dynamic module loading architecture.

LoadModule and LoadModuleByID

Caddy provides three public entry points for module instantiation:

LoadModule(structPtr any, fieldName string) (any, error)

Reflects on a struct field, reads its caddy struct tag, parses the module namespace, and dispatches to the appropriate loader. Located at lines 81-79 of context.go.

LoadModuleByID(id string, rawMsg json.RawMessage) (any, error)

Low-level loader that:

  • Looks up the module ID in the global modules registry from modules.go.
  • Constructs the module via its New() function.
  • Unmarshals raw JSON configuration.
  • Executes Provision(ctx) and Validate() if the module implements those interfaces.

loadModuleInline(key, scope string, raw json.RawMessage)

Helper for inline-key modules where the module name appears inside the JSON object, such as {"handler": "file_server", ...}.

Module Registration and Ancestry Tracking

During loading, the context performs several bookkeeping operations:

  • Module tracking – Adds the module to ctx.moduleInstances[id].
  • App registration – If the module is an App, stores it in ctx.cfg.apps for retrieval via ctx.App().
  • Ancestry management – Pushes the module onto ctx.ancestry, enabling subsequent calls to identify the provisioning chain.

Access the current module lineage using:

ctx.Module()   // Returns the most recently provisioned module
ctx.Modules()  // Returns a copy of the full ancestry slice

Practical Usage Patterns

Accessing Loggers and Configuration

The context provides convenience methods for logging that automatically attribute entries to the current module:

func (ctx Context) Logger(module ...Module) *zap.Logger
func (ctx Context) Slogger() *slog.Logger

If no module is specified, Logger() uses ctx.Module(). When running outside of a configured environment (such as in tests), these methods create development loggers automatically.

Retrieve application modules using:

app, err := ctx.App("http")           // Loads or creates the HTTP app
app, err := ctx.AppIfConfigured("tls") // Errors if TLS app isn't configured

Registering Cleanup Functions

Modules that spawn background goroutines or hold resources must register cleanup logic:

func (m *MyModule) Provision(ctx caddy.Context) error {
    // Start background work
    go m.runBackgroundWork(ctx)
    
    // Register cleanup to stop the goroutine when context cancels
    ctx.OnCancel(func() { close(m.stopCh) })
    return nil
}

For graceful shutdown scenarios where you need access to the shutdown context:

ctx.OnExit(func(shutdownCtx context.Context) {
    // Perform graceful shutdown with timeout context
})

Summary

  • The Caddy context system wraps Go’s standard context.Context to provide module lifecycle management, dependency injection, and resource cleanup.
  • Context creation via NewContext() establishes isolated module registries and configures deterministic cleanup through wrappedCancel.
  • Module loading happens through LoadModule(), LoadModuleByID(), and inline loaders, which automatically execute Provision() and Validate() lifecycle hooks.
  • Ancestry tracking maintains a stack of provisioned modules, enabling ctx.Module() to identify the current module for logging and configuration scoping.
  • Cleanup management through OnCancel() and OnExit() ensures goroutines, file handles, and network connections release properly when configurations reload or the server shuts down.

Frequently Asked Questions

How does Caddy's context system differ from Go's standard context?

While Go’s context.Context provides cancellation signals and request-scoped values, Caddy’s Context struct embeds it and adds module lifecycle management, configuration access, and automatic resource cleanup. The Caddy version tracks every instantiated module in moduleInstances, maintains provisioning ancestry, and executes cleanup hooks when the context cancels, which standard Go contexts do not handle.

What is the module ancestry stack used for?

The ancestry slice in ctx.ancestry records the order in which modules are provisioned during configuration loading. This stack enables the ctx.Module() method to return the most recently provisioned module, which logging systems use to attribute log entries to the correct component. It also allows modules to understand their position in the dependency hierarchy during the provisioning phase.

How do I properly clean up resources when a Caddy module reloads?

Implement the CleanerUpper interface and register explicit cleanup functions using ctx.OnCancel(). When Caddy reloads its configuration, it cancels the old context, triggering the wrappedCancel sequence: it calls your OnCancel functions, then invokes Cleanup() on any module implementing CleanerUpper. For graceful shutdown scenarios where you need a timeout context, use ctx.OnExit() instead.

Can I access other Caddy apps from within my module?

Yes, use ctx.App(name) to retrieve or initialize another application module by name (such as "http" or "tls"). This method looks up the app in ctx.cfg.apps and returns the configured instance. If you want to ensure the app is actually configured before retrieving it, use ctx.AppIfConfigured(name), which returns an error if the specified app isn't present in the current configuration.

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 →