Extending Caddy
Caddy is easy to extend because of its modular architecture. Most kinds of Caddy extensions (or plugins) are known as modules if they extend or plug into Caddy’s configuration structure. To be clear, Caddy modules are distinct from Go modules (but they are also Go modules).
Prerequisites:
- Basic understanding of Caddy’s architecture
- Go language proficiency
go
xcaddy
Quick Start
A Caddy module is any named type that registers itself as a Caddy module when its package is imported. Crucially, a module always implements the caddy.Module interface, which provides its name and a constructor function.
In a new Go module, paste the following template into a Go file and customize your package name, type name, and Caddy module ID:
package mymodule
import "github.com/caddyserver/caddy/v2"
func init() {
caddy.RegisterModule(Gizmo{})
}
// Gizmo is an example; put your own type here.
type Gizmo struct {
}
// CaddyModule returns the Caddy module information.
func (Gizmo) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{
ID: "foo.gizmo",
New: func() caddy.Module { return new(Gizmo) },
}
}
Then run this command from your project’s directory, and you should see your module in the list:
xcaddy list-modules
...
foo.gizmo
...
The xcaddy
command is an important part of every module developer’s workflow. It compiles Caddy with your plugin, then runs it with the given arguments. It discards the temporary binary each time (similar to go run
).
Congratulations, your module registers with Caddy and can be used in Caddy’s config document in whatever places use modules in the same namespace.
Under the hood, xcaddy
is simply making a new Go module that requires both Caddy and your plugin (with an appropriate replace
to use your local development version), then adds an import to ensure it is compiled in:
import _ "github.com/example/mymodule"
Module Basics
Caddy modules:
- Implement the
caddy.Module
interface to provide an ID and constructor - Have a unique name in the proper namespace
- Usually satisfy some interface(s) that are meaningful to the host module for that namespace
Host modules (or parent modules) are modules which load/initialize other modules. They typically define namespaces for guest modules.
Guest modules (or child modules) are modules which get loaded or initialized. All modules are guest modules.
Module IDs
Each Caddy module has a unique ID, consisting of a namespace and name:
- A complete ID looks like
foo.bar.module_name
- The namespace would be
foo.bar
- The name would be
module_name
which must be unique in its namespace
Module IDs must use snake_case
convention.
Namespaces
Namespaces are like classes, i.e. a namespace defines some functionality that is common among all modules within it. For example, we can expect that all modules within the http.handlers
namespace are HTTP handlers. It follows that a host module may type-assert guest modules in that namespace from interface{}
types into a more specific, useful type such as caddyhttp.MiddlewareHandler
.
A guest module must be properly namespaced in order for it to be recognized by a host module because host modules will ask Caddy for modules within a certain namespace to provide the functionality desired by the host module. For example, if you were to write an HTTP handler module called gizmo
, your module’s name would be http.handlers.gizmo
, because the http
app will look for handlers in the http.handlers
namespace.
Put another way, Caddy modules are expected to implement certain interfaces depending on their module namespace. With this convention, module developers can say intuitive things such as, “All modules in the http.handlers
namespace are HTTP handlers.” More technically, this usually means, “All modules in the http.handlers
namespace implement the caddyhttp.MiddlewareHandler
interface.” Because that method set is known, the more specific type can be asserted and used.
View a table mapping all the standard Caddy namespaces to their Go types.
The caddy
and admin
namespaces are reserved and cannot be app names.
To write modules which plug into 3rd-party host modules, consult those modules for their namespace documentation.
Names
The name within a namespace is significant and highly visible to users, but is not particularly important, as long as it is unique, concise, and makes sense for what it does.
App Modules
Apps are modules with an empty namespace, and which conventionally become their own top-level namespace. App modules implement the caddy.App interface.
These modules appear in the "apps"
property of the top-level of Caddy’s config:
{
"apps": {}
}
Example apps are http
and tls
. Theirs is the empty namespace.
Guest modules written for these apps should be in a namespace derived from the app name. For example, HTTP handlers use the http.handlers
namespace and TLS certificate loaders use the tls.certificates
namespace.
Module Implementation
A module can be virtually any type, but structs are the most common because they can hold user configuration.
Configuration
Most modules require some configuration. Caddy takes care of this automatically, as long as your type is compatible with JSON. Thus, if a module is a struct type, it will need struct tags on its fields, which should use snake_casing
according to Caddy convention:
type Gizmo struct {
MyField string `json:"my_field,omitempty"`
Number int `json:"number,omitempty"`
}
Using struct tags in this way will ensure that config properties are consisently named across all of Caddy.
When a module is initialized, it will already have its configuration filled out. It is also possible to perform additional provisioning and validation steps after a module is initialized.
Module Lifecycle
A module’s life begins when it is loaded by a host module. The following happens:
New()
is called to get an instance of the module’s value.- The module’s configuration is unmarshaled into that instance.
- If the module is a caddy.Provisioner, the
Provision()
method is called. - If the module is a caddy.Validator, the
Validate()
method is called. - At this point, the host module is given the loaded guest module as an
interface{}
value, so the host module will usually type-assert the guest module into a more useful type. Check the documentation for the host module to know what is required of a guest module in its namespace, e.g. what methods need to be implemented. - When a module is no longer needed, and if it is a caddy.CleanerUpper, the
Cleanup()
method is called.
Note that multiple loaded instances of your module may overlap at a given time! During config changes, new modules are started before the old ones are stopped. Be sure to use global state carefully. Use the caddy.UsagePool type to help manage global state across module loads.
Provisioning
A module’s configuration will be unmarshaled into its value automatically. This means, for example, that struct fields will be filled out for you.
However, if your module requires additional provisioning steps, you can implement the (optional) caddy.Provisioner interface:
// Provision sets up the module.
func (g *Gizmo) Provision(ctx caddy.Context) error {
// TODO: set up the module
return nil
}
This is typically where host modules will load their guest/child modules, but it can be used for pretty much anything. Module provisioning is done in an arbitrary order.
A module may access other apps by calling ctx.App()
, but modules must not have circular dependencies. In other words, a module loaded by the http
app cannot depend on the tls
app if a module loaded by the tls
app depends on the http
app. (Very similar to rules forbidding import cycles in Go.)
Additionally, you should avoid performing expensive operations in Provision
, since provisioning is performed even if a config is only being validated. When in the provisioning phase, do not expect that the module will actually be used.
Logs
If your module needs logging, do not use log.Print*()
from the Go standard library. In other words, do not use Go’s global logger. Caddy uses high-performance, highly flexible, structured logging with zap.
To emit logs, get a logger in your module’s Provision method:
func (g *Gizmo) Provision(ctx caddy.Context) error {
g.logger = ctx.Logger(g) // g.logger is a *zap.Logger
}
Then you can emit structured, leveled logs using g.logger
. See zap’s godoc for details.
Validating
Modules which would like to validate their configuration may do so by satisfying the (optional) caddy.Validator
interface:
// Validate validates that the module has a usable config.
func (g Gizmo) Validate() error {
// TODO: validate the module's setup
return nil
}
Validate should be a read-only function. It is run after the Provision()
method.
Interface guards
Caddy module behavior is implicit because Go interfaces are satisfied implicitly. Simply adding the right methods to your module’s type is all it takes to make or break your module’s correctness. Thus, making a typo or getting the method signature wrong can lead to unexpected (lack of) behavior.
Fortunately, there is an easy, no-overhead, compile-time check you can add to your code to ensure you’ve added the right methods. These are called interface guards:
var _ InterfaceName = (*YourType)(nil)
Replace InterfaceName
with the interface you intend to satisfy, and YourType
with the name of your module’s type.
For example, an HTTP handler such as the static file server might satisfy multiple interfaces:
// Interface guards
var (
_ caddy.Provisioner = (*FileServer)(nil)
_ caddyhttp.MiddlewareHandler = (*FileServer)(nil)
)
This prevents the program from compiling if *FileServer
does not satisfy those interfaces.
Without interface guards, confusing bugs can slip in. For example, if your module must provision itself before being used but your Provision()
method has a mistake (e.g. misspelled or wrong signature), provisioning will never happen, leading to head-scratching. Interface guards are super easy and can prevent that. They usually go at the bottom of the file.
Host Modules
A module becomes a host module when it loads its own guest modules. This is useful if a piece of the module’s functionality can be implemented in different ways.
A host module is almost always a struct. Normally, supporting a guest module requires two struct fields: one to hold its raw JSON, and another to hold its decoded value:
type Gizmo struct {
GadgetRaw json.RawMessage `json:"gadget,omitempty" caddy:"namespace=foo.gizmo.gadgets inline_key=gadgeter"`
Gadget Gadgeter `json:"-"`
}
The first field (GadgetRaw
in this example) is where the raw, unprovisioned JSON form of the guest module can be found.
The second field (Gadget
) is where the final, provisioned value will eventually be stored. Since the second field is not user-facing, we exclude it from JSON with a struct tag. (You could also unexport it if it is not needed by other packages, and then no struct tag is needed.)
Caddy struct tags
The caddy
struct tag on the raw module field helps Caddy to know the namespace and name (comprising the complete ID) of the module to load. It is also used for generating documentation.
The struct tag has a very simple format: key1=val1 key2=val2 ...
For module fields, the struct tag will look like:
`caddy:"namespace=foo.bar inline_key=baz"`
The namespace=
part is required. It defines the namespace in which to look for the module.
The inline_key=
part is only used if the module’s name will be found inline with the module itself; this implies that the value is an object where one of the keys is the inline key, and its value is the name of the module. If omitted, then the field type must be a caddy.ModuleMap
or []caddy.ModuleMap
, where the map key is the module name.
Loading guest modules
To load a guest module, call ctx.LoadModule()
during the provision phase:
// Provision sets up g and loads its gadget.
func (g *Gizmo) Provision(ctx caddy.Context) error {
if g.GadgetRaw != nil {
val, err := ctx.LoadModule(g, "GadgetRaw")
if err != nil {
return fmt.Errorf("loading gadget module: %v", err)
}
g.Gadget = val.(Gadgeter)
}
return nil
}
Note that the LoadModule()
call takes a pointer to the struct and the field name as a string. Weird, right? Why not just pass the struct field directly? It’s because there are a few different ways to load modules depending on the layout of the config. This method signature allows Caddy to use reflection to figure out the best way to load the module and, most importantly, read its struct tags.
If a guest module must explicitly be set by the user, you should return an error if the Raw field is nil or empty before trying to load it.
Notice how the loaded module is type-asserted: g.Gadget = val.(Gadgeter)
- this is because the returned val
is a interface{}
type which is not very useful. However, we expect that all modules in the declared namespace (foo.gizmo.gadgets
from the struct tag in our example) implement the Gadgeter
interface, so this type assertion is safe, and then we can use it!
If your host module defines a new namespace, be sure to document both that namespace and its Go type(s) for developers like we have done here.
Complete Example
Let’s suppose we want to write an HTTP handler module. This will be a contrived middleware for demonstration purposes which prints the visitor’s IP address to a stream on every HTTP request.
We also want it to be configurable via the Caddyfile, because most people prefer to use the Caddyfile in non-automated situations. We do this by registering a Caddyfile handler directive, which is a kind of directive that can add a handler to the HTTP route. We also implement the caddyfile.Unmarshaler
interface. By adding these few lines of code, this module can be configured with the Caddyfile! For example: visitor_ip stdout
.
Here is the code for such a module, with explanatory comments:
package visitorip
import (
"fmt"
"io"
"net/http"
"os"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
"github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
)
func init() {
caddy.RegisterModule(Middleware{})
httpcaddyfile.RegisterHandlerDirective("visitor_ip", parseCaddyfile)
}
// Middleware implements an HTTP handler that writes the
// visitor's IP address to a file or stream.
type Middleware struct {
// The file or stream to write to. Can be "stdout"
// or "stderr".
Output string `json:"output,omitempty"`
w io.Writer
}
// CaddyModule returns the Caddy module information.
func (Middleware) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{
ID: "http.handlers.visitor_ip",
New: func() caddy.Module { return new(Middleware) },
}
}
// Provision implements caddy.Provisioner.
func (m *Middleware) Provision(ctx caddy.Context) error {
switch m.Output {
case "stdout":
m.w = os.Stdout
case "stderr":
m.w = os.Stderr
default:
return fmt.Errorf("an output stream is required")
}
return nil
}
// Validate implements caddy.Validator.
func (m *Middleware) Validate() error {
if m.w == nil {
return fmt.Errorf("no writer")
}
return nil
}
// ServeHTTP implements caddyhttp.MiddlewareHandler.
func (m Middleware) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
m.w.Write([]byte(r.RemoteAddr))
return next.ServeHTTP(w, r)
}
// UnmarshalCaddyfile implements caddyfile.Unmarshaler.
func (m *Middleware) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
for d.Next() {
if !d.Args(&m.Output) {
return d.ArgErr()
}
}
return nil
}
// parseCaddyfile unmarshals tokens from h into a new Middleware.
func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) {
var m Middleware
err := m.UnmarshalCaddyfile(h.Dispenser)
return m, err
}
// Interface guards
var (
_ caddy.Provisioner = (*Middleware)(nil)
_ caddy.Validator = (*Middleware)(nil)
_ caddyhttp.MiddlewareHandler = (*Middleware)(nil)
_ caddyfile.Unmarshaler = (*Middleware)(nil)
)