Elyra
Elyra The coding agent eTerm The terminal that knows where each command ends Starf An activity monitor for Apple silicon that never invents a number e The native code editor Elyra Grove Native local development environment Askr The real server for Laravel & PHP Elyra Framework Rust + Svelte 5 framework for desktop apps Elyra Conductor Local project conductor Elyra SQL Server MySQL-compatible SQL server in Rust Elyra Félagi Agents as teammates on one board Elyra SQL Client Native desktop SQL workbench Elyra SQL Anywhere Replication-ready SQL engine Elyra Sjá SEO & GEO workspace for macOS Elyra DataGrid Server-driven data grid for Laravel
Release notes
Changelog
Elyra
Upgrading Askr

Upgrading Askr

The short version: within 1.x, an upgrade is a drop-in. Replace the binary (or the image tag), reload, done. You don't need to touch askr.toml.

That's a promise, not a hope — the surfaces that make it true are listed in STABILITY.md, and new features arrive as new config keys that default to off, so a config written for 1.0.0 still means exactly the same thing on the newest 1.x.

One release has broken it, deliberately. 1.7.0 refuses to start an instance that uses [[site]] together with a queue or scheduler sidecar until you say which application the sidecar serves. The alternative was to keep guessing, and the guess had been silently throwing away every queued job on those instances since 1.5.1. If that is you, see To 1.7.0 — it is one line of config, and worth reading before you upgrade.

How to upgrade

Release tarball (systemd install)

askr upgrade                 # downloads, verifies signature + sha256, swaps the prefix
sudo systemctl reload askr   # graceful; see below

askr upgrade replaces the whole prefix (binary + bundled libphp) atomically and keeps the previous version at <prefix>/../askr.old. It does not restart the server unless you pass --restart.

From 1.5.0 it verifies a minisign signature against a public key compiled into the binary, and refuses a release it cannot verify — a missing signature included. The .sha256 is still checked, for what it is: proof the download arrived intact, not proof of who produced it.

Verify it yourself if you'd rather not trust the updater:

