Creating Plugins #

Creating a Caddy plugin requires basic Go programming knowledge, but Caddy’s plugin architecture is well designed so simple plugins can be built in a short time. Every plugin is a Go module implementing a specific interface defined by Caddy.

Plugin Types You Can Create #

HTTP Handler    → Processes HTTP requests/responses
                  Example: auth middleware, request transformer, rate limiter

HTTP Middleware → A wrapper running before/after other handlers
                  Example: security headers, custom request logger

Provisioner     → Setup that runs when the configuration is loaded
                  Example: database connections, loading config from an external source

Validator       → Validates configuration before use
                  Example: checking whether a file exists, validating formats

DNS Provider    → Provider for the DNS-01 ACME challenge
                  Example: a plugin for a DNS provider that doesn't exist yet

Storage         → TLS certificate storage backend
                  Example: storing certificates in Redis, Vault, S3

Basic Plugin Structure #

my-caddy-plugin/
├── go.mod
├── go.sum
├── plugin.go        ← The plugin's main logic
├── caddyfile.go     ← Parser for Caddyfile syntax (optional)
└── README.md

Creating an HTTP Handler Plugin #

Here’s a simple plugin example that adds a custom header to every response:

// plugin.go
package caddyheaderinjector

import (
    "fmt"
    "net/http"

    "github.com/caddyserver/caddy/v2"
    "github.com/caddyserver/caddy/v2/modules/caddyhttp"
)

func init() {
    // Register the module to the Caddy registry
    caddy.RegisterModule(HeaderInjector{})
}

// HeaderInjector is a plugin that adds custom headers
type HeaderInjector struct {
    // This field can be configured from the Caddyfile or JSON
    Headers map[string]string `json:"headers,omitempty"`
}

// CaddyModule returns the module information
// Format: "http.handlers.NAME"
func (HeaderInjector) CaddyModule() caddy.ModuleInfo {
    return caddy.ModuleInfo{
        ID:  "http.handlers.header_injector",
        New: func() caddy.Module { return new(HeaderInjector) },
    }
}

// Provision is called when the configuration is loaded
// Use it for setup: DB connections, loading files, etc.
func (h *HeaderInjector) Provision(ctx caddy.Context) error {
    // Validate the configuration
    if len(h.Headers) == 0 {
        return fmt.Errorf("no headers configured")
    }
    return nil
}

// Validate is called after Provision for additional validation
func (h *HeaderInjector) Validate() error {
    return nil
}

// ServeHTTP is the main handler — called for every request
func (h HeaderInjector) ServeHTTP(
    w http.ResponseWriter,
    r *http.Request,
    next caddyhttp.Handler,
) error {
    // Add the headers to the response
    for key, value := range h.Headers {
        w.Header().Set(key, value)
    }
    
    // Forward to the next handler
    return next.ServeHTTP(w, r)
}

// Interface check — make sure all interfaces are implemented
var (
    _ caddy.Module                = (*HeaderInjector)(nil)
    _ caddy.Provisioner           = (*HeaderInjector)(nil)
    _ caddy.Validator             = (*HeaderInjector)(nil)
    _ caddyhttp.MiddlewareHandler = (*HeaderInjector)(nil)
)

go.mod for the Plugin #

// go.mod
module github.com/username/caddy-header-injector

go 1.21

require (
    github.com/caddyserver/caddy/v2 v2.8.4
)

Adding Caddyfile Support #

// caddyfile.go
package caddyheaderinjector

import (
    "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
    "github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
    "github.com/caddyserver/caddy/v2/modules/caddyhttp"
)

func init() {
    // Register the Caddyfile directive
    httpcaddyfile.RegisterHandlerDirective("header_injector", parseCaddyfile)
}

// parseCaddyfile parses the configuration from the Caddyfile
func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) {
    var hi HeaderInjector
    hi.Headers = make(map[string]string)
    
    // Parse the configuration block
    // header_injector {
    //     X-Custom-Header  "value"
    //     X-Another-Header "another-value"
    // }
    for h.NextBlock(0) {
        key := h.Val()
        var value string
        if !h.Args(&value) {
            return nil, h.ArgErr()
        }
        hi.Headers[key] = value
    }
    
    return &hi, nil
}

Using the Plugin in the Caddyfile #

example.com {
    header_injector {
        X-App-Version    "1.0.0"
        X-Environment    "production"
        X-Powered-By     "My Stack"
    }
    
    reverse_proxy backend:3000
}

Testing the Plugin #

// plugin_test.go
package caddyheaderinjector

import (
    "net/http"
    "net/http/httptest"
    "testing"
    
    "github.com/caddyserver/caddy/v2/modules/caddyhttp"
)

