Matcher #
By default, every directive you write inside a Caddyfile site block applies to all HTTP requests arriving at that site. For example, if you write reverse_proxy localhost:3000, then every request — whether to the homepage /, an image asset /logo.png, or the API endpoint /api/v1/users — gets forwarded to that backend server.
However, in real-world scenarios, you rarely want such uniform behavior. You often need conditional logic like: “enable compression for all files except images”, “require password authentication only for the /admin/* routes”, or “forward requests to the backend server only if they go to the /api/ path”. This is where the Request Matcher plays a vital role. Matchers are Caddy’s built-in mechanism for filtering and routing traffic granularly based on various request properties.
Request Matcher Evaluation Logic #
Caddy evaluates incoming requests through a series of matcher filters before applying the relevant directive.
To visualize this request matching flow, look at the evaluation flowchart below:
flowchart TD
Request["Incoming Request (GET /api/users)"] --> EvalHost{"1. Evaluate Host Matcher?"}
EvalHost -- "No Match" --> Ignore["Ignore Block / Skip"]
EvalHost -- "Match" --> EvalPath{"2. Evaluate Path Matcher?"}
EvalPath -- "No Match" --> Ignore
EvalPath -- "Match" --> EvalMethod{"3. Evaluate Method Matcher?"}
EvalMethod -- "No Match" --> Ignore
EvalMethod -- "Match" --> CheckCEL{"4. Evaluate CEL Expression (If any)?"}
CheckCEL -- "Evaluates False" --> Ignore
CheckCEL -- "Evaluates True" --> ApplyDirective["Apply Directive to Request"]Two Ways to Write Matchers #
The Caddyfile supports two matcher writing styles depending on the complexity of the condition you need:
1. Inline Matcher (Directly on the Directive) #
An inline matcher is written directly on the same line as the directive. This is the fastest and cleanest way for simple matching conditions that only involve URL paths.
example.com {
# The inline '/api/*' matcher limits reverse_proxy to that path only
reverse_proxy /api/* localhost:8080
# Serve static files for all remaining requests
file_server
}
[!NOTE] By default, every inline matcher in Caddy is treated as a Path Matcher if it starts with a slash (
/) or asterisk (*) character.
2. Named Matcher #
A named matcher is defined separately using the @ character followed by a custom name of your choosing. Named matchers are very useful for defining complex conditions involving multiple parameters (like combined path, method, IP address, and header), or when the condition needs to be reused across several different directives.
example.com {
# Named matcher definition '@adminAccess'
@adminAccess {
path /admin/*
method GET POST
not remote_ip 192.168.1.0/24
}
# Use that matcher in a basicauth directive
basicauth @adminAccess {
admin $2a$14$hashexample...
}
}
Named Matcher vs Inline Matcher: When to Choose Which? #
Choosing the right matcher writing style greatly determines the maintainability of your Caddyfile as domains and application logic grow.
When to Use Inline Matchers? #
- Simple Single Routes: When you only need matching based on URL path (e.g.,
/images/*or/health). - Concise Syntax: Avoid creating new blocks for very simple routes, keeping the Caddyfile short.
When to Use Named Matchers? #
- Multi-Criteria Combinations: When you need to match requests by IP address and HTTP method simultaneously (e.g., “Only accept POST methods from the Office IP”).
- DRY Principle (Don’t Repeat Yourself): When the same condition must be used by several different directives at once. For example, you want to detect API requests to enable compression, add CORS headers, and route the proxy:
example.com {
# Define the matcher once
@apiRequest {
path /api/*
header Accept application/json
}
# Reuse the '@apiRequest' matcher across 3 different directives
header @apiRequest Access-Control-Allow-Origin "https://app.example.com"
encode @apiRequest gzip
reverse_proxy @apiRequest localhost:8080
}
List of Available Standard Matchers #
Caddy provides many built-in matcher types ready to filter requests based on various aspects of the HTTP protocol:
1. path & path_regexp (Matching URL Paths)
#
Matches requests based on the URL path requested by the client browser.
# Match the exact path '/login' only
@exact path /login
# Match the '/static/' prefix (using the '*' wildcard)
@static path /static/*
# Match several paths at once
@assets path /css/* /js/* /images/*
# Match specific file extensions
@phpFiles path *.php
If standard wildcard matching isn’t flexible enough, you can use path_regexp with Regular Expressions:
# Match the /users/[number]/profile route
@userProfile path_regexp user ^/users/([0-9]+)/profile$
# You can use the capture group variable in other directives
rewrite @userProfile /profile-handler?user_id={re.user.1}
2. host (Matching Hostname/Domain)
#
Very useful inside site blocks serving multiple domains at once (multi-domain site block).
example.com, api.example.com, admin.example.com {
@isApi host api.example.com
@isAdmin host admin.example.com
# Route traffic based on the host matcher
reverse_proxy @isApi localhost:8080
reverse_proxy @isAdmin localhost:9000
# Default for example.com
file_server
}
3. method (Matching HTTP Methods)
#
Filters traffic based on the HTTP action sent by the browser (like GET, POST, PUT, DELETE).
# Match only if the method is POST
@postRequest method POST
# Match read-only data operations
@readOps method GET HEAD
4. header (Matching HTTP Headers & Cookies)
#
Filters requests based on the presence or value of specific HTTP headers, including Session and Cookie.
# Match if the Authorization header contains any value (wildcard '*')
@hasToken header Authorization *
# Match if the request asks for JSON format
@expectsJSON header Accept application/json
# Match if the browser used is Google Chrome
@isChrome header User-Agent *Chrome*
# Match a specific cookie (Caddy reads Cookie as an HTTP header)
@activeSession header Cookie *session_id=*
5. remote_ip (Matching Client IP Addresses)
#
Restricts access based on the requesting client’s IP address. Supports single IPs and CIDR notation.
# Match if the IP comes from the local network (LAN)
@internalNetwork remote_ip 10.0.0.0/8 192.168.1.0/24
# If Caddy is behind Cloudflare/Load Balancer, use the 'forwarded' option
# so Caddy reads the client's real IP from the X-Forwarded-For header
@trustedClient remote_ip forwarded 203.0.113.50
6. query (Matching Query Parameters)
#
Matches requests based on the query string at the end of the URL.
# Match if the URL contains '?debug=true'
@debugMode query debug=true
# Match if the 'page' parameter exists with any value
@paginated query page=*
7. file (Matching Physical File Existence)
#
Checks whether the file requested by the client actually exists on the server disk before deciding the next action.
example.com {
root * /var/www/html
# Match if the requested file does NOT exist on disk
@fileNotFound {
not file {path}
}
# Rewrite the request to index.html if the file isn't found (SPA Routing)
rewrite @fileNotFound /index.html
file_server
}
8. protocol (Matching HTTP Protocol)
#
Filters requests based on the protocol version used by the client.
# Identify WebSocket requests
@isWebSocket {
protocol http
header Upgrade websocket
}
# Identify HTTP/3 (QUIC) requests
@isHTTP3 {
protocol h3
}
Standard Matcher Capability Summary Matrix #
Here’s a comparison summary table of the capabilities of each core request matcher in Caddy:
| Matcher Name | Main Evaluation | Example Syntax | Main Production Use |
|---|---|---|---|
path | URL Path String | path /api/* | High-level URL routing. |
path_regexp | URL Path Regex | path_regexp ^/user/\d+$ | Extracting ID parameters from URLs. |
host | HTTP Host Domain | host api.domain.com | Multi-domain routing in one block. |
method | HTTP Method | method GET POST | Restricting application read/write routes. |
header | HTTP Header Key/Value | header Cookie *session* | Security header and session cookie checks. |
remote_ip | IP Address & CIDR | remote_ip 10.0.0.0/8 | IP whitelisting and intranet restrictions. |
query | URL Query Parameters | query debug=true | Troubleshooting parameter detection. |
file | Disk Filesystem | file {path} | Router fallback for Single Page Apps (SPA). |
protocol | HTTP Version / TLS | protocol https | Special handling for WebSocket / HTTP3 traffic. |
Logical Operators: NOT, AND, OR #
By default, if you write several conditions inside one named matcher block, Caddy evaluates them with AND logic (all conditions must be met for the matcher to produce true).
However, you can build more complex logic using the following operators:
1. The not Operator (Negation/Inverse)
#
Flips the evaluation result of the condition inside it.
# Match if the path does NOT start with /public/
@notPublic {
not path /public/*
}
# Match if the sender IP is NOT from the LAN
@outsideOffice {
not remote_ip 10.0.0.0/8
}
2. OR Logic (Any Condition Met) #
To test OR conditions, you can define several separate named matchers and point them at the same directive, or use the Expression Matcher based on the CEL language.
# Option A: Using several separate named matchers
@adminRoute path /admin/*
@superRoute path /superuser/*
# Both routes are protected by the same basicauth
basicauth @adminRoute { ... }
basicauth @superRoute { ... }
CEL Expressions (Common Expression Language) #
For very complex advanced conditional logic (like string operations, nested OR logic, or dynamic runtime variable checks), Caddy provides the expression matcher using the CEL (Common Expression Language) parser.
example.com {
# Complex condition: Match only if the method is POST, content type is JSON,
# and it comes from an IP outside the office network
@specialCondition {
expression {
request.method == "POST" &&
request.header["Content-Type"][0] == "application/json" &&
!req_ip("10.0.0.0/8")
}
}
reverse_proxy @specialCondition localhost:8080
}
Some CEL functions that are often useful in production:
request.uri.path.startsWith('/api/')— Checks the path prefix.request.header['User-Agent'][0].contains('bot')— Bot detection.req_ip('192.168.1.0/24')— Dynamic IP matching.
Matcher Application Patterns in Production #
Here are some matcher implementation templates that are very useful for securing and tidying your production server configuration:
1. API vs Frontend Traffic Split (Single Domain) #
Often you want to serve a static frontend and a backend API using the same domain to avoid CORS issues.
example.com {
# 1. Define Matchers for the API and Static Assets
@apiPath path /api/*
@staticAssets path /static/* /assets/*
# 2. The API is forwarded to the backend application
reverse_proxy @apiPath localhost:8000
# 3. Static assets are served from a dedicated folder
file_server @staticAssets {
root /var/www/app/static
}
# 4. All other requests are routed to the React/Vue SPA
@spaFallback {
not path /api/*
not file {path}
}
rewrite @spaFallback /index.html
file_server {
root /var/www/app/dist
}
}
2. Dynamically Blocking Malicious Bots (Scrapers/Crawlers) #
You can block access to your site for data-scraping bots that burden the server using a User-Agent header combination.
example.com {
# Expression matcher to detect malicious bot User-Agents
@evilBot {
expression {
request.header["User-Agent"][0].contains("scrapy") ||
request.header["User-Agent"][0].contains("wget") ||
request.header["User-Agent"][0].contains("curl")
}
}
# Send an instant 403 Forbidden status without processing the request to disk/backend
respond @evilBot "Access Denied" 403
reverse_proxy localhost:3000
}
3. Layered Security for an Admin Dashboard #
Restricts admin page access to only the office internal network (IP Whitelisting) while still requiring password verification.
admin.company.com {
# Matcher 1: Detect requests coming from OUTSIDE the office network
@outsideOffice {
not remote_ip 10.0.0.0/8 192.168.1.0/24
}
# Block outside access with status 403
respond @outsideOffice "Unauthorized Network Access" 403
# Matcher 2: All requests must pass basicauth
basicauth {
spv $2a$14$hashspv...
}
reverse_proxy localhost:9000
}
Debugging Request Matchers #
If your matcher configuration isn’t working as expected (for example, routes don’t match or requests are blocked when they should be allowed), you can debug using these two methods:
1. Use the caddy adapt Command
#
Adapt your Caddyfile to JSON to see how AND/OR logic and negation are turned into internal matcher objects. Check for any wrong nested structures.
caddy adapt --config Caddyfile | jq .apps.http.servers
The JSON adaptation output shows how named matchers are translated into a structured match array, making it easier to spot logical syntax errors.
2. Temporarily Add a respond Directive
#
To confirm whether your request is actually caught by a particular matcher, put a temporary respond directive under that matcher:
example.com {
@testMatcher {
path /test-path/*
method POST
}
# Return test text instantly for verification
respond @testMatcher "Matcher Condition Met!" 200
reverse_proxy localhost:3000
}
If you access /test-path/ with POST and receive that text, your matcher is working correctly. After testing, you can remove the respond line.
Summary #
- Request Matchers are used to apply directives conditionally only to specific request criteria.
- Inline Matchers (like
/api/*) are perfect for simple path route filtering on the same line as the directive.- Named Matchers start with the
@symbol (e.g.,@access) to build complex, structured filter combinations.- Conditions inside one named matcher block are evaluated with AND logic by default.
- Use the
notoperator to invert a matching condition (like blocking IPs or excluding files).- The
expressionmatcher based on the CEL language provides full programming flexibility for advanced routing logic in Caddy.