VER=v1.7.0; ARCH=$(uname -m)
BASE=https://github.com/kwhorne/askr/releases/download/$VER
TARBALL=askr-${VER#v}-linux-$ARCH.tar.gz

curl -fLO $BASE/$TARBALL
curl -fLO $BASE/$TARBALL.minisig
curl -fsSL https://raw.githubusercontent.com/kwhorne/askr/$VER/keys/release.pub -o askr.pub
minisign -V -p askr.pub -m $TARBALL

# And the provenance attestation, which binds it to the workflow and commit that built it
gh attestation verify $TARBALL --repo kwhorne/askr

Docker

docker pull ghcr.io/kwhorne/askr:1.7.0     # or :1.7 to follow patches

Pin the exact version in production and bump it deliberately. :1.7 follows patch releases, :latest follows everything — convenient for a laptop, surprising on a server at 3am.

The -full tags (1.7.0-full) are the same server built with the optional features compiled in: sql-backend, observ, otel, http3. If you use any of those, stay on -full.

The Laravel package

composer update kwhorne/askr-laravel

The package and the server are versioned independently; any recent package works with any 1.x server. Upgrade it when you want a new PHP-side helper.

Zero-downtime upgrades

A reload replaces the workers without dropping a connection:

kill -HUP $(pidof askr)      # or: systemctl reload askr, or POST /api/reload

Workers finish their in-flight requests, then are replaced one at a time — there is always a live worker accepting. A reload does not pick up a new Askr binary: the master process is the old one. For a new binary you need a restart, which means a brief gap unless something in front of you retries.

If you can afford one more moving part, the sturdiest sequence is:

  1. askr upgrade (new binary on disk, old one kept)
  2. askr config-check askr.toml — catches a config that the new version rejects before you stop anything
  3. drain the queue — the shared-memory ring does not survive a restart, and jobs still in it are lost with no error anywhere the application can see (Maintenance)
  4. restart

Turn on the canary so a bad application deploy can't take the fleet with it — worth having in place before you start upgrading things:

[reload]
canary = true
canary_window = 5
canary_min_requests = 20

See Deployment.

Rolling back

  • Tarball: the previous prefix is at <prefix>/../askr.old. Swap it back and restart.
  • Docker: run the previous tag. This is why pinning matters.
  • Config: a config written for an older 1.x is still valid on a newer binary, so upgrading never requires touching askr.toml. The reverse is not true: unknown keys are rejected, so if you have added a key for a newer release — [queue] root in 1.7.0, say — a rollback needs that key removed. askr config-check askr.toml run against the binary you are about to roll back to tells you in one command, before you stop anything.

Rolling back is a supported operation, not an emergency improvisation. If a downgrade ever fails on a config that the newer version accepted, that's a bug worth reporting — it means we added something that isn't additive.

Version-by-version notes

Nothing here is required. These are the things worth adopting after each upgrade.

To 1.7.0

Read this one before upgrading if you use [[site]]. Two things can bite, and one of them may already have.

Your queue may have been dead since 1.5.1. Shared memory is namespaced per application, derived from the docroot. [[site]] makes each virtual host a separate application — but queue and scheduler sidecars took the namespace of the top-level [server] root, and askr_queue_pop matches the namespaced key. A sidecar rooted at one application cannot see another's jobs at all. Jobs were accepted, stored, and never read: no exception, no failed job, nothing the application could see. One deployment ran that way for six days.

Check before you upgrade, on whatever version you are on. Enqueue a job and watch reserved go above zero:

curl -s -H "Authorization: Bearer $ASKR_ADMIN_TOKEN" http://127.0.0.1:9000/api/status \
  | jq '.queues'

reserved: 0 on a lane with pending above zero, holding steady, is the signature. An empty queue proves nothing, and neither does an absence of backlog warnings straight after a restart — a restart recreates the ring, so the warnings are guaranteed to be quiet for a while whether or not anything is wrong.

A [[site]] instance with a sidecar now refuses to start until you say which application the sidecar serves:

[queue]
root = "/var/www/example.com/public"   # = the [[site]] root that dispatches the jobs
slots = 8192
workers = 4
script = "/opt/askr/examples/askr-queue.php"

[scheduler]
root = "/var/www/example.com/public"   # defaults to [queue] root
script = "/opt/askr/examples/askr-scheduler.php"

If the top-level application is the one queueing, set [queue] root to the same path as [server] root. That is a valid answer — it just has to be an answer, because Askr cannot infer it from a queue script and the previous default was a guess that failed in silence.

Single-application instances need nothing. No [[site]], no change: both keys default to [server] root, which is already what you want.

If you alert on queue metrics, every per-queue series now carries an app label alongside queue, so a recording rule or dashboard that groups by queue alone will now see one series per application instead of one merged series. There is also a new gauge worth an alert of its own:

# Jobs one application queued that only another application's workers poll.
# Adding workers cannot fix this — the queue name is right and the application is not.
max by (queue, app) (askr_queue_unreachable) == 1

And /api/status gained app on every queue entry, plus a queue_wrong_application warning kind carrying polled_by. Switch on kind, never on detail.

To 1.6.1

Nothing to do. A dependency refresh and two test fixes; no behaviour changed, no config key added, nothing to adopt. Take it for the refreshed TLS stack (rustls 0.23.44, aws-lc-rs 1.18.1) if you terminate TLS in Askr, or skip it and take 1.6.2 when there is one.

To 1.6.0

Nothing is required, and one thing is worth wiring up the same day. GET /api/status now carries a top-level warnings array — empty when all is well, and populated with a stable kind, the queue name and the numbers behind it when a queue is not being drained. Render it somewhere a person looks. That is the whole point of the release: Askr has been able to diagnose an abandoned queue for several versions and could only say so in its log, where a production site missed it for three days.

Switch on kind (queue_unattended, queue_not_draining), never on detail — the prose is not stable, and is written for a human reading a dashboard.

If you scrape Prometheus, the new per-queue series are worth an alert:

# A lane with jobs that nothing is polling. The remedy is the queue *name*, not more workers.
max by (queue) (askr_queue_unattended) == 1

Note that askr_queue_seconds_since_poll and askr_queue_seconds_since_drain are absent for a lane where it has never happened rather than 0 — 0 would read as "just now" — so alert on askr_queue_unattended or use absent(), not on a comparison that a missing series silently never satisfies.

If your queues are deliberately batchy, [queue] stall_secs (default 30) is now the one knob behind the log line, the warnings array and askr_queue_unattended. Raise it rather than learning to ignore the warning.

Docker users: check your --admin bind. docs/DOCKER.md used to open with --admin 0.0.0.0:9000, which since 1.5.1 refuses to start without ASKR_ADMIN_TOKEN — and which, even with a token, puts the reload trigger on the Docker network. The documented pattern is --admin 127.0.0.1:9000: the image's healthcheck runs inside the container and reaches it, and the port leaves the network entirely. If you copied the old quick-start, this is the change to make. Nothing in the binary changed here; the documentation was wrong.

The job ring's layout version changed, so [queue] persist rings written by an older Askr are recreated empty at boot, with a log line saying why. Pending jobs in a persistent ring do not survive this one upgrade. Drain the queue before upgrading if that matters; persist is off by default, so most deployments are unaffected.

To 1.5.2

If you are on 1.5.1, this upgrade is not optional. 1.5.1 answered 400 to every HTTP/1.x request to a Laravel or Symfony app, because HTTP_HOST reached PHP as works.example, works.example:443 and Symfony rejects a comma in the host. HTTP/2 was unaffected, so a site behind an h2 proxy — or an HTTP client pinned to 2.0 — may never have seen it. Nothing to configure: upgrade and remove any ['version' => 2.0] you added to work around it.

One value changes, and only over HTTP/2. HTTP_HOST is now the authority exactly as the client sent it, port included — which is what nginx + FPM pass, and what PHP already saw over HTTP/1.x. Over HTTP/2 it previously arrived port-stripped, so an h2 request to a non-default port now shows example.test:8443 where it showed example.test. This is the correct form and the two protocols now agree, but if you compare HTTP_HOST against a literal, or feed it somewhere that expects no port, use SERVER_NAME — that is still the port-stripped form, and it is what virtual-host routing and the response-cache key use. In Laravel, $request->getHost() strips the port for you; getHttpHost() keeps it, as it should.

To 1.5.1

One change can stop a start. A non-loopback admin bind now requires ASKR_ADMIN_TOKEN; without it the server refuses to start rather than exposing an open reload trigger and a public dump of PIDs and memory. If you run --admin 0.0.0.0:… (or any non-loopback [admin] listen) with no token, set one, or bind the admin plane to 127.0.0.1 and reach it over SSH. A loopback bind is unchanged.

One change is visible to code that reads job ids. Queue jobs are now leased: a job's id, as the driver hands it back, changes on each retry, because it identifies the reservation rather than the row — that is what stops a worker whose lease lapsed from acknowledging a job another worker has since taken. If you correlate log lines across a job's attempts, key on the payload's uuid (what Laravel's failed_jobs uses), not the id.

