Skip to main content
Caddy modules are Go libraries that extend Caddy’s functionality. Modules can add directives to the Caddyfile, implement new configuration adapters, create HTTP handlers, and even add new server types.

Understanding Caddy Modules

Modules are the building blocks of Caddy’s extensible architecture. Every module:
  • Implements the Module interface
  • Has a unique ModuleID (namespace)
  • Provides a constructor function
  • Gets compiled into Caddy as a Go package
Caddy modules are compile-time extensions, not runtime plugins. They must be compiled into the binary.

The Module Interface

Every Caddy module must implement the Module interface from modules.go:54:
type Module interface {
    // CaddyModule returns the module information
    CaddyModule() ModuleInfo
}
The ModuleInfo struct contains:
type ModuleInfo struct {
    // ID is the "full name" of the module
    // Must be unique and properly namespaced
    ID ModuleID

    // New returns a pointer to a new, empty instance
    New func() Module
}

Module Lifecycle

When a module is loaded by a host module, the following sequence occurs:
1

Module Instantiation

ModuleInfo.New() is called to get a new instance of the module.
2

Configuration Unmarshaling

The module’s configuration is unmarshaled into that instance from JSON.
3

Provisioning

If the module implements Provisioner, the Provision() method is called.
type Provisioner interface {
    Provision(Context) error
}
4

Validation

If the module implements Validator, the Validate() method is called.
type Validator interface {
    Validate() error
}
5

Type Assertion

The module is type-asserted to the interface expected by the host module (e.g., caddyhttp.MiddlewareHandler).
6

Cleanup

When the context is canceled, if the module implements CleanerUpper, its Cleanup() method is called.
type CleanerUpper interface {
    Cleanup() error
}

Module Registration

Modules must be registered during the init phase using RegisterModule() from modules.go:138:
func init() {
    caddy.RegisterModule(YourModule{})
}
Registration happens at runtime init. The function will panic if the module’s info is incomplete or if the module is already registered.

Creating Your First Module

Here’s a complete example of an HTTP handler module based on the tracing module at modules/caddyhttp/tracing/module.go:15:
package myhandler

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

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

// MyHandler implements an HTTP handler
type MyHandler struct {
    // Configuration fields (exported, JSON-tagged)
    Message string `json:"message,omitempty"`
    
    // Internal state (unexported)
    logger *zap.Logger
}

// CaddyModule returns the Caddy module information
func (MyHandler) CaddyModule() caddy.ModuleInfo {
    return caddy.ModuleInfo{
        ID:  "http.handlers.my_handler",
        New: func() caddy.Module { return new(MyHandler) },
    }
}

// Provision sets up the module
func (m *MyHandler) Provision(ctx caddy.Context) error {
    m.logger = ctx.Logger()
    if m.Message == "" {
        m.Message = "Hello from MyHandler!"
    }
    return nil
}

// Validate ensures the module configuration is valid
func (m *MyHandler) Validate() error {
    if m.Message == "" {
        return fmt.Errorf("message cannot be empty")
    }
    return nil
}

// ServeHTTP implements caddyhttp.MiddlewareHandler
func (m MyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
    m.logger.Info("handling request", zap.String("message", m.Message))
    w.Header().Set("X-My-Handler", m.Message)
    return next.ServeHTTP(w, r)
}

// Interface guards - compile-time checks
var (
    _ caddy.Provisioner           = (*MyHandler)(nil)
    _ caddy.Validator             = (*MyHandler)(nil)
    _ caddyhttp.MiddlewareHandler = (*MyHandler)(nil)
)

Module Configuration

JSON Configuration

Modules are configured using JSON. The struct tags control how configuration is unmarshaled:
type MyModule struct {
    // Required field
    Host string `json:"host"`
    
    // Optional field with omitempty
    Port int `json:"port,omitempty"`
    
    // Nested module (another Caddy module)
    HandlerRaw json.RawMessage `json:"handler,omitempty" caddy:"namespace=http.handlers inline_key=handler"`
}

Strict Unmarshaling

Caddy uses strict JSON unmarshaling from modules.go:342 to catch configuration errors:
func StrictUnmarshalJSON(data []byte, v any) error {
    dec := json.NewDecoder(bytes.NewReader(data))
    dec.DisallowUnknownFields()
    return dec.Decode(v)
}
Unknown fields in configuration will cause errors.

Best Practices

Keep initialization side-effect free: The New() function and CaddyModule() method must have no side effects.
Use Provision for setup: Put all initialization logic in Provision(), not in New().
Implement Cleanup: If your module allocates resources, implements CleanerUpper to prevent memory leaks.
Add interface guards: Use compile-time interface checks at the end of your file:
var (
    _ caddy.Provisioner = (*MyModule)(nil)
    _ caddy.Validator   = (*MyModule)(nil)
)
Follow naming conventions: Use lowercase with underscores for module IDs (e.g., http.handlers.file_server).

Common Module Types

HTTP Handlers

Implement caddyhttp.MiddlewareHandler:
type MiddlewareHandler interface {
    ServeHTTP(http.ResponseWriter, *http.Request, Handler) error
}
Namespace: http.handlers.*

HTTP Matchers

Match requests based on criteria. Namespace: http.matchers.*

TLS Certificate Loaders

Load TLS certificates from various sources. Namespace: tls.certificates.*

Storage Backends

Implement certmagic.Storage for storing TLS certificates. Namespace: caddy.storage.*

Config Adapters

Convert other config formats to Caddy JSON. Namespace: caddy.config_adapters.*

Apps

Top-level applications that Caddy runs. Namespace: empty (e.g., http, tls)

Working with Context

The Context passed to Provision() provides:
  • Logger: ctx.Logger() returns a *zap.Logger
  • Module loading: ctx.LoadModule() loads nested modules
  • App access: ctx.App("appName") gets app instances
  • Module info: ctx.Module() returns current module info

Next Steps

Plugin Tutorial

Follow a step-by-step tutorial to build a complete plugin

Module Namespaces

Learn about the module namespace system

Custom Builds

Build Caddy with your custom modules

Contributing

Contribute your module to Caddy

Build docs developers (and LLMs) love