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*Configcontaining apps, storage, and logging configuration.ancestry– Maintains a stack of provisioned modules, enablingctx.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:
- Copies the parent’s configuration (
ctx.cfg) into the new context. - Creates a fresh
moduleInstancesmap to isolate module tracking. - Wraps the parent’s
ctx.Contextwithcontext.WithCancel. - Returns a
wrappedCancelfunction that orchestrates shutdown sequences.
Cancellation and Cleanup Hooks
When wrappedCancel executes, it triggers a deterministic cleanup sequence:
- Calls the underlying
context.CancelFunc. - Executes all functions registered via
ctx.OnCancel. - Iterates through
moduleInstancesand invokesCleanup()on any module implementing theCleanerUpperinterface.
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
modulesregistry frommodules.go. - Constructs the module via its
New()function. - Unmarshals raw JSON configuration.
- Executes
Provision(ctx)andValidate()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 inctx.cfg.appsfor retrieval viactx.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.Contextto provide module lifecycle management, dependency injection, and resource cleanup. - Context creation via
NewContext()establishes isolated module registries and configures deterministic cleanup throughwrappedCancel. - Module loading happens through
LoadModule(),LoadModuleByID(), and inline loaders, which automatically executeProvision()andValidate()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()andOnExit()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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →