Caddy Onboarding LabChapter III — The Plugin System: Modules, Registration, and Provisioning0/6Contents
Chapter III

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.


walkthrough

The generic Module interface

modules.go · lines 5477
lines 54–60

This 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.

lines 62–77

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}
step 1 of 2
walkthrough

RegisterModule: building the registry

modules.go · lines 138161
lines 138–139

RegisterModule 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.

lines 141–152

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.

lines 154–160

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}
step 1 of 3
From a JSON route to a live *FileServer
Global module registrymodules.go
Route JSON: "handle": [{"handler": "file_server"}]modules/caddyhttp/routes.go
Route.ProvisionHandlers -> ctx.LoadModulemodules/caddyhttp/routes.go
ctx.LoadModuleByID("http.handlers.file_server", rawJSON)context.go
Provision -> Validatecontext.go
*fileserver.FileServer as caddyhttp.MiddlewareHandlermodules/caddyhttp/routes.go
Figure I · Click a node to see what it does, where it lives in the code, and its labeled connections — the annotation opens beside it.
Figure II · state machineA module's life inside one Context
raw JSON is present for this fieldinstance implements ProvisionerProvision succeeds and instance implements ValidatorValidate succeeds (or was never implemented)Provision returns an errorthe containing Context is canceledNew()Config decodedProvisionedValidatedCleaned upIn use
Click a state above.
checkpoint

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?