All releases

v0.4.0

Signedpublished August 14, 2026

What changed

Trust and polish for the install path (INSTALL_UPGRADE_PLAN.md phase 4), and the first release signed with a key installers can verify against.

Added

  • DNS is checked before the stack starts. install.sh resolves the dashboard hostname and a random label under the wildcard domain, compares them against this host's addresses, and says so before Let's Encrypt burns a failed challenge against a name that does not reach you. It asks rather than refusing — split-horizon DNS and a pre-cutover install are legitimate — and --skip-dns-check turns it off.
  • --dry-run reports what an upgrade would change: release, image and asset versions, download size against free disk in both filesystems, which settings would change, and whether a snapshot would be taken. It writes nothing.
  • --yes for unattended runs: never prompts, takes the prior value for every question. It deliberately does not answer the post-failure rollback question — that is what --auto-rollback is for.
  • A concurrency lock. Two installers at once raced on .env, versions.env, config.yaml and docker compose up; the second now waits and then refuses.
  • Release signing. make publish can sign a release with a key that is not on the web host (scripts/release-key.sh generates one), and install.sh verifies it against a key embedded in the script, then refuses any artifact whose digest is not in the signed payload. Until now the only integrity check was a digest served by the same origin as the bytes, which catches corruption but not a compromised registry. Unverifiable releases warn and install; DEVGRAIL_REQUIRE_SIGNATURE=1 makes them a refusal. The trust model, the signed payload format, and the key rotation procedure are documented in docs/SECURITY.md.
  • docker-compose.override.yml is supported. The compose file and Traefik config are product-owned and rewritten on every run; local changes belong in the override, which the installer never touches and now passes to every compose invocation. Documented in docs/RUNBOOK.md.
  • README.md with an OS/arch support matrix and minimum RAM/disk, and a LICENSE.
  • A test asserting migration numbers stay unique, so the collision that exists today (005_api_keys / 005_projects) cannot be joined by another.

Changed

  • The registry's "current release" no longer depends on scan order when two releases are published in the same millisecond.
  • The admin dashboard marks each release as signed or unsigned.
  • install.sh installs openssl alongside jq when it is missing.
  • The VPS marketplace / cloud-init packaging described in the spec is now marked explicitly as not built.
  • Publishing a release is one command: scripts/publish.sh takes the version from this file, finds the registry URL, tokens and signing key on its own, and runs on Windows as well as Linux. make publish is unchanged.

Fixed

  • arm64 workspaces get a terminal that runs. The workspace image fetched the x86_64 ttyd binary whatever it was being built for. The pinned digest matched (it is the same file), so the build passed and the breakage only appeared on a customer's arm64 host, where the terminal could not start. The binary is now chosen by TARGETARCH, with a pinned digest per architecture, and an unrecognised architecture fails the build instead of shipping.

Container images

registry build 8
amd64747 MB
sha256:f061201afa934a2a14c282492afdeef7331f3310fa3b605be04279ca367a73f0
Download
arm64616 MB
sha256:96446a08986a283710a4c686c0f245d90f2ce52202ed8ebbd34f398732f8e208
Download
registry build 8
amd6445 MB
sha256:76ecbcb59368ebd4d809876aea9ab3a82668bef79900f2c974700b6362b4dc47
Download
arm6444 MB
sha256:2c61596f68f278d2039fe7c56a74c3ead151904be862f50efe14913bda784819
Download

Installer files

docker-compose.yml11 KBsha256:d716c3cd9ffd1f4a87caacb8d1b444646f9cf388949d8c2805d2676981307abc
install.sh85 KBsha256:4890788c5c172561e228ce0c8183d81b37edeccd385765a28e45ff47c836c511
traefik-dynamic-http.yml5 KBsha256:e344c8efd13c7d84996e2ebfbc75dcee2d995d8538c8a245c89c0e44fad0089b
traefik-dynamic.yml6 KBsha256:d18fa561b8eb7d53b015284e0246b2d05953bf281b65269c1fae3d6883530db6
uninstall.sh10 KBsha256:d8f09fe906e13f57bc308f6f2340972a62b010c367174ec79597c86a84c706be

Signature

ecdsa-p256-sha256key f1f1eaf642454d65

One signature covers every artifact above. It is made off this host, and neither this site nor the registry behind it can check it — that is the point, and it is why verifying it catches things a checksum from the same server cannot. How to verify it.

Verify devgrail-container (amd64)
curl -fsSL https://web-dev.trixibot.com/api/registry/releases/v0.4.0 -o release.json

# -j, not -r: -r appends a newline the signer never saw, and the check fails.
jq -j '.signature.payload'   release.json > payload
jq -r '.signature.signature' release.json | base64 -d > payload.sig

openssl dgst -sha256 -verify devgrail-release.pem -signature payload.sig payload
# -> Verified OK

# The payload must name this release, so an older signed one cannot be replayed.
head -2 payload

# ...and it must name this exact file. -qxF: whole line, literal.
grep -qxF 'image devgrail-container 8 amd64 f061201afa934a2a14c282492afdeef7331f3310fa3b605be04279ca367a73f0' payload && echo "digest is covered by the signature"
The signed payload — shown for reading, not for verifying

Copy this from the manifest with jq -j, never from this page: the signature covers exact bytes, and rendering here drops the trailing newline the signer included.

devgrail-release-signature-v1
release v0.4.0
asset docker-compose.yml 7 d716c3cd9ffd1f4a87caacb8d1b444646f9cf388949d8c2805d2676981307abc
asset install.sh 7 4890788c5c172561e228ce0c8183d81b37edeccd385765a28e45ff47c836c511
asset traefik-dynamic-http.yml 6 e344c8efd13c7d84996e2ebfbc75dcee2d995d8538c8a245c89c0e44fad0089b
asset traefik-dynamic.yml 7 d18fa561b8eb7d53b015284e0246b2d05953bf281b65269c1fae3d6883530db6
asset uninstall.sh 7 d8f09fe906e13f57bc308f6f2340972a62b010c367174ec79597c86a84c706be
image devgrail-container 8 amd64 f061201afa934a2a14c282492afdeef7331f3310fa3b605be04279ca367a73f0
image devgrail-container 8 arm64 96446a08986a283710a4c686c0f245d90f2ce52202ed8ebbd34f398732f8e208
image devgrail-server 8 amd64 76ecbcb59368ebd4d809876aea9ab3a82668bef79900f2c974700b6362b4dc47
image devgrail-server 8 arm64 2c61596f68f278d2039fe7c56a74c3ead151904be862f50efe14913bda784819