The rest need no action, but are worth knowing:

  • Multi-site instances are partitioned. With [[site]], the shared cache, sessions, locks, counters and job queue are now keyed per application (by docroot), so two sites in one instance no longer share them — two domains on one docroot still do. Queue and scheduler sidecars belong to the application at the top-level root; a second application in the same instance has no workers of its own. Broadcasting stays instance-wide (one Pusher secret per instance). See Hosting.
  • Vary responses are cached again. In 1.5.0 a response carrying its own Vary (e.g. Accept-Language) was not cached; it is now stored as one variant per value, so the hit rate returns on localised pages without serving one visitor's language to another.
  • The Pusher HTTP trigger requires a signature. POST /apps/{id}/events must carry Pusher's auth_signature (and body_md5, auth_timestamp). pusher-php-server — and therefore Laravel's broadcaster — sends it on every call, so a correctly configured app needs no change; a bespoke trigger caller must sign, or it gets a 401.

New, opt-in: [queue] persist = "<name>" keeps the job ring in a named shared-memory object so pending jobs survive a restart (askr upgrade included). In a container raise --shm-size; see Docker.

To 1.5.0

Nothing is required. Two things are worth adopting deliberately, because upgrading alone will not turn them on:

  • --sandbox-required / [server] sandbox_required. The sandbox used to warn and serve unhardened when a kernel feature was missing, which looks identical to success. Required mode refuses to serve instead. It needs sandbox_write, and refuses to start without it — seccomp alone does not stop PHP writing a webshell, because Askr interprets PHP in-process and no process creation is involved. See Sandbox.
  • ASKR_ADMIN_TOKEN, if you run behind a local reverse proxy. PURGE/BAN used to be accepted from any loopback peer with no token — and behind nginx or Caddy on 127.0.0.1, every request is a loopback peer. Setting trusted_proxies now makes the token mandatory for those methods, so if you have trusted_proxies configured and no token, cache invalidation will start answering 403. That is the fix working; set the token.

