Learn how Caddy’s module system enables extensibility and customization
Caddy’s module system is the foundation of its extensibility. Every component in Caddy—from HTTP handlers to TLS certificate issuers—is implemented as a module. This architecture allows you to extend Caddy with custom functionality or use third-party modules.
A module is any Go type that implements the Module interface:
modules.go:54-60
type Module interface { // This method indicates that the type is a Caddy module. // The returned ModuleInfo must have both a name and a constructor function. CaddyModule() ModuleInfo}
type ModuleInfo struct { // ID is the "full name" of the module. // It must be unique and properly namespaced. ID ModuleID // New returns a pointer to a new, empty instance of the module's type. // This method must not have any side-effects. New func() Module}
Module IDs follow a hierarchical naming convention:
modules.go:79-98
// ModuleID is a string that uniquely identifies a Caddy module.// It consists of dot-separated labels which form a simple hierarchy.//// Examples of valid IDs:// - http// - http.handlers.file_server// - caddy.logging.encoders.jsontype ModuleID string
Namespace structure:<namespace>.<name>Top-level modules (apps) have no namespace, just a name like http or tls.
Modules must be registered before Caddy can use them:
modules.go:130-161
func RegisterModule(instance Module) { mod := instance.CaddyModule() if mod.ID == "" { panic("module ID missing") } if mod.ID == "caddy" || mod.ID == "admin" { panic(fmt.Sprintf("module ID '%s' is reserved", mod.ID)) } if mod.New == nil { panic("missing ModuleInfo.New") } if val := mod.New(); val == nil { panic("ModuleInfo.New must return a non-nil module instance") } modulesMu.Lock() defer modulesMu.Unlock() if _, ok := modules[string(mod.ID)]; ok { panic(fmt.Sprintf("module already registered: %s", mod.ID)) } modules[string(mod.ID)] = mod}
Module registration typically happens in init() functions and will panic if:
If the module implements Provisioner, its Provision() method is called:
modules.go:288-298
type Provisioner interface { Provision(Context) error}
context.go:418-430
if prov, ok := val.(Provisioner); ok { err = prov.Provision(ctx) if err != nil { // Cleanup on error if cleanerUpper, ok := val.(CleanerUpper); ok { cleanerUpper.Cleanup() } return nil, fmt.Errorf("provision %s: %v", modInfo, err) }}
4
Validation
If the module implements Validator, its Validate() method is called:
modules.go:300-307
type Validator interface { Validate() error}
context.go:433-444
if validator, ok := val.(Validator); ok { err = validator.Validate() if err != nil { // Cleanup on error if cleanerUpper, ok := val.(CleanerUpper); ok { cleanerUpper.Cleanup() } return nil, fmt.Errorf("%s: invalid configuration: %v", modInfo, err) }}
5
Usage
The module is now ready to be used. It’s typically type-asserted to a specific interface expected by the host module.
6
Cleanup
When the config is unloaded, if the module implements CleanerUpper, its Cleanup() method is called:
modules.go:309-317
type CleanerUpper interface { Cleanup() error}
context.go:75-83
for modName, modInstances := range newCtx.moduleInstances { for _, inst := range modInstances { if cu, ok := inst.(CleanerUpper); ok { err := cu.Cleanup() if err != nil { log.Printf("[ERROR] %s (%p): cleanup: %v", modName, inst, err) } } }}
Caddy provides functions to discover registered modules:
modules.go:195-242
// GetModules returns all modules in the given scope/namespacefunc GetModules(scope string) []ModuleInfo { modulesMu.RLock() defer modulesMu.RUnlock() scopeParts := strings.Split(scope, ".") if scope == "" { scopeParts = []string{} } var mods []ModuleInfoiterateModules: for id, m := range modules { modParts := strings.Split(id, ".") // match only the next level of nesting if len(modParts) != len(scopeParts)+1 { continue } // specified parts must be exact matches for i := range scopeParts { if modParts[i] != scopeParts[i] { continue iterateModules } } mods = append(mods, m) } // make return value deterministic sort.Slice(mods, func(i, j int) bool { return mods[i].ID < mods[j].ID }) return mods}
Examples:
// Get all HTTP handler moduleshandlers := caddy.GetModules("http.handlers")// Get all top-level app modules apps := caddy.GetModules("")// Get all TLS certificate issuersissuers := caddy.GetModules("tls.issuance")