How to Write a Custom Caddy Module: A Complete Guide to Extending Caddy
To write a custom Caddy module, create a Go type that implements the caddy.Module interface, register it using caddy.RegisterModule in an init() function, and implement additional interfaces like caddyhttp.Handler or caddyfile.Unmarshaler depending on your module's purpose.
Caddy’s architecture is fundamentally modular. Every feature—from TLS management to HTTP routing and storage backends—is implemented as a module that plugs into the core. The caddyserver/caddy repository uses a global registry defined in modules.go to discover and instantiate these modules at runtime. Understanding this registration mechanism is essential for extending Caddy with your own functionality.
Understanding Caddy’s Module Architecture
At the heart of Caddy’s plugin system is the caddy.Module interface. Any type that implements this interface can be registered as a Caddy module:
type Module interface {
CaddyModule() ModuleInfo
}
The CaddyModule() method must return a ModuleInfo struct containing a unique module ID and a constructor function. Module IDs follow the namespace pattern <category>.<name>, such as http.handlers.noop or http.handlers.custom_logger.
When a module’s package is imported, its init() function calls caddy.RegisterModule, which stores the module’s information in a global map. In modules.go, the RegisterModule function (lines 30-41) handles this storage:
func RegisterModule(instance Module) {
mod := instance.CaddyModule()
modulesMu.Lock()
defer modulesMu.Unlock()
modules[string(mod.ID)] = mod
}
During configuration loading, Caddy looks up modules by ID using GetModule, instantiates them via the stored constructor, and unmarshals configuration into the new instance.
Step-by-Step Guide to Writing a Custom Caddy Module
Step 1: Create the Go Package
Create a new Go package for your module. While you can place this anywhere importable, the convention for first-party modules is under modules/. For third-party plugins, use your own repository path.
mkdir -p modules/myhandler
cd modules/myhandler
go mod init github.com/yourname/caddy-myhandler
Step 2: Define the Module Struct
Define a struct that will hold your module’s configuration. Export fields that need to be configurable via JSON or Caddyfile:
type HelloHandler struct {
Message string `json:"message,omitempty"`
Times int `json:"times,omitempty"`
}
Step 3: Implement the CaddyModule Method
Implement the required interface method to provide the module ID and constructor:
func (HelloHandler) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{
ID: "http.handlers.hello",
New: func() caddy.Module { return new(HelloHandler) },
}
}
The ID must be unique. For HTTP handlers, use the http.handlers namespace.
Step 4: Register the Module
Add an init function to register the module when the package is imported:
func init() {
caddy.RegisterModule(HelloHandler{})
}
This call adds your module to the global registry in modules.go.
Step 5: Implement Handler Logic
For HTTP handlers, implement the caddyhttp.Handler interface:
func (h HelloHandler) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
for i := 0; i < h.Times; i++ {
fmt.Fprintln(w, h.Message)
}
return next.ServeHTTP(w, r)
}
The next parameter represents the next handler in the chain. Call next.ServeHTTP to continue processing, or return early to short-circuit.
Step 6: Add Caddyfile Support
To allow configuration via Caddyfile rather than JSON, implement the caddyfile.Unmarshaler interface:
func (h *HelloHandler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
d.Next() // consume directive name
if !d.Args(&h.Message) {
return d.ArgErr()
}
if d.NextArg() {
if _, err := fmt.Sscan(d.Val(), &h.Times); err != nil {
return err
}
}
return nil
}
This uses the caddyfile.Dispenser API defined in caddyconfig/caddyfile/dispenser.go to parse tokens sequentially.
Complete Working Example: A Hello World HTTP Handler
Here is the complete, runnable code for a custom HTTP handler module that writes a configurable message:
package hello
import (
"fmt"
"net/http"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
)
// Hello is a simple HTTP handler module.
type Hello struct {
Message string `json:"message,omitempty"`
}
func init() {
caddy.RegisterModule(Hello{})
}
// CaddyModule returns the Caddy module information.
func (Hello) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{
ID: "http.handlers.hello",
New: func() caddy.Module { return new(Hello) },
}
}
// UnmarshalCaddyfile implements caddyfile.Unmarshaler.
func (h *Hello) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
d.Next()
if !d.Args(&h.Message) {
return d.ArgErr()
}
return nil
}
// ServeHTTP implements caddyhttp.Handler.
func (h Hello) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
fmt.Fprint(w, h.Message)
return next.ServeHTTP(w, r)
}
// Interface guards
var (
_ caddy.Module = (*Hello)(nil)
_ caddyhttp.Handler = (*Hello)(nil)
_ caddyfile.Unmarshaler = (*Hello)(nil)
)
This implementation follows the exact pattern used by the built-in static_response handler in modules/caddyhttp/staticresp.go. The module ID http.handlers.hello places it in the HTTP handlers namespace, making it available for use in site blocks.
Lifecycle Hooks and Advanced Interfaces
Beyond basic registration, Caddy provides interfaces for modules that need initialization or cleanup:
Provisioner– ImplementProvision(ctx caddy.Context) errorto initialize resources after configuration unmarshaling. Use this for opening database connections, loading certificates, or compiling templates.Validator– ImplementValidate() errorto perform configuration sanity checks before Caddy starts serving traffic.CleanerUpper– ImplementCleanup() errorto release resources when the module is stopped or reloaded. This is critical for preventing goroutine leaks or file descriptor exhaustion during configuration reloads.
To implement these, add the corresponding methods to your type and include interface guards at the bottom of your file:
func (h *MyHandler) Provision(ctx caddy.Context) error {
// Initialize resources
return nil
}
func (h *MyHandler) Validate() error {
if h.RequiredField == "" {
return fmt.Errorf("required_field cannot be empty")
}
return nil
}
func (h *MyHandler) Cleanup() error {
// Close connections, stop goroutines
return nil
}
var (
_ caddy.Provisioner = (*MyHandler)(nil)
_ caddy.Validator = (*MyHandler)(nil)
_ caddy.CleanerUpper = (*MyHandler)(nil)
)
Building and Testing Your Module
To integrate your custom Caddy module into a Caddy build:
- Import the package in
modules/standard/imports.go(for official modules) or in your ownmain.go:
import _ "github.com/yourname/caddy-myhandler"
The blank import triggers the init() function, which calls caddy.RegisterModule.
- Build Caddy with your module:
go build -o caddy ./cmd/caddy
- Test using the Caddyfile syntax:
example.com {
hello "Welcome to my custom module"
respond "Done"
}
- Unit test by creating module instances directly:
func TestHelloHandler(t *testing.T) {
h := &Hello{Message: "test"}
req := httptest.NewRequest("GET", "/", nil)
rec := httptest.NewRecorder()
err := h.ServeHTTP(rec, req, nil)
if err != nil {
t.Fatal(err)
}
if rec.Body.String() != "test" {
t.Errorf("expected 'test', got %s", rec.Body.String())
}
}
Summary
- Caddy modules implement the
caddy.Moduleinterface and provide a unique ID viaCaddyModule(). - Registration occurs through
caddy.RegisterModulein aninit()function, storing the module in the global registry defined inmodules.go. - HTTP handlers must implement
caddyhttp.Handlerand itsServeHTTPmethod to process requests. - Caddyfile support requires implementing
caddyfile.Unmarshalerto parse directive syntax using theDispenserAPI fromcaddyconfig/caddyfile/dispenser.go. - Lifecycle hooks (
Provisioner,Validator,CleanerUpper) allow modules to initialize resources, validate configuration, and clean up during reloads. - Building requires only importing the package to trigger registration, then compiling Caddy normally.
Frequently Asked Questions
What is the caddy.Module interface?
The caddy.Module interface is the foundation of Caddy’s plugin system. It requires a single method CaddyModule() that returns a ModuleInfo struct containing a unique string ID (like http.handlers.custom) and a constructor function that returns a new instance of your module. This interface allows Caddy’s core in modules.go to discover and instantiate your code without hard dependencies.
How do I register a custom Caddy module?
You register a module by calling caddy.RegisterModule() inside an init() function in your package. This function, defined in modules.go lines 30-41, stores your module’s ModuleInfo in a global map keyed by the module ID. When your package is imported (even with a blank import like import _ "your/package"), the init() runs automatically, making the module available to Caddy’s configuration loader without requiring manual registration steps.
Can I write a custom Caddy module without modifying Caddy's source code?
Yes. You can develop a custom Caddy module in a separate Go module or repository. Simply implement the required interfaces, register the module in your package’s init() function, and then import your package into a custom Caddy build. You do not need to modify files within the caddyserver/caddy repository itself; the blank import of your package in your main.go or in modules/standard/imports.go is sufficient to include your module in the final binary.
How do I add Caddyfile support to my custom module?
To support Caddyfile configuration syntax, implement the caddyfile.Unmarshaler interface on your module type. This requires a method UnmarshalCaddyfile(d *caddyfile.Dispenser) error that uses the Dispenser API from caddyconfig/caddyfile/dispenser.go to parse tokens sequentially. Inside this method, use d.Next() to advance through tokens and d.Args() to capture arguments, returning d.ArgErr() if the syntax is invalid. Once implemented, users can configure your module directly in a Caddyfile using your registered directive name.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →