Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/caddyserver/caddy/llms.txt

Use this file to discover all available pages before exploring further.

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.

What is a Module?

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
}

Module Information

Each module must provide metadata about itself:
modules.go:62-77
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 Namespaces

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.json
type ModuleID string
Namespace structure: <namespace>.<name>Top-level modules (apps) have no namespace, just a name like http or tls.

Common Namespaces

NamespacePurposeExample Modules
(empty)Appshttp, tls, pki
http.handlersHTTP handlersfile_server, reverse_proxy, rewrite
http.matchersRequest matchershost, path, header
tls.issuanceCertificate issuersacme, internal, zerossl
tls.certificatesCert loadersautomate, load_files, load_folders
caddy.storageStorage backendsfile_system, consul, s3
caddy.logging.encodersLog encodersjson, console, logfmt

Module Registration

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:
  • The module ID is empty or reserved
  • The constructor function is nil
  • The module is already registered

Example Registration

package myhandler

import "github.com/caddyserver/caddy/v2"

func init() {
    caddy.RegisterModule(Handler{})
}

type Handler struct {
    // Your fields here
}

func (Handler) CaddyModule() caddy.ModuleInfo {
    return caddy.ModuleInfo{
        ID:  "http.handlers.my_handler",
        New: func() caddy.Module { return new(Handler) },
    }
}

Module Lifecycle

When a module is loaded, it goes through several phases:
1

Instantiation

Caddy calls ModuleInfo.New() to create a new instance:
context.go:369
val := modInfo.New()
2

Unmarshaling

The module’s configuration is unmarshaled into the instance:
context.go:382-387
if len(rawMsg) > 0 {
    err := StrictUnmarshalJSON(rawMsg, &val)
    if err != nil {
        return nil, fmt.Errorf("decoding module config: %s: %v", modInfo, err)
    }
}
3

Provisioning

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)
            }
        }
    }
}

Loading Modules

Caddy provides the LoadModule method to load modules from configuration:
context.go:181
func (ctx Context) LoadModule(structPointer any, fieldName string) (any, error)

Supported Field Types

The LoadModule method supports several raw module types:
For a single module:
type MyConfig struct {
    HandlerRaw json.RawMessage `json:"handler,omitempty" caddy:"namespace=http.handlers inline_key=handler"`
}

val, err := ctx.LoadModule(cfg, "HandlerRaw")
handler := val.(caddyhttp.MiddlewareHandler)
For a list of modules:
type MyConfig struct {
    HandlersRaw []json.RawMessage `json:"handlers,omitempty" caddy:"namespace=http.handlers inline_key=handler"`
}

val, err := ctx.LoadModule(cfg, "HandlersRaw")
handlers := val.([]any)
for _, h := range handlers {
    handler := h.(caddyhttp.MiddlewareHandler)
}
For a map where keys are module names:
type MyConfig struct {
    AppsRaw caddy.ModuleMap `json:"apps,omitempty" caddy:"namespace="`
}

val, err := ctx.LoadModule(cfg, "AppsRaw")
apps := val.(map[string]any)
for name, app := range apps {
    // Use the app
}

Struct Tags

Modules are configured using struct tags:
modules.go:319-336
func ParseStructTag(tag string) (map[string]string, error) {
    results := make(map[string]string)
    pairs := strings.Split(tag, " ")
    for i, pair := range pairs {
        if pair == "" {
            continue
        }
        before, after, isCut := strings.Cut(pair, "=")
        if !isCut {
            return nil, fmt.Errorf("missing key in '%s' (pair %d)", pair, i)
        }
        results[before] = after
    }
    return results, nil
}
Required tags:
  • namespace - The module namespace to search (e.g., http.handlers)
Optional tags:
  • inline_key - The JSON key containing the module name (e.g., handler)
When using ModuleMap, the map key IS the module name, so inline_key is not needed.

Creating Custom Modules

Here’s a complete example of a custom HTTP handler module:
1

Define the Module

package greeting

import (
    "fmt"
    "net/http"
    
    "github.com/caddyserver/caddy/v2"
    "github.com/caddyserver/caddy/v2/modules/caddyhttp"
    "go.uber.org/zap"
)

func init() {
    caddy.RegisterModule(Greeting{})
}

type Greeting struct {
    Message string `json:"message,omitempty"`
    logger  *zap.Logger
}

func (Greeting) CaddyModule() caddy.ModuleInfo {
    return caddy.ModuleInfo{
        ID:  "http.handlers.greeting",
        New: func() caddy.Module { return new(Greeting) },
    }
}
2

Implement Provisioner

func (g *Greeting) Provision(ctx caddy.Context) error {
    g.logger = ctx.Logger()
    if g.Message == "" {
        g.Message = "Hello, World!"
    }
    return nil
}
3

Implement Validator

func (g Greeting) Validate() error {
    if len(g.Message) > 1000 {
        return fmt.Errorf("message too long (max 1000 chars)")
    }
    return nil
}
4

Implement Handler Interface

func (g Greeting) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
    g.logger.Info("serving greeting",
        zap.String("message", g.Message),
        zap.String("remote", r.RemoteAddr),
    )
    w.Write([]byte(g.Message))
    return nil
}
5

Add Interface Guards

var (
    _ caddy.Provisioner              = (*Greeting)(nil)
    _ caddy.Validator                = (*Greeting)(nil)
    _ caddyhttp.MiddlewareHandler    = (*Greeting)(nil)
)

Module Discovery

Caddy provides functions to discover registered modules:
modules.go:195-242
// GetModules returns all modules in the given scope/namespace
func GetModules(scope string) []ModuleInfo {
    modulesMu.RLock()
    defer modulesMu.RUnlock()

    scopeParts := strings.Split(scope, ".")
    if scope == "" {
        scopeParts = []string{}
    }

    var mods []ModuleInfo
iterateModules:
    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 modules
handlers := caddy.GetModules("http.handlers")

// Get all top-level app modules  
apps := caddy.GetModules("")

// Get all TLS certificate issuers
issuers := caddy.GetModules("tls.issuance")

Best Practices

1

Always Use Pointers

Module constructors should return pointers:
New: func() caddy.Module { return new(MyModule) }
2

Validate Configuration

Implement Validator to catch configuration errors early:
func (m MyModule) Validate() error {
    if m.Required == "" {
        return fmt.Errorf("required field is empty")
    }
    return nil
}
3

Clean Up Resources

Implement CleanerUpper if your module allocates resources:
func (m *MyModule) Cleanup() error {
    if m.conn != nil {
        return m.conn.Close()
    }
    return nil
}
4

Use Context Logger

Get a properly-configured logger from the context:
func (m *MyModule) Provision(ctx caddy.Context) error {
    m.logger = ctx.Logger()
    return nil
}
5

Add Interface Guards

Use compile-time interface guards to catch mistakes:
var (
    _ caddy.Provisioner = (*MyModule)(nil)
    _ caddy.Validator   = (*MyModule)(nil)
)
Common Pitfalls:
  • Forgetting to register the module in init()
  • Not returning pointers from constructors
  • Performing I/O in Provision() that should be in Start()
  • Not cleaning up resources in Cleanup()