Skip to content

Pingclairfile

A Pingclairfile is Pingclair’s configuration file, written in the Caddyfile language: an optional global options block, then one block per site, each holding directives. A Caddyfile that uses only supported directives loads unchanged. This page describes the language; the directive reference describes what each directive does.

📌 This page describes v0.2.2.

Rule Detail
Comments # to the end of the line.
Quoting A value containing spaces is quoted with ". Quotes are removed before the value is parsed.
Blocks A { that opens a block ends its line. route { respond "hi" on one line is refused.
Durations Written with a unit: 30s, 5m, 1h. A bare number is refused where a duration is expected.
Sizes kb, mb, gb, and tb are powers of 1000; kib, mib, gib, and tib are powers of 1024. 10MB is 10,000,000 bytes.
Case Directive and option names are lowercase.
Placeholders {host}, {path}, {args[0]}, {block}, and the rest of the placeholder set are expanded where the directive documents them.

A site block is named by its address. The address decides which port the site listens on and whether it is served over HTTPS.

example.com { # HTTPS on 443 with a public certificate; 80 redirects
example.com:8443 { # HTTPS on 8443: a host with a port is still HTTPS
localhost:8080 { # HTTPS on 8080, from the internal authority
:8080 { # plaintext HTTP on 8080, for any host
http://example.com { # plaintext HTTP on http_port (80 by default)
http://[::1]:8080 { # plaintext HTTP for the host [::1]
*.example.com { # one label: a.example.com, not a.b.example.com
  • A host with a port and no scheme is served over HTTPS, as in Caddy. Write http:// in front of the address to use plaintext HTTP on any port.
  • An address with a scheme and no port, such as https://example.com, listens on the global http_port or https_port.
  • A bracketed IPv6 address names a site, exactly as 127.0.0.1 does. A bracket that does not hold an IPv6 address, or that is followed by anything other than :port, is refused.
  • http://0.0.0.0:8080 is a catch-all for the port, like http://:8080.
  • Host names are compared without regard to letter case and a trailing dot, so Example.com and example.com. reach the site named example.com.

Sites that share a port share one socket, and the Host header selects the site. A site with a specific address, such as http://127.0.0.1:8080, that shares its port with a site listening on every interface is therefore reachable on every interface by a client that sends its Host; give it a port of its own if it must stay on loopback. Each such fold is logged when the configuration loads.

The configuration is refused when sites on one port need different socket policy: a site restricted by bind or default_bind beside one that listens everywhere, a plaintext site beside a TLS site, or PROXY protocol on only one of them.

A matcher limits a directive to some requests. It is written inline, such as a path like /api/*, or declared once as @name and referred to by that name.

example.com {
@api path /api/*
header @api Cache-Control "no-store"
handle /assets/* {
file_server ./assets
}
}

How a path pattern matches:

  • Letter case is ignored for ASCII letters: /Admin/* answers /admin/users. Use path_regexp when case must decide.
  • Escapes are decoded once before the comparison: /secret%21 matches path /secret!. A pattern written with an escape, such as /a%20b, must be written decoded ("/a b") to match.
  • A * may appear anywhere. A leading * matches a suffix at any depth (*.php matches /x/y/index.php), a * at each end matches a substring (*/admin/*), and a * elsewhere stays inside one path segment (/a/*x matches /a/bx, not /a/b/cx). ?, […], and \ are literal characters.
  • Braces are literal. /{id} matches only that path; use path_regexp for captures.

The addresses a request comes from are matched by two different matchers:

  • client_ip matches the client after trusted_proxies is applied: the forwarded address when the connection comes from a trusted proxy.
  • remote_ip matches the connection’s own peer, regardless of forwarding headers. Behind a trusted load balancer, that is the balancer.

Either matcher also takes Caddy’s private_ranges keyword in place of a list, which expands at load to the same six ranges trusted_proxies static private_ranges uses: 192.168.0.0/16, 172.16.0.0/12, 10.0.0.0/8, 127.0.0.1/8, fd00::/8 and ::1. So @local client_ip private_ranges matches a client on a private or loopback address.

A range that does not parse, such as 10.0.0.0/33, is refused at load.

handle, handle_path, and route take at most one matcher token before their block: *, a path that starts with /, or @name. Any other token is refused.

Placeholder Value
{client_ip}, {http.request.client_ip} The client after trusted_proxies is applied.
{remote_host}, {http.request.remote.host} The connection’s peer address.
{remote_port}, {http.request.remote.port} The connection’s peer port.
{remote}, {http.request.remote} The peer as host:port.

{remote_ip} is not a placeholder and is refused at load, with a message that names both replacements. Behind trusted_proxies, write header_up X-Real-IP {client_ip} to forward the client’s address.

A site’s routes are tried as one list, ordered the way Caddy orders them, and the first route that matches answers. The order comes from the directive, not from where the line is written: redir, handle, and route rank ahead of respond, which ranks ahead of reverse_proxy, php_fastcgi, and file_server. In the site below, /assets/a.txt receives hello instead of the file:

example.com {
root * /srv
file_server /assets/*
respond "hello" 200
}

Between routes of the same directive:

  1. The one whose single path is longer once a trailing * is removed goes first: /foobar* before /foo.
  2. For paths that differ only by the trailing *, the exact path goes first: /foo before /foo*.
  3. Otherwise, file order decides. Caddy sorts two different paths of equal length alphabetically instead.
  4. A matcher with several paths, or none, goes after every single-path route.

A middleware directive written with a matcher, such as basic_auth /admin/* or header @api …, applies authentication or header changes to every route that answers the requests it matches, wherever the order puts that route.

To keep a narrower route in front, wrap the routes in handle blocks, move a directive with the global order option (order file_server first), or list them in a route block, which keeps the written order.

Snippets are reusable fragments. A snippet declared as (name) { ... } is included with import name, and can receive a block from its caller:

(proxied) {
https://{args[0]} {
encode zstd gzip
{block}
}
}
import proxied example.com {
reverse_proxy 127.0.0.1:3000
}

{args[0]} is the first argument after the snippet name, and {block} is the block the caller supplies. When the caller supplies no block, {block} expands to nothing and the snippet still compiles. Named sub-blocks are addressed as {blocks.<name>}.

Three commands help while writing a configuration:

  • pingclair validate compiles the file, reads the certificate files it names, and names the first problem.
  • pingclair adapt --pretty prints the JSON the file compiles to, after the same validation.
  • pingclair fmt formats the file with one tab per level, and exits with status 1 when the file was not already formatted.

Command line lists every subcommand and flag.

The Caddyfile language defines more directives and options than Pingclair implements. A name Pingclair recognizes but does not implement is refused when the file loads, with a message naming the missing feature, and the configuration is rejected. Project status lists those names.