Three behaviour changes to be aware of, none of them configurable:

  • A response carrying its own Vary is no longer cached. The cache key cannot express an arbitrary Vary, and the header used to be dropped rather than honoured — so a localised app answering Vary: Accept-Language had one visitor's language served to everyone. Correctness costs hit rate on exactly those responses.
  • Scheme is part of the cache key. With force_https off, http and https no longer share an entry. Expect a one-time dip in hit rate.
  • Underscored header names are dropped. X_Forwarded_For no longer becomes HTTP_X_FORWARDED_FOR in $_SERVER, because it collided with the dashed spelling and bypassed anything filtering it. This is the same default nginx ships. If an app of yours genuinely reads an underscored header, rename it to use dashes.

Releases are now signed, and askr upgrade refuses one whose signature does not verify against the key compiled into the binary. Upgrading to 1.5.0 from an older build still uses the old checksum-only path — the verification lives in the new binary, so it protects the upgrade after this one.

To 1.4.14

Nothing to do. If you use askr doctor --app, two things behave better:

  • It now resolves configuration the way the application does — real environment variables beat .env — and names the source of each value. Run it inside the container for this to be meaningful.
  • Its scheduler check no longer fires on Artisan::command(), which defines a command rather than scheduling one.

To 1.4.13

If you run queue workers, make sure [queue] slots is set8192 is a reasonable start. Askr now refuses to start without it when queue workers are configured, so a misconfiguration is an error at boot rather than mail that quietly never sends.

[queue]
slots = 8192          # required alongside workers
workers = 4
script = "/opt/askr/examples/askr-queue.php"

The equivalent on the command line: --queue-script now requires --queue-slots.

To 1.4.12

Take this one if your app draws QR codes — Laravel Fortify's two-factor setup, most likely. Before it, iconv was not compiled in, and the page answered 500 with Call to undefined function iconv(). Nothing to change beyond the version.

If you build from source on macOS, PROFILE=minimal also works again there.

To 1.4.11

Nothing to do; everything is additive. Two things you may now see for the first time:

  • WARN queue backlog is not being consumed in the log, naming a queue. It is telling you the truth: jobs on that queue are not being taken. Either no queue worker is running (--queue with --queue-script) or it doesn't poll that name (ASKR_QUEUE, comma-separated). This was previously invisible.
  • /api/status has a queues array. queue_ready and friends are unchanged.

Worth running once against a running deployment:

askr doctor --app /var/www/example.com        # from inside the container, ideally
./scripts/smoke.sh https://example.com http://127.0.0.1:9000 "$ASKR_ADMIN_TOKEN"

