CLI reference
askr <command> [options]
Commands: serve, test, replay,
doctor, tune, cache-report, config-check,
upgrade. Run askr <command> --help for the built-in help.
Global: -V/--version, -h/--help. Logging verbosity is RUST_LOG (e.g.
RUST_LOG=askr=debug); default askr=info.
askr serve
Serve a PHP application over HTTP(S).
askr serve --root ./public --worker-script examples/laravel-worker.php \
--workers 8 --tls-self-signed --admin 127.0.0.1:9000
Pass options as flags, or put everything in a config file and run
askr serve --config askr.toml (the file is then the single source of truth and
the other flags are ignored). See Configuration.
Core
| Flag | Default | Meaning |
|---|---|---|
--config <FILE> |
— | Load all settings from askr.toml (other flags ignored). |
--root <DIR> |
./public |
Document root. |
--front <FILE> |
index.php |
Front controller, relative to root. |
--listen <ADDR> |
127.0.0.1:8000 |
Address to bind. |
--https |
off | Mark requests as HTTPS in $_SERVER (behind a TLS terminator). |
--ini <LINES> |
— | Extra php.ini lines (e.g. opcache). Overrides $ASKR_PHP_INI. |
--max-body-size <SIZE> |
16M |
Reject larger bodies with 413 (K/M/G). |
--force-https |
off | Redirect plain HTTP to HTTPS (308). Host redirects (www→apex) go in [[redirect]] in askr.toml. |
--tls-handshake-timeout <SECS> |
10 |
Max time a client may take to finish the TLS handshake (slowloris guard). |
--header-read-timeout <SECS> |
15 |
Max time a client may take to send request headers (slowloris guard). |
Workers & scaling
| Flag | Default | Meaning |
|---|---|---|
--workers <N> |
CPU cores (cgroup-aware) | Worker processes (one interpreter each). |
--worker-script <FILE> |
— | Boot the app once, serve many (Octane model). Omit for per-request. |
--max-requests <N> |
0 |
Recycle each worker after N requests (0 = never). |
--max-rss <MB> |
0 |
Recycle a worker gracefully once its RSS exceeds N MB (0 = never). Leak-aware: drains before PHP hits memory_limit and OOMs. Linux only. |
--shadow-to <URL> |
— | Mirror sampled safe (GET/HEAD, cookie-less) requests to this upstream (e.g. a staging deploy of the next version) and report divergence on /metrics. Never affects the client. |
--shadow-sample <PCT> |
100 |
Percent of eligible requests to mirror to --shadow-to. |
--workers-min <N> |
--workers |
CoW autoscaling floor. |
--workers-max <N> |
--workers |
CoW autoscaling ceiling (> min enables autoscaling). |
--cow |
off | CoW template: boot once, fork warm workers (~ms respawn). Needs --worker-script. |
--paranoid |
off | Dev: detect state bleed between requests (worker mode; expensive). |
TLS
| Flag | Default | Meaning |
|---|---|---|
--tls-cert <PEM> / --tls-key <PEM> |
— | Serve HTTPS from a cert + key (ALPN h2/http1.1). |
--tls-self-signed |
off | Generate a self-signed cert on startup (dev). |
--http3 |
off | Serve HTTP/3 (QUIC) on the TLS port alongside h1/h2, advertised via Alt-Svc. Requires --tls-cert/--tls-key; build with --features http3 (in the -full image). |
--acme |
off | Auto-TLS via ACME/Let's Encrypt (HTTP-01). See AUTOTLS. |
--acme-domain <D> |
— | Domain(s) (repeatable). Required with --acme. |
--acme-email <E> |
— | ACME account contact. |
--acme-dir <DIR> |
/var/lib/askr/acme |
Account + cert cache. |
--acme-staging |
off | Let's Encrypt staging. |
--acme-http <ADDR> |
0.0.0.0:80 |
Where to answer HTTP-01 challenges. |
--acme-directory-url <URL> / --acme-ca-root <PEM> |
— | Custom ACME directory / CA (Pebble, private CA). |
Sidecars (same process tree)
| Flag | Default | Meaning |
|---|---|---|
--queue <N> + --queue-script <FILE> |
0 |
Supervised queue-worker processes (floor when autoscaling). |
--queue-max <N> |
= --queue |
Autoscaling ceiling: scale the queue-worker pool between --queue and N on backlog (Horizon balance=auto, no extra daemon). |
--scheduler-script <FILE> |
— | Run the built-in scheduler (cron). |
--sidecar "<cmd>" |
— | Supervise an arbitrary command (repeatable), e.g. Inertia SSR. |
In-binary services (no Redis/Reverb)
| Flag | Default | Meaning |
|---|---|---|
--cache-slots <N> |
0 |
Shared kv cache (askr_cache_*; ~4.3 KB/slot). |
--cache-large-slots <N> |
0 |
Large-value region (64 KB/slot) — sessions, fragments. |
--response-cache <N> |
0 |
Full-response cache + tag invalidation (~140 KB/slot). See CACHE. |
--queue-slots <N> |
0 |
Shared-memory job queue (askr_queue_*; 32 KB/slot). |
--broadcast |
off | askr_broadcast() + SSE at /askr/events. See BROADCAST. |
--pusher |
off | Pusher-compatible WebSocket + trigger (drop-in Reverb; auto-enables broadcast). |
--pusher-secret <S> |
$ASKR_PUSHER_SECRET |
Verify private/presence subscription auth. |
Operations & hardening
| Flag | Default | Meaning |
|---|---|---|
--admin <ADDR> |
off | Admin dashboard/API + Prometheus /metrics. Bind to localhost. See ADMIN. |
--access-log <PATH|-> |
off | JSON access log per request (- = stdout). |
--canary |
off | Canary reload: roll one worker + health-check before the rest. |
--record-errors <DIR> |
off | Persist 5xx requests for askr replay. Sensitive. |
--sandbox |
off | Linux hardening: seccomp no-exec. See SANDBOX. |
--sandbox-write <DIR> |
— | Landlock: writes allowed only here (repeatable). |
Signals: SIGHUP = graceful rolling reload; SIGTERM/SIGINT = graceful drain + shutdown.
askr test
Run tests by forking a fresh, warm process per file (boot once, perfect isolation).
askr test --root . --runner examples/askr-test.php tests/
| Flag | Meaning |
|---|---|
[paths…] |
Test files/dirs (dirs scanned for *Test.php); default ./tests. |
--root <DIR> |
App base ($ASKR_APP_BASE). |
--runner <FILE> |
Runner invoked per file (PHPUnit/Pest); omit to run files directly. |
--parallel <N> |
Concurrent files (default CPU cores). |
--ini <LINES> |
Extra php.ini. |
askr replay
Replay a recorded failing request (see serve --record-errors).
askr replay /var/lib/askr/errors/<id>.json
askr doctor
Pre-flight checks: PHP build, extensions (required + recommended), platform
(io_uring probe on Linux). --ini <LINES> to load opcache. Exit non-zero on
critical failure.
--app <PATH>
Also check an application against the environment it is about to run in. Point it at the app
root (the directory with composer.json), ideally from inside the container that will serve
it, so it sees the same environment:
docker compose exec askr /opt/askr/askr doctor --app /var/www/example.com
• SESSION_DRIVER=askr — needs --cache-large-slots (sessions exceed 4 KB)
• QUEUE_CONNECTION=askr — needs --queue-slots *and* a worker
queue names found in app/: imports, mail, webhooks
this worker would poll: default
✗ jobs dispatched to imports, mail, webhooks would never be processed —
set ASKR_QUEUE=imports,mail,webhooks,default
✓ MAIL_MAILER=resend with an API key present
Configuration is resolved the way the application will resolve it: a real environment
variable beats .env, because Laravel's Dotenv skips variables that already exist. Each
run names the source it used, so running it from the host against a containerised server —
where it cannot see the container's environment — is visible rather than silently wrong. An
empty real variable counts as unset, since that is what the application sees.
• is an observation, ✓ a verified check, ✗ a failure that exits non-zero — so this can
gate a deploy. The distinction matters: doctor cannot see the flags a later serve will
get, and a tick that means "noted" teaches you to skim ticks.
What it looks for, all of it drawn from failures that cost real time:
- Queue names. It greps
app/foronQueue('x')and$queue = 'x'and compares them withASKR_QUEUE. A job dispatched to a queue no worker polls sits in the ring indefinitely with no error anywhere — the failure that made a site's password-reset and invitation mail silently stop. - Shared-memory drivers that need slots.
SESSION_DRIVER=askrwithout--cache-large-slotsloses sessions quietly, which surfaces as 419 on every form. - Mail that looks configured.
MAIL_MAILER=resendwith neitherRESEND_KEY(Laravel's name) norRESEND_API_KEY(Resend's own documentation) falls back to writing mail into the log. - Scheduled
->command()tasks, which shell out to aphpbinary. Askr compiles PHP in and ships no CLI, so in the Docker image every such task fails with exit code 127.
The grep is a grep: a queue name built at runtime won't be found, which is why the output says "found" rather than "all".
askr cache-report
Measure what caching would buy before caching anything — and whether it would be safe:
askr serve --config askr.toml --traffic-log /tmp/traffic.jsonl # run for an hour
askr cache-report /tmp/traffic.jsonl
pattern ttl hit PHP saved safety
----------------------------------------------------------------------------------
/products/* 60s 94% 1.48 s/m ✓ identical for every visitor
/dashboard 94% ... 0.11 s/m ✗ unsafe: 15 responses differed
/login 88% ... 0.06 s/m ✗ unsafe: 8 responses set a cookie
/ 88% ... 0.05 s/m ✓ identical for every visitor
Safe rules alone would have removed 73% of the PHP time above.
Suggested askr.toml:
[[cache.rule]]
path = "/products/*"
ttl = 60
The reason full-page caching is rare isn't performance, it's uncertainty: nobody knows how much a rule would win, or whether it would serve one visitor's page to everyone.
--traffic-log writes one JSON line per request that ran PHP — so it describes the
work still being done, not what the cache already absorbed — including a hash of the
response body. cache-report then groups URLs into patterns (/products/1421 →
/products/*), simulates each TTL, and for every candidate checks the question a
hit-rate estimate can't answer:
did the same URL ever return different bytes inside the TTL window?
If it did, the page is personalised and caching it would be a bug — so it's marked
unsafe and left out of the suggested config. Set-Cookie in the response is likewise
disqualifying. Pages whose requests carried cookies get a warning rather than a
refusal, because those cookies may be analytics only (see
ignore_cookies).
The report states what it doesn't know: a sample shorter than a minute is flagged as too short to extrapolate, and "identical for every visitor" means during the sample — not forever.
--traffic-log is a diagnostic, not a permanent setting. It costs one write per
PHP-served request and the file grows with traffic; turn it on, learn something, turn
it off.
askr tune
Measure the app and print an askr.toml you can paste, with one line of reasoning
per number:
askr tune --root public --requests 30
PHP boot 182.4 ms
Request (mean) 24.9 ms wall, 0.1 ms CPU
RSS after warm-up 94 MB
Suggested askr.toml:
[server]
workers = 64 # only 1% CPU-bound (waits on I/O) ⇒ more workers than cores
max_rss_mb = 220 # 2× observed peak (110 MB); memory grew 0.31 MB/request
It runs your front controller in-process a number of times and measures boot time, wall vs CPU time per request (that ratio is what decides whether more workers than cores will help), memory growth, and response size.
There is deliberately no HTTP load generator: Askr's own benchmarks show PHP is ~99.5 % of request time, so the interpreter is the thing worth measuring. The output also prints what it didn't cover — one route, no cookies, no concurrency — because a wrong recommendation is worse than none. Run it against a copy of production data, not an empty database.
askr config-check
askr config-check askr.toml — validate a config file and print the resolved
settings without starting the server.
askr upgrade
Self-update the release install in place. Downloads the matching Linux tarball
from GitHub, verifies its sha256, and swaps the whole prefix (binary + bundled
libphp) atomically — the previous version is kept at <prefix>/../askr.old for
rollback. Does not restart the running server unless you pass --restart.
askr upgrade --check # is a newer release available?
sudo askr upgrade # install the latest (then restart manually)
sudo askr upgrade --restart # install + systemctl restart askr
sudo askr upgrade --version 0.8.0 # pin a version (also to roll back)
| Flag | Meaning |
|---|---|
--check |
Report whether a newer release exists; install nothing. |
--version <X.Y.Z> |
Install a specific version instead of the latest (rollback). |
--restart |
Run systemctl restart askr after a successful swap. |
Needs write access to the install directory (run with sudo for /opt/askr).
Refuses to run inside a container — upgrade those by pulling a new
ghcr.io/kwhorne/askr tag. Linux release only; elsewhere build from source.