Skip to content

Concepts

The Raddexfile is the heart of raddex: a plain-text configuration in a Caddyfile-style syntax, validated with raddex check and served with raddex run. This page gives you the mental model. The directive reference has the complete syntax, and Sites, ports & HTTPS covers how a request is matched to a site.

A Raddexfile is an optional global block followed by one or more site blocks:

{
acme_email ops@example.com
}
api.example.com {
reverse_proxy 127.0.0.1:8080
}
static.example.com {
root /var/www/html
file_server
}
  • The global block — a bare { ... } at the top of the file — carries site-wide settings such as your ACME email.
  • Each site block maps a host (optionally with a port) to behavior.

Once a request is matched to a site, raddex executes the site’s lines strictly in write order. Every line is one of three kinds:

  • Terminal directives decide who serves the request: reverse_proxy, file_server, redir, respond, error. The first terminal that matches ends the site — later lines never run. A terminal may carry an inline matcher; one that does not match is a no-op and execution continues.
  • Modifier directives describe how to serve, regardless of position: header_up, header_down, encode, rewrite. They apply to whichever terminal serves the block, even if written after it.
  • Guard directives decide who may be served: rate_limit, basic_auth, forward_auth. They act like modifiers — applying to whichever terminal serves the block — and reject the request (429, 401) when the guard fails.

Caddy users: Caddy reorders directives behind the scenes; raddex does not. You control the order, and the first match wins.

Matchers select which requests a directive serves

Section titled “Matchers select which requests a directive serves”

A matcher is a set of terms — path, host, method, header, query, remote_ip, protocol — all of which must match. Matchers attach to handle blocks and, inline, to terminals:

handle path /status method GET {
respond 200 ok
}

Terms are space-separated and ANDed; there are no parentheses or &&. handle /static/* is shorthand for handle path /static/*.

handle <matcher> { ... } is a scoped block: if the matcher matches, its lines run and matching stops; otherwise execution continues past it. Think of it as “for these requests, use this terminal.” handle_path is the same, except it strips the matched path prefix before the block’s terminal runs.

Put the model together on a concrete site:

api.example.com {
handle /static/* {
root /var/www/html
file_server
encode zstd gzip
}
reverse_proxy 127.0.0.1:8080
rate_limit remote_ip 100r/s
header_up X-Real-IP {remote_host}
}

Request: GET /static/app.js

  1. handle /static/* matches the path, so its lines run in order.
  2. root sets the directory; file_server serves /var/www/html/static/app.js and ends site execution.
  3. encode zstd gzip — a modifier inside the block — compresses the response when the client accepts it.
  4. The site-level modifiers (rate_limit, header_up) also apply to this terminal, because site-level modifiers apply to whichever terminal serves the block. rate_limit checks the client’s bucket; header_up targets the upstream request, so it has no effect on a local file.

Request: GET /api/users

  1. handle /static/* does not match, so execution continues.
  2. reverse_proxy has no matcher, so it always matches and serves the request — proxied to 127.0.0.1:8080, ending the site.
  3. rate_limit remote_ip 100r/s guards this terminal too (it is a site-level modifier). If the bucket is empty you get 429 Too Many Requests.
  4. header_up X-Real-IP {remote_host} adds the client’s IP to the upstream request headers.

Write order matters only between terminals. Modifiers apply wherever they are written, within their scope: site-level modifiers apply to every terminal in the site; modifiers inside a handle block apply only to that block’s terminal.