Skip to content

Dwaarfile Reference

The Dwaarfile is Dwaar’s configuration format. It is designed to be readable, minimal, and production-ready with zero boilerplate.

Dwaar looks for configuration in this order:

  1. --config CLI flag: dwaar --config /path/to/Dwaarfile
  2. ./Dwaarfile in the current directory
  3. /etc/dwaar/Dwaarfile

A Dwaarfile consists of domain blocks. Each block configures one domain:

domain.com {
directive value
directive value
}

Rules:

  • One domain per block
  • Directives are one per line
  • Comments start with #
  • No semicolons, no quotes around simple values
  • Indentation is convention (2 or 4 spaces), not required

The table below lists every directive Dwaar recognises. Follow the link in the Reference column for full syntax, options, and examples.

DirectiveDescriptionReference
reverse_proxyForward requests to one or more upstream backendsReverse Proxy
file_serverServe static files from diskFile Server
php_fastcgiForward requests to a PHP-FPM socket or addressFastCGI
redirIssue an HTTP redirect to a new URLRedirects & Rewrites
rewriteRewrite the request URI internally before routingRedirects & Rewrites
uriStrip a path prefix, append a suffix, or replace path segmentsRedirects & Rewrites
handleGroup directives that apply to a specific path prefixHandle & Route
handle_pathLike handle, but strips the matched prefix from the request pathHandle & Route
routeEvaluate a group of directives in strict orderHandle & Route
respondWrite a static response body and status codeRespond & Errors
errorSynthesise an error with a given status codeRespond & Errors
abortClose the connection immediately with no responseRespond & Errors
handle_errorsDefine how Dwaar handles error responsesRespond & Errors
tlsControl TLS mode: auto, off, or manual with explicit cert pathsAutomatic HTTPS
encodeCompress responses using Brotli or GzipCompression
cacheCache upstream responses at the edgeCaching
rate_limitEnforce per-IP request rate limits; returns 429 on breachRate Limiting
ip_filterAllow or deny requests by IP address or CIDR rangeIP Filtering
basic_authProtect routes with HTTP Basic authenticationBasic Auth
forward_authDelegate authentication to an external serviceForward Auth
headerSet or delete response headersinline
request_headerSet or delete request headers before proxyinginline
logConfigure per-domain access loggingLogging
rootSet the filesystem root used by file_server and try_filesFile Server
try_filesAttempt a list of paths in order before falling throughFile Server
bindBind the server socket to a specific address or interfaceinline
request_bodySet a maximum allowed request body sizeinline
wasm_pluginLoad a WebAssembly plugin for custom request/response logicWASM Plugins
varsDefine named variables available as placeholdersPlaceholders
mapMap an input value to an output variable via a lookup tablePlaceholders
@nameDeclare a named matcher for reuse across directivesNamed Matchers
metricsExpose a Prometheus /metrics endpointPrometheus
skip_logSuppress access log entries for matched requestsLogging
importInclude a snippet, file, or glob pattern at the point of the directiveImports & Snippets

Directives marked inline are simple enough to be fully described inline in examples below rather than warranting a dedicated page.

header — Set or delete response headers:

example.com {
reverse_proxy localhost:8080
header X-Frame-Options "DENY"
header -Server # delete the Server header
}

request_header — Set or delete request headers before the request is forwarded:

example.com {
reverse_proxy localhost:8080
request_header X-Real-IP {remote_host}
request_header -Cookie # strip cookies before proxying
}

bind — Bind to a specific address instead of all interfaces:

internal.example.com {
bind 10.0.0.1
reverse_proxy localhost:9000
}

request_body — Reject requests whose body exceeds a size limit:

upload.example.com {
reverse_proxy localhost:8080
request_body 10MB
}

The following are active for every domain with no configuration required:

FeatureDefaultOverride
TLSauto (Let’s Encrypt)tls off or tls manual
HTTP → HTTPS redirectOnDisabled when tls off
CompressionOn (Brotli/Gzip)encode off
Security headersOn (HSTS, X-Content-Type-Options, etc.)Not yet configurable
HTTP/2OnNot yet configurable
Access loggingOn (JSON to stdout)log directive or skip_log
X-Request-IdOn (UUID v7)Not yet configurable
Proxy headersOn (X-Real-IP, X-Forwarded-For)Not yet configurable

The import directive runs as a text preprocessor before the parser sees the Dwaarfile. It has three forms:

import <snippet-name> # named snippet defined elsewhere in the file
import <path> # single file relative to the Dwaarfile's directory
import <glob-pattern> # every file matching a glob (`*`, `?`, `[...]`)

All three forms support positional arguments ({args[0]}, {args[1]}, {args[:]}) for parameterized reuse. Recursion is bounded at 10 levels of nesting. Circular imports are detected by canonical path and rejected at parse time.

