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()andValidate()are optional but recommended.- Register the plugin in
func init()usingcaddy.RegisterModule()— Caddy automatically recognizes the module when init is called.- For HTTP middleware, implement the
caddyhttp.MiddlewareHandlerinterface with theServeHTTP(w, r, next)method — always callnext.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-NAMEfor repositories,http.handlers.NAMEfor HTTP modules,dns.providers.NAMEfor 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.