Both exit non-zero on a failure, so they can gate a deploy. The first found a real misconfiguration on the deployment it was written against; the second found a real production fault on its first run.

To 1.4.10

Nothing to do — both changes are additive.

If you use auto-TLS and wanted a config file, you can now have both: move the flags into an [acme] section (see CONFIGURATION.md). Note that [acme] and [tls] are mutually exclusive, and that a [acme] section with domains but no enabled = true is now an error rather than a silent no-op.

If you monitor queue depth, delayedSize(), reservedSize() and creationTimeOfOldestPendingJob() now report real numbers instead of 0/null — a threshold that has never fired may start firing, because it was reading zeros.

To 1.4.9

Package-only fix — composer update kwhorne/askr-laravel. On Laravel 13 the queue driver was a fatal error at class-load time (missing contract methods), so anything that resolved the queue — sending mail, dispatching a job — killed the worker and answered 502 with askr: php worker died mid-request. No server upgrade needed.

Note that delayedSize() and reservedSize() report 0 and creationTimeOfOldestPendingJob() reports null on this driver: Askr's queue knows those values internally but doesn't expose them to PHP yet. If you rely on queue:monitor thresholds, use pendingSize()/size(), which are accurate.

To 1.4.8

Upgrade if you run Livewire, Flux or anything needing Alpine in worker mode, and take the worker script with it — the fix is in examples/laravel-worker.php, so a new binary alone changes nothing if you copied the old script into your project:

docker pull ghcr.io/kwhorne/askr:1.4.8
docker run --rm ghcr.io/kwhorne/askr:1.4.8 \
  sh -lc 'cat /opt/askr/examples/laravel-worker.php' > storage/askr-worker.php   # if you keep your own copy

Symptoms this fixes: interactivity that works for the first page load or two and then stops, wire: and x- attributes doing nothing, a Flux appearance toggle showing both icons — all with an empty console, because the script tag was missing rather than broken.

To 1.4.7

Upgrade if you serve HTTPS. Over HTTP/2 — which ALPN picks by default over TLS — Askr lost the host the request was addressed to. Nothing to configure; the effects were:

  • generated URLs and redirects came out as https://localhost/… (Laravel builds them from the request, not from APP_URL);
  • virtual hosts fell through to the default site, so with [[site]] an h2 request for one domain could be served another's app — check your access logs if you host several;
  • response-cache entries could be shared between domains. If you cache and host more than one domain, flush the cache after upgrading: curl -X BAN -H 'X-Ban-Url: /*' … or simply restart without [cache] persist.

To 1.4.6

If you use --config together with other flags, Askr will now refuse to start and tell you which flags it would have ignored. That's the fix: they were never applied. Move them into the config file. Nothing else changes.

Worth reading if you deploy with Docker on Linux: the new bind-mount ownership and behind-nginx sections cover the traps that make a laptop-tested compose file fail on a server.

To 1.4.5

Upgrade — this closes Askr-46. If you serve a Laravel app with Flux/Livewire in worker mode, file responses (flux.js, downloads, streamed exports) were killing workers; that is gone, with the standard asset setup and no workarounds. Also: an exit() or an escaping exception in the app now costs that one request instead of the worker. The docs/WORKER_MODE.md known-issue section is obsolete as of this release.

To 1.4.4

Nothing to do. No behaviour changes for a healthy app — this release is about what happens when one isn't:

  • a failed accept() no longer takes down a worker that is serving other requests;
  • a worker that dies mid-request answers 502 instead of a complete-looking 200 with an empty body (if you have monitoring that only checks status codes, it will start seeing these — they were always failures, just invisible ones);
  • the log says what actually ended a worker instead of guessing "fatal/OOM?". If you have alerts matching that string, they won't fire any more. The replacements name the case: the request channel closing, or the worker script leaving its loop.

To 1.4.3

Upgrade if you run Laravel in worker mode, and take the new worker script with it — the fixes are in examples/laravel-worker.php, so a new binary alone changes nothing if you copied the old script into your project:

