# How to Write a Custom Caddy Module: A Complete Guide to Extending Caddy

> Learn to write a custom Caddy module with this complete guide. Implement the caddy.Module interface and extend Caddy's functionality effectively. Start building now.

- Repository: [Caddy/caddy](https://github.com/caddyserver/caddy)
- Tags: how-to-guide
- Published: 2026-03-03

---

**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`](https://github.com/caddyserver/caddy/blob/main/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:

```go
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`](https://github.com/caddyserver/caddy/blob/main/modules.go), the `RegisterModule` function (lines 30-41) handles this storage:

```go
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.

```bash
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:

```go
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:

```go
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:

```go
func init() {
    caddy.RegisterModule(HelloHandler{})
}

```

This call adds your module to the global registry in [`modules.go`](https://github.com/caddyserver/caddy/blob/main/modules.go).

### Step 5: Implement Handler Logic

For HTTP handlers, implement the `caddyhttp.Handler` interface:

```go
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:

```go
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`](https://github.com/caddyserver/caddy/blob/main/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:

```go
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`](https://github.com/caddyserver/caddy/blob/main/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`** – Implement `Provision(ctx caddy.Context) error` to initialize resources after configuration unmarshaling. Use this for opening database connections, loading certificates, or compiling templates.
- **`Validator`** – Implement `Validate() error` to perform configuration sanity checks before Caddy starts serving traffic.
- **`CleanerUpper`** – Implement `Cleanup() error` to 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:

```go
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:

1. **Import the package** in [`modules/standard/imports.go`](https://github.com/caddyserver/caddy/blob/main/modules/standard/imports.go) (for official modules) or in your own [`main.go`](https://github.com/caddyserver/caddy/blob/main/main.go):

```go
import _ "github.com/yourname/caddy-myhandler"

```

The blank import triggers the `init()` function, which calls `caddy.RegisterModule`.

2. **Build Caddy** with your module:

```bash
go build -o caddy ./cmd/caddy

```

3. **Test using the Caddyfile** syntax:

```caddy
example.com {
    hello "Welcome to my custom module"
    respond "Done"
}

```

4. **Unit test** by creating module instances directly:

```go
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.Module` interface and provide a unique ID via `CaddyModule()`.
- **Registration** occurs through `caddy.RegisterModule` in an `init()` function, storing the module in the global registry defined in [`modules.go`](https://github.com/caddyserver/caddy/blob/main/modules.go).
- **HTTP handlers** must implement `caddyhttp.Handler` and its `ServeHTTP` method to process requests.
- **Caddyfile support** requires implementing `caddyfile.Unmarshaler` to parse directive syntax using the `Dispenser` API from [`caddyconfig/caddyfile/dispenser.go`](https://github.com/caddyserver/caddy/blob/main/caddyconfig/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`](https://github.com/caddyserver/caddy/blob/main/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`](https://github.com/caddyserver/caddy/blob/main/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`](https://github.com/caddyserver/caddy/blob/main/main.go) or in [`modules/standard/imports.go`](https://github.com/caddyserver/caddy/blob/main/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`](https://github.com/caddyserver/caddy/blob/main/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.