Caddyfile Structure #
For many server administrators and developers, configuring traditional web servers like Apache HTTPD or Nginx often feels exhausting. Long configuration files, rigid syntax, an abundance of boilerplate code (like manual SSL/TLS configuration), and the risk of a tiny mistake bringing down the entire server are classic recurring problems.
Caddy offers a radical solution with the Caddyfile. The Caddyfile was designed from the start to be a human-centric configuration language. Its main goal is simple yet ambitious: to let you write a complete, secure, high-performance web server configuration with automatic HTTPS in just a few lines of code that anyone can understand — even someone seeing it for the first time. However, behind that simplicity, the Caddyfile has structured rules and a unique parser behavior you need to understand so you can build complex configurations reliably without unexpected errors.
Why Is the Caddyfile So Concise? #
Before we dissect the syntax rules, we need to understand the philosophy behind the Caddyfile’s conciseness. With traditional web servers, you must explicitly define every operational step: the ports to listen on, the SSL certificate locations, the allowed encryption ciphers, and the HTTP-to-HTTPS redirect.
Caddy takes a different approach by applying the principle of sensible defaults. Caddy assumes that if you put a public domain in the Caddyfile, you surely want:
- The server to run on the standard HTTPS port (
443) and HTTP port (80). - TLS certificates obtained and renewed automatically from Let’s Encrypt or ZeroSSL.
- All insecure HTTP connections redirected automatically to HTTPS.
- Modern protocols like HTTP/2 and HTTP/3 enabled by default.
Because Caddy automates those repetitive things in the background, your Caddyfile stays free of hundreds of lines of boilerplate and focuses only on your server’s business logic, like static file directories or backend reverse proxy addresses.
Basic Caddyfile Elements #
The Caddyfile structure is built from three main elements: the Global Options Block, Site Blocks, and Directives.
To visualize how these elements interact, look at the compilation and processing flow diagram below:
flowchart TD
CF["Caddyfile (Human-Friendly Format)"] --> Parser["Caddy Parser (Tokenization & Syntax)"]
Parser --> Validation{"Valid?"}
Validation -- "No (Syntax Error)" --> Error["Show Error & Stop Process"]
Validation -- "Yes" --> Adapt["Adaptation Process (caddy adapt)"]
Adapt --> JSON["JSON Config (Native Engine Format)"]
JSON --> Engine["Caddy Main Engine"]
Engine --> AdminAPI["Admin API (localhost:2019)"]
Engine -. "Automatic TLS Management" .-> ACME["Certificate Authority (Let's Encrypt / ZeroSSL)"]Structurally, the physical layout of those elements inside the Caddyfile looks like this:
flowchart TD
subgraph Caddyfile["Caddyfile"]
direction TB
Global["Global Options Block (Optional)"]
subgraph Sites["Site Blocks"]
direction TB
Site1["Site Block 1 (example.com)"]
Site2["Site Block 2 (api.example.com)"]
end
end
style Caddyfile stroke:#333,stroke-width:2px
style Global stroke:#0288d1,stroke-width:1px
style Sites stroke:#757575,stroke-width:1px,stroke-dasharray:5,5Here’s a Caddyfile code illustration based on the structure above:
# Global Options Block (Optional)
{
email [email protected]
}
# Site Block 1
example.com {
directive1 arg1 arg2
directive2 {
subdirective key value
}
}
# Site Block 2
api.example.com {
directive3
}
1. Global Options Block #
This block is written at the very top of the Caddyfile and wrapped in curly braces { } without any site address before it. The block is optional but very important for controlling the behavior of the entire Caddy server instance, such as the email address for automatic SSL registration, global log detail level (debug mode), default ports, and trusted proxy settings.
2. Site Block #
A site block defines how Caddy should handle requests for a specific address. It starts with one or more site addresses, followed by an opening curly brace {, a list of directives, and ends with a closing curly brace }.
If your Caddyfile only contains configuration for a single site, the opening and closing curly braces of the site block can be omitted. Caddy treats the entire file contents as one implicit site block. However, if you have more than one site block, the wrapping braces for each site block must be written explicitly.
3. Directives #
Directives are functional block instructions you place inside a site block. Each directive tells Caddy to perform a specific action on incoming traffic. Examples include root (setting the file root folder), file_server (enabling the static file service), and reverse_proxy (forwarding requests to a backend application server).
Directives can have optional arguments written on the same line, or their own configuration blocks (subdirectives) wrapped in curly braces for further customization.
Tokens and How Caddy Reads the Caddyfile #
Behind the scenes, the Caddy parser reads the Caddyfile by splitting it into a series of tokens. This tokenization is very similar to how an OS shell (like bash or zsh) reads terminal commands. The parser uses whitespace and newlines as separators between tokens.
Quoting Rules #
Because the parser splits text by whitespace, any argument that naturally contains spaces must be wrapped in double quotes ("..."). If you don’t wrap such a value in double quotes, the parser reads it as several separate arguments, which can cause a configuration read failure (syntax error).
For example, note the difference between these two writings:
# ANTI-PATTERN: Writing a value with spaces without quote wrapping
# Caddy reads this as the 'root' directive with arguments '*', '/var/www/my', and 'awesome', then 'website' triggers an error
root * /var/www/my awesome website
# CORRECT: Wrap values containing spaces in double quotes
root * "/var/www/my awesome website"
Quotes can also be used to include special characters or blank lines inside certain arguments. If an argument value itself contains double quotes, you can escape them with a backslash character (\").
Newline Semantics #
In programming languages like JavaScript or C++, semicolons (;) end statements while newlines are generally ignored by the parser. The Caddyfile applies the opposite rule. Newlines carry very important semantic meaning: a newline marks the end of one directive.
You must not write more than one directive on the same line. Consider the following example:
# ANTI-PATTERN: Writing multiple directives on one line to save space
# Caddy treats 'file_server' as an argument of 'root' and triggers a startup failure
root * /var/www/html file_server
# CORRECT: Separate each directive with a newline
root * /var/www/html
file_server
If you have a very long argument and want to split it across several lines for readability, you can use the backslash character (\) at the end of the line as a line continuation marker. However, in real-world scenarios, this technique is rarely needed because the Caddyfile structure is already concise.
Brace Rules #
The Caddyfile parser is very strict about where the opening curly brace { goes. The opening curly brace must be placed on the same line as the site address or directive name. Placing the opening brace on a new line triggers a syntax parsing failure.
Why? Because if the parser encounters a site name or directive and then detects a newline at its end, it considers the instruction complete. When the parser then finds the opening brace { on the next line, it gets confused and treats { as a new, invalid site address.
# ANTI-PATTERN: Placing the opening brace on a new line
# Caddy reads 'example.com' as an empty one-line site, then treats '{' as a wrong new domain name
example.com
{
reverse_proxy localhost:3000
}
# CORRECT: The opening brace must be on the same line
example.com {
reverse_proxy localhost:3000
}
Comments in the Caddyfile #
Comments are important for explaining the intent of your configuration to teammates or to your future self. The Caddyfile uses the hash tag character (#) to define comments.
Any text written after the # character until the end of the line is completely ignored by the Caddy parser. Note that the Caddyfile does not support multiline comments (like /* ... */ in programming languages). If you want to write a long multi-line comment, you must start each line with the # character individually.
# ==========================================
# PROXY CONFIGURATION FOR THE APPLICATION SERVER
# ==========================================
# This snippet ensures all websocket connections
# are forwarded with adequate timeouts.
example.com {
# Inline comments inside site blocks are also allowed
root * /var/www/html
reverse_proxy localhost:8080 # Proxy to the Go/Node.js backend
}
Managing Multiple Sites and Addresses #
One of Caddy’s main strengths is its ability to manage dozens to hundreds of independent websites in just one Caddyfile. Each site can have completely different behavior, domains, and ports.
One Block for Multiple Domains #
Often, you want several domains to have identical configuration. For example, you might want the main domain example.com and the www.example.com subdomain served from the same static file directory. The Caddyfile lets you assign multiple site addresses to one site block using a comma (,) separator or newlines.
# Option 1: Separating domain addresses with commas on one line
example.com, www.example.com, blog.example.com {
root * /var/www/main
file_server
}
# Option 2: Writing each domain on a new line (Highly recommended for readability)
example.com
www.example.com
blog.example.com {
root * /var/www/main
file_server
}
Both methods above produce exactly the same internal configuration inside the Caddy engine. However, option 2 is preferred because it makes reading and tracking changes (git diff) easier when domains are added or removed later.
Directive Execution Order #
This is the concept that most often confuses new Caddy users: By default, the order directives are written in the Caddyfile does not determine the order the server handles requests.
With web servers like Nginx, the order of configuration lines often heavily determines how requests are processed. If you wrongly place a rewrite line below a reverse proxy, that rewrite may never execute. Caddy solves this by defining an internal priority order for all its built-in directives.
That means even if you write file_server on the first line and root on the last line of your site block, Caddy intelligently executes root first to establish the working directory, then executes file_server to serve files from that directory.
Here’s Caddy’s internal handler execution priority table, ordered from earliest to latest execution:
| Order | Directive Name | Brief Description |
|---|---|---|
| 1 | tracing | Enables distributed request tracing. |
| 2 | map | Dynamically maps a value from one variable to another. |
| 3 | root | Sets the root directory path for static file lookup. |
| 4 | vars | Declares custom internal variables for the current request. |
| 5 | rewrite | Rewrites the request URI (path/query) internally without an external redirect. |
| 6 | uri | Manipulates the request URI (like stripping a prefix or adding query params). |
| 7 | try_files | Looks for a file match on disk, then redirects if none exists. |
| 8 | basicauth | Applies HTTP Basic authentication with username and password hash. |
| 9 | forward_auth | Delegates authentication decisions to an external service (OAuth, Authelia). |
| 10 | request_header | Modifies the HTTP headers sent from the client to the server. |
| 11 | rate_limit | Limits the request count from a given IP within a time interval (if using a plugin). |
| 12 | encode | Compresses content (such as using Gzip or Zstandard). |
| 13 | push | Enables HTTP/2 Server Push to speed up asset delivery. |
| 14 | header | Modifies the HTTP headers sent in responses to the client. |
| 15 | copy_response_headers | Copies response headers from a previous handler. |
| 16 | respond | Sends a static response (text/HTML/JSON) directly to the client without a disk file. |
| 17 | metrics | Provides an internal metrics endpoint (like Prometheus metrics). |
| 18 | reverse_proxy | Forwards requests to upstream backend application servers. |
| 19 | file_server | Serves static files from local disk based on the request path. |
| 20 | php_fastcgi | Forwards PHP requests to the PHP-FPM service via socket/TCP. |
| 21 | templates | Evaluates dynamic template expressions in documents before sending. |
| 22 | abort | Forcefully terminates the HTTP connection with the client without a response. |
Changing the Default Order: The route Block
#
In most scenarios, the default priority order above is already optimal and safe. However, sometimes you have special needs to skip or change that order. Caddy provides a special block named route for this purpose.
Inside a route block, code writing order becomes linear again. Caddy executes the directives inside a route block exactly in the order the lines are written in the Caddyfile.
# Why do we need a route block?
# By default, Caddy executes 'basicauth' (order 8) BEFORE 'reverse_proxy' (order 18).
# But if we want to conditionally short-circuit the request path to an admin area:
example.com {
route {
# We want to validate certain parameters, then rewrite the URI
# before authentication is checked
rewrite /admin-internal/* /login-bypass
# Execute the next request
reverse_proxy localhost:8080
}
}
Besides route, there’s also the handle block, which is similar but mutually exclusive (only one matching handle block executes). The handle concept is covered in more depth in the Request Matcher article.
Environment Variables and Placeholders #
To make your Caddyfile flexible and safe across different work environments (like Development, Staging, and Production), Caddy supports dynamic variables.
1. Environment Variables #
You can inject OS environment variables directly into the Caddyfile using the {env.VARIABLE_NAME} syntax. This is a best practice for hiding sensitive information like DNS API tokens, encryption keys, or backend ports from being hardcoded in the Caddyfile.
# Read domain and backend port configuration from environment variables
{env.SITE_DOMAIN} {
reverse_proxy localhost:{env.BACKEND_PORT}
tls {
dns cloudflare {env.CLOUDFLARE_API_TOKEN}
}
}
Before running Caddy, just define those variables in your operating system:
export SITE_DOMAIN="myplatform.com"
export BACKEND_PORT="9000"
export CLOUDFLARE_API_TOKEN="cf_secure_token_abc123"
# Run Caddy
caddy run --config Caddyfile
2. Runtime Placeholders #
Unlike environment variables, whose values are determined when Caddy first starts, placeholders are Caddy’s internal variables whose values are evaluated at request processing time (runtime). Placeholder syntax is written with curly braces {name.placeholder}.
Some frequently used placeholders include:
{http.request.host}— The domain requested by the client.{http.request.uri}— The full URI path including query string (e.g.,/users?id=10).{http.request.orig_uri.path}— The original path before being modified by therewritedirective.{http.request.remote.ip}— The original IP address of the client sending the request.{http.request.header.User-Agent}— The browser/client information used.
Example of using placeholders for dynamic domain redirects:
www.example.com {
# Redirect all requests to the apex domain, preserving the original path the client requested
redir https://example.com{http.request.uri} permanent
}
CLI Tools for the Caddyfile #
Caddy provides several built-in command-line (CLI) tools that help you manage, format, and validate Caddyfile files.
1. Validating Syntax (caddy validate)
#
Before making configuration changes on a running production server, you should always verify that your Caddyfile syntax is correct and free of typos. The caddy validate command checks the file without running the server.
caddy validate --config /etc/caddy/Caddyfile
If the configuration is valid, Caddy shows a success message. If not, Caddy points out exactly which line has the error along with a specific reason.
2. Formatting Configuration Files (caddy fmt)
#
Consistent code style within a team is important for minimizing conflicts during code reviews. Caddy has a built-in automatic formatter that cleans up your Caddyfile according to official Caddy standards.
By default, the Caddy formatter uses the tab character for indentation (not spaces).
# Show the formatted result in the terminal without changing the original file
caddy fmt /etc/caddy/Caddyfile
# Directly update the original file with the neatly formatted result
caddy fmt --overwrite /etc/caddy/Caddyfile
3. Adapting the Caddyfile to JSON (caddy adapt)
#
The Caddy core engine doesn’t actually understand Caddyfile syntax directly. The Caddy engine communicates internally using JSON-formatted configuration structures. The Caddyfile is just a high-level abstraction layer (syntactic sugar) designed to make life easier for you as a server administrator.
You can use the caddy adapt command to see how Caddy translates your Caddyfile lines into native JSON structures. This tool is very useful for debugging tricky configuration problems.
caddy adapt --config /etc/caddy/Caddyfile --adapter caddyfile
The Relationship Between the Caddyfile and Native JSON #
Let’s run a small experiment to understand how Caddy translates Caddyfile abstractions into the JSON understood by the main engine. Suppose we have this simple Caddyfile:
example.com {
reverse_proxy localhost:3000
}
When you run the caddy adapt command, the Caddy parser produces a structured JSON output similar to this (simplified for clarity):
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443",
":80"
],
"routes": [
{
"match": [
{
"host": [
"example.com"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"handler": "reverse_proxy",
"upstreams": [
{
"dial": "localhost:3000"
}
]
}
]
}
]
}
],
"terminal": true
}
]
}
}
},
"tls": {
"automation": {
"policies": [
{
"subjects": [
"example.com"
]
}
]
}
}
}
}
From the JSON adaptation result above, we can observe several automatic processes performed by Caddy:
- Even though we only wrote the domain name
example.com, the parser automatically defines thelistenarray on port:443(HTTPS) and port:80(for automatic HTTP-to-HTTPS redirect). - The TLS automation policy (
tls.automation.policies) is configured for the subjectexample.com, triggering automatic SSL certificate acquisition. - The
reverse_proxydirective is turned into a structured handler object with a dial target oflocalhost:3000.
This proves the Caddyfile successfully saves you from writing dozens of lines of complex, typo-prone JSON configuration.
Common Anti-Patterns and Solutions #
Here’s a summary of the most frequently encountered structural syntax mistakes along with their fixes:
# ====================================================================
# ANTI-PATTERN 1: Forgetting the closing curly brace
# ====================================================================
# DON'T: Forget to close the site block
example.com {
root * /var/www/html
file_server
# ← Error! Caddy detects EOF (End Of File) without a matching closer
# CORRECT: Always make sure curly braces are properly paired
example.com {
root * /var/www/html
file_server
}
# ====================================================================
# ANTI-PATTERN 2: Opening brace on a new line
# ====================================================================
# DON'T: Write the opening brace after a newline
example.com
{
file_server
}
# CORRECT: Put it on the same line as the domain
example.com {
file_server
}
# ====================================================================
# ANTI-PATTERN 3: Stacking multiple site blocks without a clear separator
# ====================================================================
# DON'T: Write the next site address right below the previous site's
# directives without first closing the previous block's brace
example1.com {
reverse_proxy localhost:3000
example2.com {
reverse_proxy localhost:4000
}
# CORRECT: Close the first site block before declaring the second site
example1.com {
reverse_proxy localhost:3000
}
example2.com {
reverse_proxy localhost:4000
}
Summary #
- The Global Options Block is written without a site address at the very top of the Caddyfile and wrapped in curly braces
{}.- The Opening Curly Brace (
{) must be placed on the same line as the site name or opening directive; putting it on a new line triggers a syntax error.- Whitespace & Quote Semantics are very strict because the Caddy parser splits instructions by whitespace. Argument values containing spaces must be wrapped in double quotes (
"...").- Directive Writing Order in the Caddyfile does not affect Caddy’s internal execution order. Use the
routeblock if you want to execute instructions linearly in line order.- Environment Variables (
{env.VAR_NAME}) and Placeholders ({http.request.uri}) help you build dynamic, secure configurations.- Use
caddy validateto verify syntax,caddy fmtto auto-format the file, andcaddy adaptto see its native JSON translation.