func TestHeaderInjector(t *testing.T) {
    // Set up the plugin
    hi := &HeaderInjector{
        Headers: map[string]string{
            "X-Test-Header": "test-value",
        },
    }
    
    // Set up the test request and recorder
    req := httptest.NewRequest("GET", "/", nil)
    w := httptest.NewRecorder()
    
    // The next handler that returns 200
    nextHandler := caddyhttp.HandlerFunc(func(
        w http.ResponseWriter,
        r *http.Request,
    ) error {
        w.WriteHeader(200)
        return nil
    })
    
    // Run the plugin
    err := hi.ServeHTTP(w, req, nextHandler)
    
    // Assertions
    if err != nil {
        t.Fatalf("unexpected error: %v", err)
    }
    
    result := w.Result()
    if result.Header.Get("X-Test-Header") != "test-value" {
        t.Errorf("expected header X-Test-Header: test-value, got: %s",
            result.Header.Get("X-Test-Header"))
    }
}
# Run the tests
go test ./...

# Test with the race detector
go test -race ./...

Building and Testing with xcaddy #

# Test the plugin directly with xcaddy
xcaddy build \
    --with github.com/username/caddy-header-injector=./

# Or from a local path during development
xcaddy build \
    --with github.com/username/caddy-header-injector=/path/to/plugin

# Verify the module is registered
./caddy list-modules | grep header_injector
# http.handlers.header_injector

# Test the configuration
cat > test.Caddyfile << 'EOF'
localhost:8080 {
    header_injector {
        X-Test "hello from plugin"
    }
    respond "OK" 200
}
EOF

./caddy run --config test.Caddyfile

# Test from another terminal
curl -I http://localhost:8080/
# Should have: X-Test: hello from plugin

Publishing the Plugin #

# 1. Push to GitHub with a clear name
# Naming convention format: caddy-NAME or caddy-CATEGORY-NAME
# github.com/username/caddy-header-injector

# 2. Tag the version according to semantic versioning
git tag v0.1.0
git push origin v0.1.0

# 3. Make sure go.mod is valid
go mod tidy
go mod verify

# 4. Register in the caddyserver community
# Create a post at https://caddy.community/
# Tags: plugins, module

# 5. Add to the README:
# xcaddy build --with github.com/username/caddy-header-injector

Storage-Type Plugin #

A simple example of a storage plugin that stores certificates in memory (for testing):

// memory_storage.go
package caddymemstorage

import (
    "sync"
    "github.com/caddyserver/caddy/v2"
    "github.com/caddyserver/certmagic"
)

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

type MemStorage struct {
    mu   sync.RWMutex
    data map[string][]byte
}

func (MemStorage) CaddyModule() caddy.ModuleInfo {
    return caddy.ModuleInfo{
        ID:  "caddy.storage.memory",
        New: func() caddy.Module { return &MemStorage{data: make(map[string][]byte)} },
    }
}

// Implement all the certmagic.Storage methods...

Summary #

  • A Caddy plugin is a Go module implementing specific interfaces — CaddyModule() is mandatory, Provision() and Validate() are optional but recommended.
  • Register the plugin in func init() using caddy.RegisterModule() — Caddy automatically recognizes the module when init is called.
  • For HTTP middleware, implement the caddyhttp.MiddlewareHandler interface with the ServeHTTP(w, r, next) method — always call next.ServeHTTP(w, r) to forward to the next handler.
  • Add Caddyfile support by registering a directive parser in httpcaddyfile.RegisterHandlerDirective() — this makes the plugin configurable from the Caddyfile, not just JSON.
  • Test with xcaddy build –with github.com/user/plugin=./local/path for fast iteration during development without pushing to GitHub first.
  • Follow the naming conventions: caddy-NAME for repositories, http.handlers.NAME for HTTP modules, dns.providers.NAME for DNS providers.

Example: Request ID Plugin #

A simpler plugin — generating and injecting a request ID:

// requestid/plugin.go
package requestid

import (
    "crypto/rand"
    "encoding/hex"
    "net/http"
    
    "github.com/caddyserver/caddy/v2"
    "github.com/caddyserver/caddy/v2/modules/caddyhttp"
)

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

type RequestID struct {
    Header string `json:"header,omitempty"`
}

func (RequestID) CaddyModule() caddy.ModuleInfo {
    return caddy.ModuleInfo{
        ID:  "http.handlers.request_id",
        New: func() caddy.Module { return &RequestID{} },
    }
}

func (r *RequestID) Provision(_ caddy.Context) error {
    if r.Header == "" {
        r.Header = "X-Request-ID"
    }
    return nil
}

func (ri RequestID) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
    b := make([]byte, 16)
    rand.Read(b)
    id := hex.EncodeToString(b)
    r.Header.Set(ri.Header, id)
    w.Header().Set(ri.Header, id)
    return next.ServeHTTP(w, r)
}

This plugin: (1) generates a random ID, (2) sets it as a request header to the backend, (3) adds it to the response header — enabling log correlation between Caddy and the backend.

← Previous: Caddy DNS   Next: Use Cases →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact