Configuration
Askr can be configured with CLI flags (see CLI) or a typed
askr.toml file. The config file is the declarative source of truth — the
thing tooling and the admin GUI edit — and is recommended for production.
askr config-check askr.toml # validate + print resolved settings
askr serve --config askr.toml # run (the file is authoritative)
When --config is given, the file provides everything; other serve flags are
ignored.
A config file is the whole configuration.
--configis not merged with the other flags — it replaces them. Askr refuses to start if you pass both, naming the flags that would have been ignored, because the alternative is a server that runs with settings you didn't choose and says nothing. Everything a flag can do has a key here.
askr.toml reference
A complete, commented example lives at
examples/askr.toml. An unknown key is ignored with a
warning that names it and the key it was probably meant to be — at startup and in
askr config-check, so run that after editing the file.
[server]
| Key | Type | Default | Meaning |
|---|---|---|---|
listen |
string | 127.0.0.1:8000 |
Address to bind. |
root |
path | public |
Document root (the app's public/). |
front |
string | index.php |
Front controller, relative to root. |
workers |
string | auto |
Number of worker processes, or auto (= CPU cores). |
max_requests |
int | 0 |
Recycle each worker after N requests (0 = never). |
max_rss |
int | 0 |
Recycle a worker gracefully once its RSS exceeds this many MB (0 = never). Leak-aware; Linux only. |
shadow_to |
string | — | Mirror sampled safe requests to this upstream URL for deploy validation. |
shadow_sample |
int | 100 |
Percent of eligible requests to mirror. |
max_body_size |
string | 16M |
Reject larger bodies with 413. K/M/G or plain bytes. |
tls_handshake_timeout |
int | 10 |
Seconds a client may take to finish the TLS handshake (slowloris guard). |
header_read_timeout |
int | 15 |
Seconds a client may take to send request headers (slowloris guard). |
https |
bool | false |
Force HTTPS in $_SERVER (e.g. behind a TLS terminator). Implied by TLS. |
force_https |
bool | false |
Redirect plain HTTP to HTTPS (308), using the connection's TLS state / https / X-Forwarded-Proto. |
http_redirect |
— | Answer plain HTTP here and 308 it to HTTPS, e.g. "0.0.0.0:80". Needs force_https. Automatic on the ACME challenge address with --acme. |
|
traffic_log |
path | Record one JSON line per PHP-served request for askr cache-report. A diagnostic — turn it on for an hour, then off. |
|
trusted_proxies |
list | [] |
Proxies whose X-Forwarded-For may be believed, as IPs or CIDRs ("10.0.0.0/8"). Sets the client identity everywhere: $_SERVER['REMOTE_ADDR'] (since 1.7.1), [[ratelimit]] buckets, and the forwarding headers PHP is shown — X-Forwarded-For collapsed to the client from a trusted peer, and every forwarding header removed from any other. See Behind a reverse proxy. |
workers_min |
int | = workers |
CoW autoscaling floor (with --cow). |
workers_max |
int | = workers |
CoW autoscaling ceiling (> min enables autoscaling). |
access_log |
path | — | JSON access log per request; - for stdout. Off if unset. |
http3 |
bool | false |
Serve HTTP/3 (QUIC) on the TLS port (requires TLS; build with --features http3). |
sandbox |
bool | false |
Linux hardening: seccomp no-exec. See Sandbox. |
sandbox_write |
path[] | [] |
Landlock: writes allowed only under these paths (enables the FS restriction). |
sandbox_required |
bool | false |
Refuse to serve if the sandbox doesn't fully apply. Needs sandbox_write. |
[worker]
Omit this whole section to run in per-request mode. Present it to enable worker mode (boot once, serve many — see Worker mode).
| Key | Type | Meaning |
|---|---|---|
script |
path | Worker script that boots the app and loops. |
app_base |
path | Application base path, exported as $ASKR_APP_BASE for the worker script (inherited across fork). |
ini |
string | Extra php.ini lines (newline-separated), e.g. to load opcache. |
paranoid |
bool | Dev only: detect state bleed between requests (expensive). See Worker mode. |
[tls]
| Key | Type | Meaning |
|---|---|---|
cert |
path | TLS certificate chain (PEM). Use with key. |
key |
path | TLS private key (PEM). |
self_signed |
bool | Generate a v3 self-signed cert on startup (dev). Mutually exclusive with cert/key. |
Enabling TLS negotiates HTTP/2 or HTTP/1.1 via ALPN and sets HTTPS=on in
$_SERVER (so Laravel emits secure cookies). Certs must be X.509 v3.
Auto-TLS (ACME / Let's Encrypt) has its own section — see [acme]. Do not
set both: [tls] is a certificate you supply, [acme] is one Askr fetches.
[acme]
| Key | Type | Default | Meaning |
|---|---|---|---|
enabled |
bool | false |
Obtain and renew a certificate over HTTP-01. |
domains |
list | [] |
Hostnames to certify. At least one required. Bare hostnames — no scheme, port or wildcard. |
email |
string | admin@<first domain> |
Contact address for the ACME account. |
dir |
path | /var/lib/askr/acme |
Where the account key and certificate are cached. Must survive restarts, or you will hit Let's Encrypt's rate limits. |
staging |
bool | false |
Use Let's Encrypt staging: untrusted certs, far higher limits. Do this first. |
directory_url |
string | Let's Encrypt | Custom ACME directory (a Pebble test server). Distinct from dir. |
http |
address | 0.0.0.0:80 |
Where HTTP-01 challenges are answered — and, with force_https, where plain HTTP is redirected from. |
ca_root |
path | – | Extra CA root to trust for the directory. Testing only. |
[server]
listen = "0.0.0.0:443"
root = "/var/www/example.com/public"
force_https = true
trusted_proxies = ["172.17.0.1"] # file-only, which is why [acme] had to exist
[acme]
enabled = true
domains = ["example.com", "www.example.com"]
email = "admin@example.com"
dir = "/var/lib/askr/acme"
One long-lived listener on http answers challenges and 308s everything else, so a
certificate can be issued and HTTP redirected without the two fighting over port 80.
Askr refuses to start rather than let a mistake here become a site quietly serving plain
HTTP: domains without enabled, enabled without domains, [acme] alongside
[tls], and a wildcard domain (HTTP-01 cannot validate one) are all errors.
Until 1.4.10 ACME was flags-only. Since --config is the whole configuration rather than a
set of defaults, that made auto-TLS and a config file mutually exclusive — and combinations
like "auto-TLS behind a proxy" unreachable, because trusted_proxies has never had a flag.
[admin]
| Key | Type | Meaning |
|---|---|---|
listen |
string | Admin dashboard/API address (e.g. 127.0.0.1:9000). Omit to disable. See Admin. |
[queue]
Run queue workers in the same binary, supervised alongside the web workers.
| Key | Type | Meaning |
|---|---|---|
workers |
int | Number of queue-worker processes (0 = off; floor when autoscaling). |
workers_max |
int | Autoscaling ceiling. When > workers, the pool scales on backlog (Horizon balance=auto, no extra daemon). Defaults to workers. |
script |
path | Queue runner script (e.g. examples/askr-queue.php). |
slots |
int | Shared-memory job queue slots (0 = off; 32 KB each) — askr_queue_* + the AskrQueue driver. See Cache. |
persist |
string | Name a shared-memory object for the ring so pending jobs survive a restart (1.5.1). Off by default: the object lives in /dev/shm, which containers cap at 64 MiB — see Docker. A ring whose layout no longer matches the binary is recreated, empty, with a log line saying why. |
stall_secs |
int | How long a job may sit ready and unclaimed before Askr calls the lane stalled — in the log, in /api/status's warnings, and in askr_queue_unattended. Default 30; 0 keeps the default. |
root |
path | Docroot of the application whose jobs these workers consume. Defaults to [server] root. Only needed with [[site]] — see below. |
slots is required when workers is set, and Askr refuses to start without it. The
ring is only mapped when slots are configured; without it every push returns 0, Laravel does
not check that, and queued jobs are discarded silently — which is how a live site stopped
sending mail with nothing in any log.
Ten seconds of queue latency is unremarkable; thirty means nothing is listening, which is
why stall_secs defaults to 30. Raise it for an app whose queues are deliberately
batchy, lower it to be told sooner. One threshold drives the watchdog log line, the
warnings array in GET /api/status and the askr_queue_unattended metric together, so a
dashboard cannot disagree with the log about whether a lane is stalled — see
Admin.
root is the application these workers belong to, and with [[site]] you have to say
it. Shared memory is namespaced per application, the namespace is derived from the
docroot, and askr_queue_pop matches the namespaced key. A queue sidecar is one process
with one namespace for its whole life, so it consumes exactly one application's jobs — set
root to the same path as the [[site]] root of the application that dispatches them. If
that is the top-level application, set it equal to [server] root: that is a valid and
expected answer, it just has to be an answer. Without [[site]] there is only one
application and the default is right.
Since 1.7.0 Askr refuses to start when [[site]] is configured together with a queue or
scheduler sidecar and neither [queue] root nor [scheduler] root is set — it cannot tell
which application the sidecar serves, and the error names the key to set. Guessing is what
the release fixes: an instance whose jobs were dispatched by a [[site]] application
accepted every job for six days and read none of them, with nothing in any log from the
application's side. See
Hosting.
[scheduler]
Run the scheduler (built-in cron) in the same binary.
| Key | Type | Meaning |
|---|---|---|
script |
path | Scheduler runner script (e.g. examples/askr-scheduler.php). Omit to disable. |
root |
path | Docroot of the application the scheduler runs for. Defaults to [queue] root, then [server] root. |
The scheduler sidecar is namespaced the same way as the queue workers and for the same
reason. When both run for the same application — the usual case — [queue] root already
covers it and there is no second line to write. Set [scheduler] root when the scheduler
belongs to a different application than the queue workers, or when an instance runs the
scheduler without queue workers at all; it is honoured independently of [queue] root,
not merely as a fallback.
[[sidecar]]
Supervise arbitrary external commands (array of tables; respawned if they die).
Run via sh -c in $ASKR_APP_BASE. Used for e.g. Inertia SSR — see Docker.
[[sidecar]]
command = "node bootstrap/ssr/ssr.mjs"
[[redirect]]
Declarative host redirects (array of tables), evaluated before any dispatch. from
matches the Host header exactly or as a *.suffix glob; the request path + query
are preserved; status defaults to 308 (permanent, method-preserving).
[[redirect]]
from = "www.domene.no"
to = "https://domene.no" # → https://domene.no/<path>?<query>
[[redirect]]
from = "*.old.no"
to = "https://ny.no"
status = 301
For plain-HTTP→HTTPS across all hosts, use [server] force_https = true instead.
[[site]]
Virtual hosts — serve several domains/apps from one Askr instance, routed by the
Host header. Each site has its own document root + front controller; hosts match
exactly or as a *.suffix glob. A request whose Host matches no site falls back to
[server] root.
[server]
listen = "0.0.0.0:443"
root = "/var/www/default/public" # fallback
[[site]]
hosts = ["domene.no", "*.domene.no"]
root = "/var/www/domene/public"
[[site]]
hosts = ["annet.no"]
root = "/var/www/annet/public"
front = "index.php"
Static files are served from the matching site's root in any mode. Full dynamic
dispatch (a different app per host) works in per-request mode — each request runs
that site's front controller fresh. In worker mode the single booted app is
fixed, so give each app its own instance (or route by host inside the worker script)
until per-site worker pools land. Combine with [[redirect]] for per-host www→apex.
[cache]
Enable the shared-memory cache (askr_cache_*, and the Laravel driver). See
Cache.
| Key | Type | Meaning |
|---|---|---|
slots |
int | Small kv cache slots (0 = disabled). ~4.3 KB each — counters, locks, small values. |
large_slots |
int | Large-value region slots (0 = off). 64 KB each — Laravel sessions, cached fragments/collections. |
response_slots |
int | Response cache slots (0 = off). ~140 KB each — full-response edge cache with tag invalidation. |
strip_query_params |
list | Query parameters ignored when building the response-cache key. Trailing * globs ("utm_*"). PHP still receives the full query. |
ignore_cookies |
list | Cookies that don't make a request non-cacheable (analytics: "_ga", "_gid", "_fbp"). Trailing * globs. Default: any cookie defeats caching. |
vary_user_agent |
bool | Split the response-cache key on mobile vs desktop User-Agent (also sets Vary: User-Agent). Default false. |
persist |
path | Save the response cache here on graceful shutdown and load it at boot, so a restart doesn't start cold. Unset = off. |
persist_key |
string | Release identifier. When set, a saved cache only loads if it matches — set it to your release SHA so a deploy can't resurrect pre-deploy HTML. |
saint_seconds |
int | Saint mode: seconds to treat PHP as unhealthy after a 5xx, during which requests holding a stale-if-error entry skip PHP entirely. 0 = off (default). |
[[ratelimit]]
Rate limits enforced in the Rust layer before PHP is woken, with token buckets in shared memory (so a limit spans the whole worker fleet). First match wins. See Features.
| Key | Type | Meaning |
|---|---|---|
path |
string | Path glob (*, ?), must start with /. Globs, not regexes. |
limit |
int | Requests allowed per window. Must be > 0. |
window |
int | Window length in seconds. Default 60. |
by |
string | Identity counted: ip (default), header:<Name>, cookie:<name>. |
burst |
int | Extra tokens a bursty client may accumulate on top of limit. |
[[ratelimit]]
path = "/login"
limit = 5
window = 300
Refused requests get 429 with Retry-After. Reserved /askr/* endpoints are exempt.
Set [server] trusted_proxies when running behind a load balancer, or X-Forwarded-For
is ignored and every client shares one bucket. The same setting is what gives PHP the
client in REMOTE_ADDR — see Behind a reverse proxy.
[[cache.rule]]
Per-path cache policy, applied without touching the app. First match wins — put specific rules above the catch-all. See Features.
| Key | Type | Meaning |
|---|---|---|
path |
string | Path glob (*, ?), must start with /. Globs, not regexes — a regex-shaped pattern is rejected at load. |
action |
string | "pass" = never cache this path, even if the app sent Askr-Cache. Responses carry X-Askr-Cache: PASS. |
ttl |
int | Fresh seconds. Caches a path the app never opted in to; overrides the app's TTL (the app's tags are kept). |
swr |
int | Stale-while-revalidate window, seconds past ttl. |
stale_if_error |
int | stale-if-error window, seconds past ttl. |
force |
bool | Cache even when the request carries cookies. Dangerous on anything user-specific — one visitor's page is then served to everyone. |
[[cache.rule]]
path = "/admin/*"
action = "pass"
[[cache.rule]]
path = "/static/*"
ttl = 86400
force = true
Cache-key normalisation example — tracking parameters and analytics cookies stop fragmenting the cache:
[cache]
response_slots = 512
strip_query_params = ["utm_*", "gclid", "fbclid", "_ga"]
ignore_cookies = ["_ga", "_gid", "_fbp"]
vary_user_agent = false
[broadcast]
Enable askr_broadcast() and the SSE endpoint. See Broadcasting.
| Key | Type | Meaning |
|---|---|---|
enabled |
bool | Turn on the broadcast ring + GET /askr/events. |
[pusher]
Pusher-compatible WebSocket + HTTP trigger (drop-in Reverb). Auto-enables the broadcast ring.
| Key | Type | Meaning |
|---|---|---|
enabled |
bool | Turn on the WS endpoint /app/{key} + trigger /apps/{id}/events. |
secret |
string | App secret to verify private/presence subscription auth (omit = accept, dev). |
[record]
| Key | Type | Meaning |
|---|---|---|
dir |
path | Record failing (5xx) requests here for askr replay. Captures bodies — sensitive; created 0700, files 0600, credentials redacted. |
[reload]
| Key | Type | Meaning |
|---|---|---|
canary |
bool | Canary reload: roll one worker and health-check it before rolling the rest. |
canary_window |
int | Seconds to watch the canary. Default 5. |
canary_min_requests |
int | Requests the canary must serve for a verdict; below this the rollout is inconclusive and continues with a warning. Default 20. |
canary_max_error_rate |
float | Percentage points of error rate the canary may exceed the fleet by. Default 2.0. |
canary_max_latency_factor |
float | Mean-latency factor vs the fleet. Default 3.0. |
Example
[server]
listen = "0.0.0.0:8000"
root = "/var/www/app/public"
workers = "auto"
max_requests = 1000
max_body_size = "16M"
[worker]
script = "/opt/askr/examples/laravel-worker.php"
app_base = "/var/www/app"
ini = "zend_extension=/opt/askr/vendor/php-build/install/lib/php/extensions/no-debug-non-zts-20240924/opcache.so\nopcache.enable=1\nopcache.validate_timestamps=0"
[tls]
cert = "/etc/askr/cert.pem"
key = "/etc/askr/key.pem"
[admin]
listen = "127.0.0.1:9000"
Environment variables
| Variable | Meaning |
|---|---|
ASKR_PHP_INI |
Extra php.ini lines, appended to the engine defaults. Overridden by --ini / [worker] ini. Commonly used to load opcache. |
ASKR_APP_BASE |
Application base path for the worker script (set automatically from [worker] app_base, or export it yourself in flag mode). |
ASKR_PHP_CONFIG |
Path to a php-config for a specific embed-enabled, non-ZTS PHP install (used at build time). |
RUST_LOG |
Log filter, e.g. askr=debug. Default askr=info. |
ASKR_CACHE_DB / ASKR_QUEUE_DB / ASKR_BROADCAST_DB |
Durable L2 backend paths (--features sql-backend; unset = L1 shared memory). See Storage backends. |
ASKR_OBSERV_DSN (+ ASKR_OBSERV_SERVICE/HOST/BATCH/FLUSH_MS/QUEUE) |
Ship per-request logs to a MySQL-wire database (--features observ). See Observability. |
ASKR_OTEL_ENDPOINT (+ ASKR_OTEL_SERVICE) |
Export OpenTelemetry traces (root http.request + child php.execute) over OTLP/gRPC (--features otel). See Observability. |
opcache
PHP 8.5 compiles OPcache into libphp and auto-registers it, so there is no
zend_extension line — just enable it (and JIT) in the INI:
[worker]
ini = "opcache.enable=1\nopcache.enable_cli=1\nopcache.validate_timestamps=0\nopcache.jit=tracing\nopcache.jit_buffer_size=128M"
validate_timestamps=0 maximises throughput (no stat() per file); pair it with
a SIGHUP reload on deploy so fresh workers recompile the new code. opcache.jit
enables the JIT (on by default in this build). askr-run.sh sets sensible
defaults automatically.