Single-file imports are resolved relative to the directory containing the Dwaarfile:

# Dwaarfile
import common/headers.conf
example.com {
reverse_proxy localhost:8080
}
common/headers.conf
header -Server
header X-Frame-Options DENY

The parser reads common/headers.conf at the point of the directive and splices the contents into the source before tokenization. Errors in the imported file surface with the real file name in their location.

Glob imports enumerate every file matching a shell-style pattern. Matches are sorted lexicographically for deterministic load order across reloads, regardless of filesystem iteration order:

# Dwaarfile — one line does everything
import apps/*.dwaar
apps/api.dwaar
api.example.com {
reverse_proxy localhost:3001
rate_limit 200/s
}
apps/www.dwaar
www.example.com {
reverse_proxy localhost:3002
}

On startup, Dwaar expands apps/*.dwaar to apps/api.dwaar then apps/www.dwaar, then runs the parser on the combined text. Adding apps/admin.dwaar and issuing dwaar reload picks up the new site — no edit to the top-level Dwaarfile is required.

Glob patterns support the standard wildcards:

TokenMeaning
*Match any number of characters (excluding /)
?Match exactly one character
[abc]Match any character in the set

The glob form is the primary mechanism for zero-mutation deploy-agent workflows. An external agent (Permanu, a CI job, a shell script) writes one .dwaar file per application into a managed directory and then calls dwaar reload. The top-level Dwaarfile is owned by the operator and never touched by automation:

# Operator-owned top-level Dwaarfile
{
email ops@example.com
}
# Shared snippets
import common/defaults.conf
# Per-app configs dropped in by the deploy agent
import apps/*.dwaar

The agent’s responsibilities shrink to file writes in apps/ plus one HTTP call to the admin API. Rollback is rm apps/<app>.dwaar && dwaar reload. Concurrent agents can write independent files without coordination.

A glob pattern that matches zero files is not an error. The import directive expands to nothing and the rest of the Dwaarfile proceeds normally. This lets you ship a Dwaarfile with import apps/*.dwaar before any agent has written a file — the proxy starts clean and picks up apps as they land:

# Dropped into an empty tree — still loads.
import apps/*.dwaar

A tracing::debug! event is emitted when a pattern matches zero files so operators can spot typos without failing startup.

Every resolved match is canonicalized and verified to stay inside the Dwaarfile’s base directory. A symlink pointing outside the tree causes the entire import to fail with PathTraversal — no silent skip. The string .. in a pattern is rejected before any filesystem access.

ConditionResult
Pattern matches one or more files, all inside base dirFiles are imported in lexicographic order.
Pattern matches zero filesEmpty expansion. Not an error.
Pattern contains ..PathTraversal error.
A matched symlink resolves outside the base directoryPathTraversal error — the whole import aborts.
Pattern is malformed (e.g. unclosed [)InvalidGlob error with the pattern text and parser message.
A single matched file cannot be readIoError with the canonical path.
An imported file imports a file already in the current chainCircularImport error.
Recursion exceeds 10 levelsDepthLimitExceeded error.

Non-file matches (directories that happen to satisfy a broad pattern like apps/*) are skipped automatically.

Named snippets defined at the top level of a Dwaarfile can be imported the same way. Snippet definitions use the (name) { ... } form and are extracted before the parser runs:

(common) {
encode gzip
header -Server
}
example.com {
import common
reverse_proxy localhost:8080
}

Snippets accept positional arguments via {args[0]}, {args[1]}, or {args[:]} for all arguments joined by space:

(backend) {
reverse_proxy {args[0]}
header X-Backend {args[1]}
}
example.com {
import backend localhost:9000 api
}

# Production API — rate-limited, no analytics, metrics exposed
api.example.com {
reverse_proxy localhost:3000
rate_limit 200/s
encode on
metrics /internal/metrics
log {
output file /var/log/dwaar/api.log
}
skip_log /healthz
}
# Marketing site — static files with a fallback to the SPA index
www.example.com {
root /var/www/marketing
try_files {path} /index.html
file_server
encode on
}
# PHP application via FastCGI
app.example.com {
root /var/www/app/public
php_fastcgi unix//run/php/php8.3-fpm.sock
}
# Admin panel — forward auth, no public TLS
admin.internal {
bind 10.0.0.0/8
tls off
forward_auth http://authd:9001/validate {
copy_headers X-User X-Role
}
reverse_proxy localhost:5000
}
# Redirect bare domain to www
example.com {
redir https://www.example.com{uri} permanent
}

Check your Dwaarfile for errors without starting the server:

Terminal window
dwaar validate
# Config valid.
dwaar validate --config /path/to/Dwaarfile
# Config valid.

Format your Dwaarfile consistently:

Terminal window
dwaar fmt
# Formatted 3 domain blocks.
dwaar fmt --check
# Exit code 1 if unformatted (useful in CI).