docker pull ghcr.io/kwhorne/askr:1.4.3
composer update kwhorne/askr-laravel        # for the cache/queue config fix
# using your own copy of the worker script? re-copy it:
docker run --rm ghcr.io/kwhorne/askr:1.4.3 \
  sh -lc 'cat /opt/askr/examples/laravel-worker.php' > storage/askr-worker.php

What changes for you:

  • Authenticated requests no longer leak between visitors. If you ran worker mode with sessions before 1.4.3, this was happening — quietly, and only on workers that had served a login.
  • HTML form posts work. If you saw unexplained 419s on submit and worked around them with a header or by disabling CSRF for a route, undo that.
  • File downloads and streamed responses have bodies. Anything you thought was a Flux, Livewire or download bug is worth re-testing.
  • CACHE_STORE=askr works without editing config/cache.php. The manual entry the package README documented is now optional; keeping it changes nothing.

To 1.4.2

Two things are worth acting on.

If you set ASKR_ADMIN_TOKEN and run the Docker image, your containers were reporting unhealthy — the healthcheck polled the gated /api/status. Nothing to configure; the image now polls /healthz. If you wrote your own probe, point it at /healthz too.

If you want http:// visitors redirected, you can now have it:

[server]
force_https = true
http_redirect = "0.0.0.0:80"      # not needed with --acme; automatic there

Also worth knowing:

  • The admin plane denies by default. If you drive it with a script that hits some path other than /, /favicon.ico or /healthz, that path now needs the bearer token. The documented endpoints are unchanged.
  • ACME keys are re-written 0600. If your tooling read key.pem as a non-owner user, it will stop. That it worked before was the bug.

To 1.4.1

Upgrade if you serve with the Docker image or the release tarball. Before 1.4.1, a PHP notice, warning or deprecation was written into the HTTP response body — including absolute filesystem paths — and not logged. The published 1.4.0 image served this to anyone requesting the homepage of a stock Laravel 12 app on PHP 8.5:

Deprecated: Constant PDO::MYSQL_ATTR_SSL_CA is deprecated since 8.5 …
in /app/vendor/laravel/framework/config/database.php on line 62

A framework masks this once its own error handler is installed, but config files are parsed before that, so boot-time diagnostics went straight to the client. In worker mode it also preceded the headers and truncated the page.

Nothing to configure — diagnostics now go to the log. Two things to know:

  • If you relied on seeing PHP errors in the browser during development, opt back in: ASKR_PHP_INI="display_errors=1".
  • Check your log volume after upgrading. Notices that were previously discarded (log_errors was off) are now written. If a busy app emits one per request, that's real output — and a good prompt to fix the notice, since it was always there.

error_reporting is unchanged (E_ALL), so nothing new is hidden; it changed destination, not visibility.

To 1.4.0

Worth doing, in this order:

# 1. Find out what's actually worth caching — and what only looks cacheable
askr serve --traffic-log /tmp/traffic.jsonl    # leave it for an hour of normal traffic
askr cache-report /tmp/traffic.jsonl
// 2. Cache the routes it called safe. No tag list to maintain: the page is tagged
//    with the models it read, so a save() clears exactly the pages that showed them.
Route::get('/products/{product}', ProductController::class)
    ->middleware('askr.cache:300');

Requires composer update kwhorne/askr-laravel for the middleware.

One behaviour change to know about. A response carrying more cache tags than an entry can hold (8) is now refused rather than cached. Before, the surplus tags were silently dropped — which meant askr_cache_forget_tag() could never reach them, and the page stayed stale until its TTL expired.

If you hand-write long tag lists you may therefore see a page stop being cached, and askr_cache_tag_overflow_total count up on /metrics. That page was already broken; it just failed quietly. Tag by class (posts) instead of per instance (posts:3), or cache a smaller fragment with ESI.

--traffic-log is a diagnostic: it writes a line per PHP-served request, so turn it off again when you have your answer.

