Caddyfile blocks, matchers, and directives don’t just configure Caddy; they sculpt the very way requests are routed and transformed.
Let’s see Caddy in action. Imagine a simple Caddyfile:
:80 {
reverse_proxy localhost:8080
}
example.com {
root * /var/www/example.com
file_server
}
sub.example.com {
reverse_proxy localhost:9090 {
header_up Host {http.request.host}
}
}
When a request for example.com hits Caddy on port 80, it first checks if the request’s host matches example.com. If it does, Caddy then looks for directives within that block. Here, root * /var/www/example.com sets the document root for all requests within this block (the * is a matcher, meaning "match all requests"), and file_server tells Caddy to serve static files from that root.
For sub.example.com, a different block applies. The reverse_proxy directive sends the request to localhost:9090. Crucially, the header_up Host {http.request.host} line injects the original Host header from the incoming request into the request Caddy sends to the backend. This is vital for backend servers that host multiple sites on a single IP address.
The :80 block is a default. If no other host matches, Caddy will use this block. Here, it simply forwards all requests to a backend running on localhost:8080.
The fundamental building blocks of a Caddyfile are directives. Directives are commands that tell Caddy what to do (e.g., reverse_proxy, file_server, log). Most directives take arguments. For reverse_proxy, the argument is the upstream address like localhost:8080. For root, it’s the path like /var/www/example.com.
To control when a directive applies, you use matchers. Matchers are conditions that must be met for the directives within their associated block to be executed. In the example.com block, example.com itself is a host matcher. The * in root * /var/www/example.com is a path matcher. Other common matchers include method, header, query, and remote_ip. You can combine multiple matchers within a single block to create very specific routing rules.
Blocks group directives and matchers. A block starts with a matcher (or a port like :80 which implicitly binds to all hosts on that port) and is followed by curly braces {} containing directives. Caddy processes these blocks sequentially. The first block whose matchers all evaluate to true for an incoming request "wins," and its directives are executed. This is why the order of blocks can matter, though Caddy’s internal logic often handles common cases intelligently.
The real power comes from nesting. You can have directives within directives, and matchers within matchers. For example, to serve specific files only via GET requests:
example.com {
root * /var/www/example.com
handle_methods GET {
file_server
}
}
Here, handle_methods GET is a directive that also acts as a matcher for requests where the method is GET. Only then will file_server be executed.
Caddy’s directive and matcher system is designed to be extensible and powerful. You can define custom directives using plugins, and the matcher syntax allows for complex conditional logic without writing a single line of code outside the Caddyfile. This declarative approach makes configuring sophisticated web server behavior surprisingly manageable.
When you specify a path matcher like handle /api/*, Caddy doesn’t just match the literal /api/. It actually uses a glob pattern, meaning it will match /api/, /api/users, /api/users/123, and so on. This glob matching is a subtle but important aspect of how path matchers operate, allowing for flexible routing to API endpoints or directories.
The next step in mastering Caddy configuration is understanding how to combine multiple directives within a single block for complex request processing chains, often involving reverse_proxy with middleware like rewrite or header_up.