Skip to content

Releases

v0.6.1

Choose a tag to compare

@github-actions github-actions released this 08 Nov 18:01
Immutable release. Only release title and notes can be modified.
a88aecc

Souin's shared cache no longer leaks one client's Set-Cookie to the next. The Caddy cache plugin moves from v1.7.9 to v1.7.10, and cmd/motel/souin_test.go holds nine of its fixes in motel's own configuration. This release is the commit that ran as v0.6.1-rc1 on a real box: upgraded from v0.6.0, serving, an hourly backup and a restore drill, then upgraded to this release with docs/upgrading.md as written.

Upgrading:

  • On the box, follow docs/upgrading.md. Nothing is renamed or moved; the package keeps your Caddyfile.
  • If a box repository's MOTEL_VERSION variable pinned the candidate, delete it, so its checks use this release.

Behaviour changes since v0.6.0

Responses that set a cookie are not cached. With cache in front of an app, a response carrying Set-Cookie was stored and replayed to the next visitor, cookie and all (darkweak/souin#805). It is no longer stored unless the origin sends Cache-Control: no-cache="Set-Cookie", in which case the copy is stored without the cookie. Apps behind cache that set a session cookie on every response will see their hit rate fall; that traffic was never safe to cache.

cache_keys in a handler block start applying, and stale-while-revalidate refreshes in the background. On v1.7.9 the first cache_keys rule was dropped and a backslash in a rule's regexp silently disabled the cache (darkweak/souin#688); stale-while-revalidate never refreshed behind reverse_proxy (#699). Both now work as the Souin docs describe, so origins see the background revalidation requests. Two Souin bugs remain and are skipped in the suite with the condition that lifts each: otter keeps no stale copy for stale-if-error, and cache_keys in the global block never reach a bare cache directive.

motel-backup setup --existing <url> opens a rebuilt box's repository without making a new password: it asks for the existing one and sets the box up on the snapshots already there (#389). On a rebuilt box, run it where the guide says setup.

A {$NAME:default} directive no longer refuses the push. The backups check read a defaulted placeholder as an unset variable (#394); it now checks the directive by form.

The upgrade's Backups: line says what is so. On a set-up box it reported the backups as not set up and told you to run setup on every upgrade (#403). It now reports each timer's state and whether the backups are set up, and points at motel-backup status.

Security sweep (#407). Six findings of the 2026-11-06 sweep fixed across backups, box proof and liveswap; none was reachable from outside the box. go-jose/v3 (three unfixed denial-of-service advisories) leaves the build (#405).

What's Changed

  • build(deps): bump the dockerfiles group across 2 directories with 1 update by @dependabot[bot] in #398
  • fix(packaging): postinstall says the backups are set up when they are by @earthlingdavey in #404
  • deps: drop go-jose/v3 (smallstep/certificates master + go.step.sm/crypto 0.92.0); x/net 0.62.0; pin-watch comment and stale checks by @earthlingdavey in #405
  • build(deps): bump github.com/spf13/pflag from 1.0.10 to 1.1.0 in the go-deps group across 1 directory by @dependabot[bot] in #408
  • build(deps): bump curlimages/curl from 8.22.0 to 8.23.0 in the compose-images group by @dependabot[bot] in #409
  • ci(pin-watch): the header says what the workflow watches since #405 by @earthlingdavey in #410
  • test(liveswap): date deploy records in the tests that read their order (#376) by @earthlingdavey in #411
  • feat(backups): setup --existing opens a rebuilt box's repository and makes no password (#389) by @earthlingdavey in #413
  • security: fix six findings of the 2026-11-06 sweep by @earthlingdavey in #407
  • fix(backups): a {$NAME:default} a directive checks by form no longer refuses the push (#394) by @earthlingdavey in #412
  • liveswap: replace known values before the caps cut; a length-aware entropy bar by @earthlingdavey in #414
  • fix(usage): one ledger file per machine, summed on the page; --match takes several names by @earthlingdavey in #406
  • chore(usage): this VM's ledger file is lima64; sessions to 2026-11-07 by @earthlingdavey in #415
  • deps(souin): bump the Caddy plugin to v1.7.10 by @earthlingdavey in #416

Full Changelog: v0.6.0...v0.6.1

v0.6.1-rc1

v0.6.1-rc1 Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 08 Nov 17:40
Immutable release. Only release title and notes can be modified.
a88aecc

What's Changed

  • build(deps): bump the dockerfiles group across 2 directories with 1 update by @dependabot[bot] in #398
  • fix(packaging): postinstall says the backups are set up when they are by @earthlingdavey in #404
  • deps: drop go-jose/v3 (smallstep/certificates master + go.step.sm/crypto 0.92.0); x/net 0.62.0; pin-watch comment and stale checks by @earthlingdavey in #405
  • build(deps): bump github.com/spf13/pflag from 1.0.10 to 1.1.0 in the go-deps group across 1 directory by @dependabot[bot] in #408
  • build(deps): bump curlimages/curl from 8.22.0 to 8.23.0 in the compose-images group by @dependabot[bot] in #409
  • ci(pin-watch): the header says what the workflow watches since #405 by @earthlingdavey in #410
  • test(liveswap): date deploy records in the tests that read their order (#376) by @earthlingdavey in #411
  • feat(backups): setup --existing opens a rebuilt box's repository and makes no password (#389) by @earthlingdavey in #413
  • security: fix six findings of the 2026-11-06 sweep by @earthlingdavey in #407
  • fix(backups): a {$NAME:default} a directive checks by form no longer refuses the push (#394) by @earthlingdavey in #412
  • liveswap: replace known values before the caps cut; a length-aware entropy bar by @earthlingdavey in #414
  • fix(usage): one ledger file per machine, summed on the page; --match takes several names by @earthlingdavey in #406
  • chore(usage): this VM's ledger file is lima64; sessions to 2026-11-07 by @earthlingdavey in #415
  • deps(souin): bump the Caddy plugin to v1.7.10 by @earthlingdavey in #416

Full Changelog: v0.6.0...v0.6.1-rc1

v0.6.0

Choose a tag to compare

@github-actions github-actions released this 06 Nov 15:24
Immutable release. Only release title and notes can be modified.
22e0249

What's Changed

hotserve is now motel. Same program, new name everywhere: package, binaries, units, paths, accounts and environment variables. See below for the manual upgrade path from hotserve.

  • docs(backups): docs/ is for the operator — the guide keeps what an operator needs; the release checklist and the design record move beside the package by @earthlingdavey in #399
  • refactor: rename hotserve to motel, with no upgrade path by @earthlingdavey in #400
  • deps: klauspost/compress 1.20.2 (GO-2026-6865) by @earthlingdavey in #402

Full Changelog: v0.5.0...v0.6.0

Moving a box

  # 1. Back up
  d=/root/hotserve-backup-$(date -u +%Y%m%dT%H%M%SZ); mkdir -m 700 $d
  tar -C / -czf $d/hotserve.tgz etc/hotserve etc/hotserve-backup
  var/lib/hotserve/caddy
  cp /etc/hotserve/Caddyfile $d/
  
  # 2. Remove hotserve. Kept: the age identity, secrets, restic credentials,
  #    Caddy certificates, /var/lib/liveswap and both accounts.
  apt purge hotserve
  
  # 3. Rename the accounts in place, so uids and file ownership survive
  usermod -l motel -d /var/lib/motel -c "motel server" hotserve
  groupmod -n motel hotserve 
  usermod -l motel-backup -c made-by-motel hotserve-backup
  groupmod -n motel-backup hotserve-backup
  
  # 4. Move what was kept to its motel name
  mv /etc/hotserve /etc/motel
  mv /etc/hotserve-backup /etc/motel-backup
  mv /var/lib/hotserve /var/lib/motel
  
  # 5. Convert the Caddyfile and put it in place before installing,
  #    so the package keeps it instead of writing its starter.
  #    Check afterwards that each `claim repository` still names your repo.
  sed 's/hotserve/motel/g' $d/Caddyfile > /etc/motel/Caddyfile
  
  # 6. Install and start (Your first deploy, step 2)
  mkdir -p /var/local/motel && cd /var/local/motel
  v=0.6.0; arch=$(dpkg --print-architecture)
  base=https://github.com/smallhoursorg/motel/releases/download/v$v
  curl -fsSLO "$base/motel_${v}_${arch}.deb" && curl -fsSLO "$base/checksums.txt" &&
    sha256sum -c --ignore-missing checksums.txt && apt install
  ./motel_${v}_${arch}.deb 
  systemctl enable --now motel
  
  # 7. Check
  systemctl is-active motel motel-box-apply.path motel-backup.timer
  journalctl -u motel -o cat | grep relaunched      # each app comes back on its own
  motel-backup run && motel-backup status           # the moved credentials work

v0.6.0-rc2

v0.6.0-rc2 Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 06 Nov 14:19
Immutable release. Only release title and notes can be modified.
22e0249

What's Changed

hotserve is now motel. Same program, new name everywhere: package, binaries, units, paths, accounts and environment variables. There is no upgrade path from hotserve.

To move a box: back up /etc/hotserve and /etc/hotserve-backup, then apt purge hotserve, install this release and set the box up again from Your first deploy. The purge keeps the box's age identity, its secrets, the restic credentials and everything under /var/lib/liveswap; move the first three under their motel names and give the Caddyfile its new admin socket, audience and env_file paths. Repositories made from the old templates take the new ones: audience motel, variables MOTEL_URL and MOTEL_VERSION.

  • docs(backups): docs/ is for the operator — the guide keeps what an operator needs; the release checklist and the design record move beside the package by @earthlingdavey in #399
  • refactor: rename hotserve to motel, with no upgrade path by @earthlingdavey in #400
  • deps: klauspost/compress 1.20.2 (GO-2026-6865) by @earthlingdavey in #402

Full Changelog: v0.5.0...v0.6.0-rc2

v0.5.0

Choose a tag to compare

@github-actions github-actions released this 02 Nov 22:14
Immutable release. Only release title and notes can be modified.
68745f6

hotserve 0.5.0 adds off-box backups. hotserve-backup copies what each app declares in a backup block of the Caddyfile into one restic repository off the box, every hour. That covers its SQLite databases, copied safely while the app runs, and its files under shared/. Each week it proves a restore of every app. Backups sets it up. This release is the code that passed as v0.5.0-rc2 on a real box against Backblaze B2, plus #396's Deno bump to the test images and the Deno example.

Upgrading from 0.4.0

Upgrading has the steps. Three things to know:

  • New dependencies: the package now needs restic, sqlite3 and ca-certificates, which apt installs with it.
  • Nothing runs a backup until you set one up. The package makes a system account, hotserve-backup, and two timers, one hourly and one weekly. The timers start nothing until hotserve-backup setup has written the repository's credential file.
  • A box whose apps declare no backup serves as before.

From v0.3.x, read v0.4.0's notes first: each app wants a redeploy after that upgrade.

New: off-box backups

  • Declare what to keep in an app's block of the Caddyfile, with paths under the app's shared/:

    backup {
        sqlite app.db
        files  uploads
    }
    

    hotserve validates the block. With a box repository, the box also runs hotserve-backup validate on each push before applying it.

  • hotserve-backup setup <url>, as root, takes an s3: URL and the storage key. It makes the repository, or finds the one already there, as on a rebuilt box. A new repository's password is shown once, to store off the box.

  • After that:

    • a run every hour;
    • a drill every week, which fetches each app's newest snapshot whole and checks a 52nd of the repository's data;
    • hotserve-backup status, which any account may run;
    • hotserve-backup restore <app>, which puts an app back.
  • The bucket is the protection. It is versioned, and neither of its two keys, the box's and your own machine's, can destroy a version. Nothing on the box forgets or prunes: retention runs from your own machine. So a deletion or overwrite made with either key can be undone for 180 days, if someone looks from off the box at least once every 90. A stolen login to the provider account can still destroy everything: give it a second factor.

  • Run on Backblaze B2, through its S3-compatible endpoint, with all twelve release-candidate checks. An attack with the box's own key destroyed nothing. The backups were recovered into a new bucket without the account's key, and restored onto a rebuilt box under new account ids. restic's own b2: backend is refused, and setup names the S3 URL to use instead.

Behaviour changes

  • Once any app declares a backup, hotserve validate and systemctl reload hotserve refuse a Caddyfile that a backup run could not read in full: for example, a declaration in a file imported from outside /etc/hotserve. A refused reload keeps the running config. hotserve run serves such a file, and says why.
  • apt purge hotserve keeps /etc/hotserve-backup/repository.env, the repository's password and key, and says so. It is the only way to read the backups already made.
  • A hotserve-backup account that hotserve did not make is refused by setup and by every run, until it is put right.

Not verified yet

  • AWS S3 and Hetzner: the guide covers them, but nothing has run there.
  • On B2, two readings are still to come: whether its lifecycle rule removes a marker hidden through the S3 endpoint, and one drill's egress on a bill.
  • On a rebuilt box (#389): a storage slow to answer setup's first look gets a new password shown before it is asked for the repository's own. setup says to store the new one beside the password you keep, never in its place.

What's Changed

  • feat(liveswap): an app declares what to back up by @earthlingdavey in #138
  • feat(backups): the backup engine — plan, dump, upload, verify, clean by @earthlingdavey in #139
  • feat(backups): restore, and the restore drill by @earthlingdavey in #140
  • feat(liveswap): a backup path is 400 bytes at most by @earthlingdavey in #141
  • feat(hotserve): validate and reload refuse a backup a backup run would not see by @earthlingdavey in #147
  • feat(backups): status, validate, and a listing of the repository per run by @earthlingdavey in #146
  • feat(backups): hotserve-backup setup, the account, and the credential file's move by @earthlingdavey in #152
  • feat(backups): the units, the timers, packaging, and the docs on a fresh box by @earthlingdavey in #153
  • fix(backups): the package's lifecycle, the account's rule, and an upgrade that leaves a run alone by @earthlingdavey in #155
  • test(backups): e2e-backup from ~13 min to about 4 on CI by @earthlingdavey in #159
  • feat(backups): the repository check, the clean-run record, and D4's backstop (PR 5) by @earthlingdavey in #161
  • test(backups): cut upkeep — pin every third-party behaviour, one print format, the cache key (PR 6) by @earthlingdavey in #162
  • test(backups): the pin race no longer runs out of attempts under load (PR 7, backups half) by @earthlingdavey in #166
  • docs(backups): the release candidate's docs — an operator guide, a bucket no key can destroy, the checks only a real provider can answer (PR 8) by @earthlingdavey in #168
  • docs(backups): keep the design record in the repository while the feature is on ice by @earthlingdavey in #179
  • Merge main into backup: the box channel, v0.3.x, and the review fixes by @earthlingdavey in #321
  • docs(backups): two accepted residuals from #321, as an independent trace found them by @earthlingdavey in #326
  • fix(backups): status's idle line says nothing running now (#174) by @earthlingdavey in #342
  • docs(backups): say what restic is, once at each door by @earthlingdavey in #343
  • test(smoke): stage 5 tells a timer's scheduled run from a catch-up (#327) by @earthlingdavey in #341
  • fix(backups): status's idle line is true between two units of a run by @earthlingdavey in #349
  • docs(backups): the guide's intro no longer says the drill restores by @earthlingdavey in #344
  • fix(backups): the drill's fetch names no cause for restic's exit 1, which it gives them all (#170) by @earthlingdavey in #346
  • fix(backups): setup says what the storage answered, and what became of the password, from restic's own words (#175) by @earthlingdavey in #353
  • build(package): restic, sqlite3 and ca-certificates are Depends — an image that installs no recommended package gets them too (D8, #347) by @earthlingdavey in #351
  • fix(backups): setup holds a key to writing before it takes it — a read-only key is refused at setup, not a quarter of an hour into every run (RC check 6, #347) by @earthlingdavey in #352
  • docs(backups): what v0.3.0-rc1 on real B2 found, in the guide's own wording (#347) by @earthlingdavey in #356
  • feat(backups): Backblaze B2 through restic's S3 backend; restic's own b2: refused (#354) by @earthlingdavey in #357
  • fix(backups): root's lines name root, not sudo — the guide, hotserve-backup's words, the smoke as root (#323) by @earthlingdavey in #364
  • docs(backups): escape the pipes that split two residuals rows by @earthlingdavey in #367
  • fix(backups): one try a request on S3 for the upload, fetch and check (#177) by @earthlingdavey in #368
  • docs(backups): #177 closed — what is left of it is accepted, not owed by @earthlingdavey in #369
  • Merge main into backup: resource caps, the sibling-IPC and restart fixes, the CA floor by @earthlingdavey in #371
  • Record main in backup's history, after #371's squash by @earthlingdavey in #377
  • build(deps): bump the go-deps group across 2 directories with 2 updates by @dependabot[bot] in #385
  • Merge main (v0.4.0) into backup by @earthlingdavey in #387
  • docs(hardening): modprobe's dry run can print its dependencies firs...
Read more

v0.5.0-rc2

v0.5.0-rc2 Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 02 Nov 21:20
Immutable release. Only release title and notes can be modified.
70568f4

What's Changed

  • build(deps): bump the go-deps group across 2 directories with 2 updates by @dependabot[bot] in #385
  • docs(hardening): modprobe's dry run can print its dependencies first by @earthlingdavey in #386
  • fix(backups): setup names a bucket not there, and a password shown on an unsure look is stored beside the kept one (RC checks 1 and 7) by @earthlingdavey in #388
  • docs(backups): what v0.5.0-rc1 found on B2's S3 endpoint — the twelve checks' answers, and the checks' own corrections by @earthlingdavey in #390
  • docs(threat-model): backups — the asset, its principals, what each attacker gains, the residuals (#172) by @earthlingdavey in #391
  • Merge main (#385, #386) into backup by @earthlingdavey in #392
  • fix(backups): the plan's trailing-input check reads to the end, and s… by @earthlingdavey in #397

Full Changelog: v0.5.0-rc1...v0.5.0-rc2

v0.5.0-rc1

v0.5.0-rc1 Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 01 Nov 17:02
Immutable release. Only release title and notes can be modified.
1775be2

What's Changed

  • feat(liveswap): an app declares what to back up by @earthlingdavey in #138
  • feat(backups): the backup engine — plan, dump, upload, verify, clean by @earthlingdavey in #139
  • feat(backups): restore, and the restore drill by @earthlingdavey in #140
  • feat(liveswap): a backup path is 400 bytes at most by @earthlingdavey in #141
  • feat(hotserve): validate and reload refuse a backup a backup run would not see by @earthlingdavey in #147
  • feat(backups): status, validate, and a listing of the repository per run by @earthlingdavey in #146
  • feat(backups): hotserve-backup setup, the account, and the credential file's move by @earthlingdavey in #152
  • feat(backups): the units, the timers, packaging, and the docs on a fresh box by @earthlingdavey in #153
  • fix(backups): the package's lifecycle, the account's rule, and an upgrade that leaves a run alone by @earthlingdavey in #155
  • test(backups): e2e-backup from ~13 min to about 4 on CI by @earthlingdavey in #159
  • feat(backups): the repository check, the clean-run record, and D4's backstop (PR 5) by @earthlingdavey in #161
  • test(backups): cut upkeep — pin every third-party behaviour, one print format, the cache key (PR 6) by @earthlingdavey in #162
  • test(backups): the pin race no longer runs out of attempts under load (PR 7, backups half) by @earthlingdavey in #166
  • docs(backups): the release candidate's docs — an operator guide, a bucket no key can destroy, the checks only a real provider can answer (PR 8) by @earthlingdavey in #168
  • docs(backups): keep the design record in the repository while the feature is on ice by @earthlingdavey in #179
  • docs: secrets are released in v0.3.1, and the upgrade example names it by @earthlingdavey in #247
  • fix(examples): make check's too-old refusal names no version (#234) by @earthlingdavey in #241
  • docs(triage): flag one-way and wide fixes for the person who merges by @earthlingdavey in #248
  • docs(box): bin/apply names all three step-6 children; an env_file the box does not hold is an accepted residual by @earthlingdavey in #243
  • build(make): secretscan scans the working tree in a git worktree, and the fixture allowlist matches both path forms (#234) by @earthlingdavey in #253
  • fix(build): hotserve version names hotserve's release ahead of Caddy's by @earthlingdavey in #250
  • fix(examples): box scripts read non-ASCII paths, and check names a silent failure by @earthlingdavey in #252
  • fix(liveswap): refuse env and claim keys that Caddy strips as "@id" (#242) by @earthlingdavey in #254
  • docs(upgrading): the upgrade reads the latest release, and a failed check stops before apt by @earthlingdavey in #249
  • fix(packaging): a postrm — purge removes the box's config, the starter's record and the applier's exchange tree by @earthlingdavey in #255
  • feat(liveswap): a deploy or rollback to the running version relaunches it, same artifact only (#234) by @earthlingdavey in #256
  • fix(box): step 6 refuses a push whose server would refuse the workflow's connection by @earthlingdavey in #258
  • feat(examples): a release per box — /HOTSERVE_VERSION picks the hotserve make check checks it with (#223) by @earthlingdavey in #257
  • docs: the config is a Caddyfile, and a JSON config is unsupported (#251) by @earthlingdavey in #259
  • fix(liveswap): app units run with LimitCORE=0, so a crash writes no dump of the app's memory (#234) by @earthlingdavey in #260
  • fix(box): step 6 refuses a push that moves where the box's server listens (#223) by @earthlingdavey in #262
  • fix(box): step 6 refuses request limits and a request_body a push cannot work with (#223) by @earthlingdavey in #263
  • feat(usage): the public ledger of what building hotserve costs (make usage) by @earthlingdavey in #261
  • build(deps): bump the actions group across 1 directory with 2 updates by @dependabot[bot] in #265
  • build(deps): bump the go-deps group across 3 directories with 3 updates by @dependabot[bot] in #266
  • build(deps): bump python from 3.13-slim to 3.14-slim in the compose-images group by @dependabot[bot] in #264
  • fix(lint): readOwnFile tells a link from a loop with Readlink, so gosec's G703 no longer flakes the liveswap lint lane by @earthlingdavey in #269
  • fix(box): the upload's bound is the handler's own on the body, not the connection's deadline alone (#223) by @earthlingdavey in #268
  • fix(box): step 6 refuses a timeouts directive whose read settings would cut a push's upload (#223) by @earthlingdavey in #267
  • docs(box): the secrets caps are confirmed, no longer proposed by @earthlingdavey in #270
  • docs(hardening): step 4, Debian's updates on their own, with no automatic reboot (#223) by @earthlingdavey in #280
  • docs(box): no migration steps for box repositories from before v0.3.1; the tutorial links to the secrets commands (#272) by @earthlingdavey in #281
  • docs(liveswap): accept a host that refuses the sandbox failing at step 3, not step 2 (#274) by @earthlingdavey in #282
  • docs(box): init's lines refuse a placeholder left in the commit (#273) by @earthlingdavey in #283
  • feat(examples): deploy.sh names HOTSERVE_RUNS_ON when the box refuses a release's architecture (#277) by @earthlingdavey in #285
  • feat(examples): the apply job's summary says what an applied push did (#272) by @earthlingdavey in #286
  • docs(liveswap): a Node app that listens on PORT runs unchanged with env PORT {socket} (#279) by @earthlingdavey in #287
  • feat(examples): make signer speaks of a running box only when it added you to one (#279) by @earthlingdavey in #288
  • refactor(box): no handling for releases and templates from before v0.3.1 (#272) by @earthlingdavey in #290
  • docs(first-deploy): say why commits are signed before step 1 (#279) by @earthlingdavey in #284
  • refactor(packaging): no handling for packages from before v0.3.1 (#272) by @earthlingdavey in #291
  • fix(liveswap): a status that shows a restarted instance shows its restart; box test stubs run under the box's deadline (#289) by @earthlingdavey in #292
  • fix(liveswap): a unit's command execs byte for byte, with no $VAR substitution (#295) by @earthlingdavey in #301
  • docs: the remaining doc-vs-code rows from the 2026-09-25 review (#294) by @earthlingdavey in #302
  • fix(liveswap): explicit 0 in the Caddyfile, pre_start outlives the request (#295) by @earthlingdavey in #304
  • fix(deploytrust): bound the JWKS refetch and the local token's lifetime (#296) by @earthlingdavey in #303
  • build(deps): bump the go-deps group across 2 directories with 1 update by @dependabot[bot] in #322
  • fix(liveswap): a prune or a deploy never goes through a link under the root (#300) by @earthlingdavey in #293
  • docs(liveswap): a bind mount's source must be the app's own (#308) by @earthlingdavey in #309
  • fix(liveswap): pre-flight on every relaunch, Destruct waits for a deploy, the watchdog waits for a deploy's end (#297) by @earthlingdavey in #314
  • Merge main into backup: the box channel, v0.3.x, and the review fixes by @earthlingdavey in #321
  • fix(liveswap): no io_uring ring and no x86-32 ABI in an app unit (#298) by @earthlingdavey in #312
  • docs(backups): two accepted residuals from #321, as an indepe...
Read more

v0.4.0

Choose a tag to compare

@github-actions github-actions released this 01 Nov 15:59
Immutable release. Only release title and notes can be modified.
f6e7cc7

Upgrading from v0.3.x

Upgrading has the steps. What an upgrade can change for a running box:

  • 32-bit executables are refused before launch.
  • io_uring is denied inside an app's sandbox.
  • soak 0 means no soak, where it meant the default of 15 s. keep 0 and watchdog_grace 0, which also meant the default, are now refused.
  • A root path holding a space, colon, backslash, quote or control character is refused when the config loads.
  • An oversized environment or command is refused at pre-flight.
  • An app's command runs byte for byte (#301): a $VAR in it reaches the app as written, no longer substituted.
  • An env or claim key that Caddy strips as @id is refused (#254), where it was silently dropped.
  • Each app has its own IPC and /dev/shm (#355): apps that talked to each other through them no longer can.
  • Redeploy each app after upgrading. The fix for a one-process app losing its grace reaches a running app only at its next start, so its first stop after the upgrade can still kill it at once.

What's Changed

  • docs: secrets are released in v0.3.1, and the upgrade example names it by @earthlingdavey in #247
  • fix(examples): make check's too-old refusal names no version (#234) by @earthlingdavey in #241
  • docs(triage): flag one-way and wide fixes for the person who merges by @earthlingdavey in #248
  • docs(box): bin/apply names all three step-6 children; an env_file the box does not hold is an accepted residual by @earthlingdavey in #243
  • build(make): secretscan scans the working tree in a git worktree, and the fixture allowlist matches both path forms (#234) by @earthlingdavey in #253
  • fix(build): hotserve version names hotserve's release ahead of Caddy's by @earthlingdavey in #250
  • fix(examples): box scripts read non-ASCII paths, and check names a silent failure by @earthlingdavey in #252
  • fix(liveswap): refuse env and claim keys that Caddy strips as "@id" (#242) by @earthlingdavey in #254
  • docs(upgrading): the upgrade reads the latest release, and a failed check stops before apt by @earthlingdavey in #249
  • fix(packaging): a postrm — purge removes the box's config, the starter's record and the applier's exchange tree by @earthlingdavey in #255
  • feat(liveswap): a deploy or rollback to the running version relaunches it, same artifact only (#234) by @earthlingdavey in #256
  • fix(box): step 6 refuses a push whose server would refuse the workflow's connection by @earthlingdavey in #258
  • feat(examples): a release per box — /HOTSERVE_VERSION picks the hotserve make check checks it with (#223) by @earthlingdavey in #257
  • docs: the config is a Caddyfile, and a JSON config is unsupported (#251) by @earthlingdavey in #259
  • fix(liveswap): app units run with LimitCORE=0, so a crash writes no dump of the app's memory (#234) by @earthlingdavey in #260
  • fix(box): step 6 refuses a push that moves where the box's server listens (#223) by @earthlingdavey in #262
  • fix(box): step 6 refuses request limits and a request_body a push cannot work with (#223) by @earthlingdavey in #263
  • feat(usage): the public ledger of what building hotserve costs (make usage) by @earthlingdavey in #261
  • build(deps): bump the actions group across 1 directory with 2 updates by @dependabot[bot] in #265
  • build(deps): bump the go-deps group across 3 directories with 3 updates by @dependabot[bot] in #266
  • build(deps): bump python from 3.13-slim to 3.14-slim in the compose-images group by @dependabot[bot] in #264
  • fix(lint): readOwnFile tells a link from a loop with Readlink, so gosec's G703 no longer flakes the liveswap lint lane by @earthlingdavey in #269
  • fix(box): the upload's bound is the handler's own on the body, not the connection's deadline alone (#223) by @earthlingdavey in #268
  • fix(box): step 6 refuses a timeouts directive whose read settings would cut a push's upload (#223) by @earthlingdavey in #267
  • docs(box): the secrets caps are confirmed, no longer proposed by @earthlingdavey in #270
  • docs(hardening): step 4, Debian's updates on their own, with no automatic reboot (#223) by @earthlingdavey in #280
  • docs(box): no migration steps for box repositories from before v0.3.1; the tutorial links to the secrets commands (#272) by @earthlingdavey in #281
  • docs(liveswap): accept a host that refuses the sandbox failing at step 3, not step 2 (#274) by @earthlingdavey in #282
  • docs(box): init's lines refuse a placeholder left in the commit (#273) by @earthlingdavey in #283
  • feat(examples): deploy.sh names HOTSERVE_RUNS_ON when the box refuses a release's architecture (#277) by @earthlingdavey in #285
  • feat(examples): the apply job's summary says what an applied push did (#272) by @earthlingdavey in #286
  • docs(liveswap): a Node app that listens on PORT runs unchanged with env PORT {socket} (#279) by @earthlingdavey in #287
  • feat(examples): make signer speaks of a running box only when it added you to one (#279) by @earthlingdavey in #288
  • refactor(box): no handling for releases and templates from before v0.3.1 (#272) by @earthlingdavey in #290
  • docs(first-deploy): say why commits are signed before step 1 (#279) by @earthlingdavey in #284
  • refactor(packaging): no handling for packages from before v0.3.1 (#272) by @earthlingdavey in #291
  • fix(liveswap): a status that shows a restarted instance shows its restart; box test stubs run under the box's deadline (#289) by @earthlingdavey in #292
  • fix(liveswap): a unit's command execs byte for byte, with no $VAR substitution (#295) by @earthlingdavey in #301
  • docs: the remaining doc-vs-code rows from the 2026-09-25 review (#294) by @earthlingdavey in #302
  • fix(liveswap): explicit 0 in the Caddyfile, pre_start outlives the request (#295) by @earthlingdavey in #304
  • fix(deploytrust): bound the JWKS refetch and the local token's lifetime (#296) by @earthlingdavey in #303
  • build(deps): bump the go-deps group across 2 directories with 1 update by @dependabot[bot] in #322
  • fix(liveswap): a prune or a deploy never goes through a link under the root (#300) by @earthlingdavey in #293
  • docs(liveswap): a bind mount's source must be the app's own (#308) by @earthlingdavey in #309
  • fix(liveswap): pre-flight on every relaunch, Destruct waits for a deploy, the watchdog waits for a deploy's end (#297) by @earthlingdavey in #314
  • fix(liveswap): no io_uring ring and no x86-32 ABI in an app unit (#298) by @earthlingdavey in #312
  • fix(liveswap): releases ordered by deploy time; no prune through a linked app dir (#299) by @earthlingdavey in #310
  • fix(box): no io_uring ring and no x86-32 ABI in the applier unit (#313) by @earthlingdavey in #329
  • fix(box): step 6's validate refusal keeps the reason, not the head of Caddy's module chain (#324) by @earthlingdavey in #330
  • docs: the app-dir claim, the liveswap file tables, the triage skill's "newest five" (#320) by @earthlingdavey in #328
  • docs(liveswap): the no-link rules give today's reason, not a pre-sandbox upgrade's by @earthlingdavey in #331
  • fix(box): GET / answers 409 when the box runs a file the walk refuses (#325) by @earthlingdavey in #332
  • fix(box): GET / names the no-signer way back, as a push does (#325) by @earthlingdavey in #334
  • fix(liveswap): seccomp proven in force, by the capability probe and in the lanes (#316) by @earthlingdavey in #333
  • docs(liveswap): soak 0 needs a health_path, for good (#305) by @earthlingdavey in https://github.com/sma...
Read more

v0.4.0-rc1

v0.4.0-rc1 Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 01 Nov 09:19
Immutable release. Only release title and notes can be modified.
f6e7cc7

What's Changed

  • docs: secrets are released in v0.3.1, and the upgrade example names it by @earthlingdavey in #247
  • fix(examples): make check's too-old refusal names no version (#234) by @earthlingdavey in #241
  • docs(triage): flag one-way and wide fixes for the person who merges by @earthlingdavey in #248
  • docs(box): bin/apply names all three step-6 children; an env_file the box does not hold is an accepted residual by @earthlingdavey in #243
  • build(make): secretscan scans the working tree in a git worktree, and the fixture allowlist matches both path forms (#234) by @earthlingdavey in #253
  • fix(build): hotserve version names hotserve's release ahead of Caddy's by @earthlingdavey in #250
  • fix(examples): box scripts read non-ASCII paths, and check names a silent failure by @earthlingdavey in #252
  • fix(liveswap): refuse env and claim keys that Caddy strips as "@id" (#242) by @earthlingdavey in #254
  • docs(upgrading): the upgrade reads the latest release, and a failed check stops before apt by @earthlingdavey in #249
  • fix(packaging): a postrm — purge removes the box's config, the starter's record and the applier's exchange tree by @earthlingdavey in #255
  • feat(liveswap): a deploy or rollback to the running version relaunches it, same artifact only (#234) by @earthlingdavey in #256
  • fix(box): step 6 refuses a push whose server would refuse the workflow's connection by @earthlingdavey in #258
  • feat(examples): a release per box — /HOTSERVE_VERSION picks the hotserve make check checks it with (#223) by @earthlingdavey in #257
  • docs: the config is a Caddyfile, and a JSON config is unsupported (#251) by @earthlingdavey in #259
  • fix(liveswap): app units run with LimitCORE=0, so a crash writes no dump of the app's memory (#234) by @earthlingdavey in #260
  • fix(box): step 6 refuses a push that moves where the box's server listens (#223) by @earthlingdavey in #262
  • fix(box): step 6 refuses request limits and a request_body a push cannot work with (#223) by @earthlingdavey in #263
  • feat(usage): the public ledger of what building hotserve costs (make usage) by @earthlingdavey in #261
  • build(deps): bump the actions group across 1 directory with 2 updates by @dependabot[bot] in #265
  • build(deps): bump the go-deps group across 3 directories with 3 updates by @dependabot[bot] in #266
  • build(deps): bump python from 3.13-slim to 3.14-slim in the compose-images group by @dependabot[bot] in #264
  • fix(lint): readOwnFile tells a link from a loop with Readlink, so gosec's G703 no longer flakes the liveswap lint lane by @earthlingdavey in #269
  • fix(box): the upload's bound is the handler's own on the body, not the connection's deadline alone (#223) by @earthlingdavey in #268
  • fix(box): step 6 refuses a timeouts directive whose read settings would cut a push's upload (#223) by @earthlingdavey in #267
  • docs(box): the secrets caps are confirmed, no longer proposed by @earthlingdavey in #270
  • docs(hardening): step 4, Debian's updates on their own, with no automatic reboot (#223) by @earthlingdavey in #280
  • docs(box): no migration steps for box repositories from before v0.3.1; the tutorial links to the secrets commands (#272) by @earthlingdavey in #281
  • docs(liveswap): accept a host that refuses the sandbox failing at step 3, not step 2 (#274) by @earthlingdavey in #282
  • docs(box): init's lines refuse a placeholder left in the commit (#273) by @earthlingdavey in #283
  • feat(examples): deploy.sh names HOTSERVE_RUNS_ON when the box refuses a release's architecture (#277) by @earthlingdavey in #285
  • feat(examples): the apply job's summary says what an applied push did (#272) by @earthlingdavey in #286
  • docs(liveswap): a Node app that listens on PORT runs unchanged with env PORT {socket} (#279) by @earthlingdavey in #287
  • feat(examples): make signer speaks of a running box only when it added you to one (#279) by @earthlingdavey in #288
  • refactor(box): no handling for releases and templates from before v0.3.1 (#272) by @earthlingdavey in #290
  • docs(first-deploy): say why commits are signed before step 1 (#279) by @earthlingdavey in #284
  • refactor(packaging): no handling for packages from before v0.3.1 (#272) by @earthlingdavey in #291
  • fix(liveswap): a status that shows a restarted instance shows its restart; box test stubs run under the box's deadline (#289) by @earthlingdavey in #292
  • fix(liveswap): a unit's command execs byte for byte, with no $VAR substitution (#295) by @earthlingdavey in #301
  • docs: the remaining doc-vs-code rows from the 2026-09-25 review (#294) by @earthlingdavey in #302
  • fix(liveswap): explicit 0 in the Caddyfile, pre_start outlives the request (#295) by @earthlingdavey in #304
  • fix(deploytrust): bound the JWKS refetch and the local token's lifetime (#296) by @earthlingdavey in #303
  • build(deps): bump the go-deps group across 2 directories with 1 update by @dependabot[bot] in #322
  • fix(liveswap): a prune or a deploy never goes through a link under the root (#300) by @earthlingdavey in #293
  • docs(liveswap): a bind mount's source must be the app's own (#308) by @earthlingdavey in #309
  • fix(liveswap): pre-flight on every relaunch, Destruct waits for a deploy, the watchdog waits for a deploy's end (#297) by @earthlingdavey in #314
  • fix(liveswap): no io_uring ring and no x86-32 ABI in an app unit (#298) by @earthlingdavey in #312
  • fix(liveswap): releases ordered by deploy time; no prune through a linked app dir (#299) by @earthlingdavey in #310
  • fix(box): no io_uring ring and no x86-32 ABI in the applier unit (#313) by @earthlingdavey in #329
  • fix(box): step 6's validate refusal keeps the reason, not the head of Caddy's module chain (#324) by @earthlingdavey in #330
  • docs: the app-dir claim, the liveswap file tables, the triage skill's "newest five" (#320) by @earthlingdavey in #328
  • docs(liveswap): the no-link rules give today's reason, not a pre-sandbox upgrade's by @earthlingdavey in #331
  • fix(box): GET / answers 409 when the box runs a file the walk refuses (#325) by @earthlingdavey in #332
  • fix(box): GET / names the no-signer way back, as a push does (#325) by @earthlingdavey in #334
  • fix(liveswap): seccomp proven in force, by the capability probe and in the lanes (#316) by @earthlingdavey in #333
  • docs(liveswap): soak 0 needs a health_path, for good (#305) by @earthlingdavey in #335
  • fix(liveswap): a hotserve restart mid-deploy leaves the migration to finish and the version deployable (#317, #318) by @earthlingdavey in #336
  • fix(deploytrust): no token waits on an issuer; an 8 KiB token cap (#306) by @earthlingdavey in #337
  • ci(usage): every job logs its runner's specs, region and CPU time by @earthlingdavey in #338
  • docs: deploying from outside GitHub — any OIDC issuer, GitLab, or a key (#221) by @earthlingdavey in #339
  • docs(hardening): kernel modules no app needs, as the operator's step (#315) by @earthlingdavey in #340
  • feat(liveswap): per-app resource caps — memory_max, tasks_max, cpu_quota (#71) by @earthlingdavey in #345
  • docs: questions, bugs and ideas start in Discussions by @earthlingdavey in #348
  • fix(liveswap): sibling IPC closed — PrivateIP...
Read more

v0.3.1

Choose a tag to compare

@github-actions github-actions released this 15 Oct 20:46
ae6a260

Secrets are commits. An app's secrets live in the box's repository, one age-encrypted file per variable. They reach the box over the same signed channel as the Caddyfile, and the box decrypts them into /etc/hotserve/secrets/<app>.env. GitHub and CI only ever hold ciphertext. The reference is docs/secrets.md. This release is the commit that passed as v0.3.1-rc2, after an acceptance run of rc1 and an upgrade from rc1 to rc2 on a real box.

Upgrading:

  • On the box, follow docs/upgrading.md. The upgrade no longer asks about /etc/hotserve/Caddyfile (below).
  • A box repository made from v0.3.0's template copies in the new files (docs/secrets.md, "From an earlier template").
  • If a box repository's HOTSERVE_VERSION variable pinned a candidate, delete it, so its checks use this release.

Behaviour changes since v0.3.0

Secrets are commits. An app's secrets can now live in the box's repository: one age-encrypted file per variable under <host>/secrets/<app>/. The box decrypts them into /etc/hotserve/secrets/<app>.env when it applies a push, and the app reads them at its next launch.

Getting it:

  • Update your box repository from the template: Makefile, bin/secrets, bin/signer, bin/apply, bin/check, .gitattributes. The commented env_file line in its Caddyfile names the old path: add the line make secret prints instead (docs/secrets.md, "From an earlier template").
  • On the laptop, brew install age (or apt install age).
  • Read docs/secrets.md.
  • The package makes the box's key when it is installed or upgraded on a running system; hotserve box key, as root, prints it.

Env files you made by hand keep working unchanged. To move one into the repository:

  1. ssh root@box cat /etc/hotserve/<app>.env | make secrets-import APP=<app> BOX_KEY=$(ssh root@box hotserve box key)
  2. Set env_file /etc/hotserve/secrets/<app>.env in the app's block, and push.
  3. Once that run is green, remove the old file.

<host>/secrets is reserved. Beside a box's Caddyfile, secrets is now the box's own: a repository that already has its own secrets entry there is refused until it is moved.

The template's sudoers file is gone. An administrator needs only the adm group. Once your secrets are in the repository, remove the old grant as root: rm /etc/sudoers.d/hotserve-admin && groupdel hotserve-admin.

Upgrades no longer ask about /etc/hotserve/Caddyfile. The starter ships at /usr/share/hotserve/Caddyfile. The package installs it at /etc/hotserve/Caddyfile only when nothing is there, and replaces it only while that file is byte-identical to the starter the package last wrote. Any other file is never touched.

A box still on a v0.3.x starter keeps it. To bring it up to date and have later upgrades follow the starter, run cp /usr/share/hotserve/Caddyfile /etc/hotserve/ and then dpkg-reconfigure hotserve, which records it. The cp alone leaves the next upgrade with a changed starter nothing to match. (Corrected after merge: the original sentence left out dpkg-reconfigure; Copilot on #240.)

apt purge hotserve no longer removes /etc/hotserve/Caddyfile on a box installed from this release on. On a box upgraded from v0.3.x, it still does.

A refused deploy token is always answered 401. Both deploy webhooks, an app's and the box's, answer every refused token with the same flat 401, however often the address has failed. There is no 429 and no Retry-After from them any more. The per-address and process-wide budgets still bound what the journal records (ten and a hundred failures logged a minute). The box template's bin/apply reports a 429, which can now only come from something in front of the box, as red with its status and body. Anything scripted against the 429 needs changing.

The .deb now requires systemd 257 or newer. apt refuses to install it on Debian 12 and earlier or Ubuntu 24.04, naming systemd (>= 257). Those hosts could never run an app: every app's sandbox needs PrivatePIDs=, which systemd 257 added. Before this, they installed and failed only when the first config that defines an app was loaded (#225).

What's Changed

  • fix(examples): a pin of just "v" is refused, and the tutorial installs the latest release by @earthlingdavey in #224
  • docs(box): what the rc2 walkthrough found: the hidden .github/, GitHub's badge, the 503 quoted, hardening on its own page by @earthlingdavey in #226
  • fix(packaging): the .deb needs systemd 257, and step 2 says what it checks by @earthlingdavey in #225
  • docs(box): secrets as commits — the design (box PR 7a) by @earthlingdavey in #227
  • feat(box): secrets' pure cores — the env-file renderer, the secrets proof, the box key (box PR 7b) by @earthlingdavey in #229
  • feat(box): root installs secrets — the set, the key, recovery (box PR 7c) by @earthlingdavey in #231
  • feat(examples): secrets as commits — make secret, secrets, rekey (box PR 7e1) by @earthlingdavey in #228
  • feat(examples): secrets ride the workflow — bundle, recipient check, e2e; the sudoers file goes (box PR 7e2) by @earthlingdavey in #233
  • docs: secrets as commits — the tutorial step and docs/secrets.md (box PR 7f) by @earthlingdavey in #230
  • fix(examples): bin/apply names a symlinked box file; two comments say what is true (#223) by @earthlingdavey in #235
  • feat(deploytrust): a refused token is always the flat 401; the throttle only samples the journal by @earthlingdavey in #236
  • docs(readme): box joins the README — built-in table, install, security, dependencies by @earthlingdavey in #238
  • fix(packaging): upgrades never ask about /etc/hotserve/Caddyfile (#237) by @earthlingdavey in #239
  • docs: one triage rule for the whole repository, as a skill, scoring the fix as well by @earthlingdavey in #240
  • docs(triage): score only failures; operator input is occurrence 5; score backlog items before building by @earthlingdavey in #246

Full Changelog: v0.3.0...v0.3.1