The Plugin System: Modules, Registration, and Provisioning
Every long-lived piece of Caddy — the http app, the file_server handler, and dozens of others — is a "Caddy module": a Go type that implements a two-method interface and registers itself under a namespaced ID like http.handlers.file_server. This module explains that generic Module/RegisterModule contract and then follows a JSON config value through caddy.Context.LoadModule/LoadModuleByID to see how a raw byte string becomes a live, provisioned, type-asserted Go struct. Once you've seen this mechanism, every other CaddyModule() call you meet in the codebase is just another instance of the same pattern.
The generic Module interface
modules.go · lines 54–77This is the entire generic contract every Caddy module must satisfy: one method, CaddyModule(), returning metadata about itself. There's no marker interface, no annotation, no registration file to edit by hand — implementing this method is what makes a type a module at all.
ModuleInfo is the metadata that CaddyModule() hands back: a namespaced ID string and a New closure that produces a fresh, empty instance. New is deliberately inert — no initialization happens here; that's reserved for a later Provision() step, so that just asking "what modules exist" never has side effects.
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. This method // must not have any side-effects. CaddyModule() ModuleInfo}// ModuleInfo represents a registered Caddy module.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, // and no other initialization should // occur within it. Any initialization // of the returned value should be done // in a Provision() method (see the // Provisioner interface). New func() Module}
RegisterModule: building the registry
modules.go · lines 138–161RegisterModule takes a throwaway zero-value instance — FileServer{}, App{} — purely so it can call CaddyModule() on it and read the metadata; the instance itself is discarded immediately after.
Before anything is stored, the metadata contract is enforced once: the ID must be set (and not one of the two reserved core IDs), and New must actually be a working constructor. Checking this at registration time means every later lookup can simply trust the registry without re-validating.
Only then does it take a lock and insert the ModuleInfo into the package-level modules map, keyed by the full ID string. This map is the registry every LoadModuleByID call reads from later; registering the same ID twice panics rather than silently overwriting, so a naming collision fails loudly at startup instead of quietly at runtime.
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}
Check your understanding
Answered in place — nothing is graded, everything is explained. 0 / 3 passed
What determines a module's full namespaced ID, such as "http.handlers.file_server"?
When a module implementing both Provisioner and Validator is loaded via LoadModuleByID, in what order do their methods run?
What happens if two different types are registered with the same module ID via RegisterModule?