To 1.3.0

No action needed. Internally this is a dependency refresh (including four major bumps) plus a much larger test suite; no user-visible behaviour changed.

Two things to know:

  • A persisted response cache may be dropped once. [cache] persist files are tied to the entry layout, and a new Askr build can invalidate them. The first boot after an upgrade then starts with a cold cache and logs response cache dump ignored (different build or cache size). That's the guard working — a cache is never reinterpreted across layouts.
  • ASKR_*_DB SQLite files are opened by a newer bundled SQLite (rusqlite 0.31 → 0.40). SQLite is backwards compatible with older files, so nothing to do, but take your usual backup first if those hold queue jobs you can't lose.

To 1.2.0

Worth adopting:

# Refuse abusive traffic before PHP wakes up — enforced across the whole fleet
[server]
trusted_proxies = ["10.0.0.0/8"]     # required behind a load balancer

[[ratelimit]]
path = "/login"
limit = 5
window = 300

# Keep the cache across restarts
[cache]
persist = "/var/lib/askr/rcache.bin"
persist_key = "your-release-sha"
  • If you configure rate limits behind a proxy and forget trusted_proxies, every client shares one bucket and you'll rate-limit your whole site. Askr warns at startup; take the warning seriously.
  • Run askr tune --root public for measured starting values for workers and max_rss_mb.
  • The canary gate got much better in this release (it compares the new worker against the rest of the fleet instead of an absolute error count). If you'd tried canary before and found it aborted deploys for no reason, try it again.

To 1.1.0

Worth adopting: ESI if a single live widget is what keeps a page uncacheable, PURGE/BAN for URL-targeted invalidation, and [[cache.rule]] if you need cache policy for an app you can't edit.

PURGE/BAN are gated on ASKR_ADMIN_TOKEN, or restricted to loopback when no token is set. Set the token if you want to invalidate from a deploy script on another host.

To 1.0.1

Upgrade if you're on 1.0.0. Static file serving could disclose PHP sources and dotfiles: GET /index.php returned source, and with a document root pointed at an app root, GET /.env returned APP_KEY and database credentials. Fixed in 1.0.1, with the suffixed variants (index.php.bak, config.php~) fixed in 1.1.0.

While you're there, check that your document root is a dedicated public/ directory and not the application root.

From 0.9.x to 1.0

1.0 added no features — it froze the surface. If your 0.9.12 setup worked, 1.0 works identically.

One deprecation carried over: --acme-directory became --acme-directory-url in 0.9.7 (it was too easily confused with --acme-dir, the local certificate cache). The old spelling still works as a hidden alias.

What can actually bite you

Honest list, in rough order of how often it happens:

  1. A cold cache after a restart. The response cache lives in shared memory. Unless [cache] persist is set — and the dump is still valid — the first requests after any restart hit PHP. Coalescing stops it becoming a stampede, but a big site restarting at peak will feel it. Reload rather than restart when you can.
  2. A config the new version rejects. Validation gets stricter as it gets better (glob patterns that look like regexes, rules with no effect, unknown keys). This is deliberate — a silently ignored rule is worse — but it means askr config-check askr.toml belongs in your deploy script, before the restart.
  3. libphp and the binary are a matched pair. A release tarball contains both. Don't mix a new askr with an old bundled libphp; askr upgrade and the Docker images handle this for you.
  4. A PHP security release means a new Askr release. PHP is compiled into the distribution, so you can't patch it independently. Watch PHP's releases as well as ours.
  5. Optional features live in the -full build. Upgrading from a -full tag to a plain one silently drops sql-backend, observ, otel and http3. The server will start and your ASKR_OBSERV_DSN will simply do nothing.

After upgrading

askr doctor                    # PHP build, extensions, platform probes
askr config-check askr.toml    # config still valid and resolving as you expect
curl -s localhost:9000/api/status   # workers alive, version, rollout state

If something looks wrong, the admin API and the observability guide are the fastest ways to see what the server thinks is happening.