Upgrading & Downgrading
Crewship is a single binary plus acrewship-sidecar runtime that ships
alongside it. Upgrading swaps the binary; the database migrates itself on the
next start, behind an automatic snapshot. Rolling back a binary is one
step (self-update keeps a .bak), but once migrations have run, recovery
also requires restoring that pre-migration snapshot — see
Rollback / downgrade below.
Upgrade: crewship self-update
One command on macOS and Linux (on Windows, self-update is disabled —
download the newer zip and replace crewship.exe manually, see
Windows install):
self-update picks the right mechanism from how crewship was installed:
The download is checksum-verified before anything is swapped, and the swap is
atomic (write-temp-then-rename), so an interrupted update never leaves a
half-written binary. The previous binary is kept at
<binary>.bak for a
manual rollback. self-update only acts when a newer release actually exists
— otherwise it reports you’re already current and exits.
Every API response also carries an X-Crewship-Server-Version header, and the
CLI compares it against its own version once per process: when a release CLI
talks to a different release server, it prints a one-line stderr hint naming
the remedy — the most common cause of confusing API errors is exactly this
skew. Silence it with CREWSHIP_SKIP_UPDATE_CHECK=1.
self-update verifies provenance end-to-end, no cosign binary needed: it
checks the Sigstore keyless signature on checksums.txt — the certificate
must chain to the embedded production Fulcio CA, carry this repo’s signing
workflow identity (release.yml for stable releases, nightly.yml for
nightly builds — see Nightly builds), and be issued via
GitHub Actions OIDC — before trusting any checksum in it. Only then is the
archive’s SHA-256 compared. A release with missing or invalid signature assets is refused.
Operators mirroring releases without signatures (air-gapped setups, forks
signing under a different identity) can bypass the gate with
CREWSHIP_SKIP_SIGNATURE_VERIFY=1 — this prints a loud warning, because it
means trusting the download origin alone.self-update needs a released binary. A development build (version = dev,
e.g. go run ./cmd/crewship) refuses — there’s nothing to compare against.Nightly builds
If you’re trackingmain and running a nightly-<date>-r<n> build (a dev
slot, or anyone who installed a pre-release binary directly), self-update
and self-update --check recognize the nightly channel and compare against
the most recently published nightly-* pre-release — by date, then by build
number — rather than against the stable release line.
The upgrade itself follows the same verified pipeline as a stable install
(download, signature + checksum verification, atomic swap), with two
nightly-specific differences under the hood:
- Asset discovery. Nightly archives are goreleaser snapshot builds, so
their filenames carry a
0.0.0-snapshot-<commit>version instead of the release tag.self-updatereads the release’s published asset list and picks the archive for your OS/arch and build variant from it — it never guesses the filename from the tag. Stable releases use the same discovery, falling back to the deterministic tag-derived name if the listing is unavailable. - Signature identity. Nightly
checksums.txtfiles are signed by thenightly.ymlworkflow, notrelease.yml, so the Sigstore identity pin is per-channel: a nightly upgrade verifies against thenightly.ymlworkflow identity, and a stable upgrade still accepts onlyrelease.yml— neither channel accepts the other’s signatures.
/api/v1/system/version) cache the nightly answer on disk for 1 hour
(24 hours on the stable channel), so a frequently polled server stays within
GitHub’s unauthenticated API quota. An explicit crewship self-update or
--check always asks GitHub directly.
An installed version that’s neither a release tag nor a recognized nightly
build (a hand-rolled make build with no embedded version, for example) has
nothing to compare against; self-update says so plainly rather than failing
with a parse error.
Migrations run on the next start
self-update swaps the binary only. Schema migrations apply on the next
crewship start, and Crewship takes a pre-migration snapshot first
(<database>.pre-migrate-v<from>-to-v<to>-<timestamp>.bak, next to your DB).
So the upgrade flow is:
Server installs (systemd): self-update --systemd
On a long-running install managed by systemd — a VM or bare-metal server, the
way our own production and dev slots run — a plain self-update swaps the
binary but leaves the old process running until you restart it yourself.
--systemd orchestrates the whole cutover and rolls back automatically if the
new binary is bad:
The flag is
--systemd, not --server: the global -s/--server flag already
means “the crewship server URL”, which self-update doesn’t use.- Prepare — download the new release and verify its checksum before touching the running server, so a bad download never causes downtime.
- Stop the systemd unit (so the binary is swapped without a live process).
- Swap the binary + companions in place (the previous binary is kept as
<binary>.bak). - Start the unit on the new binary — migrations run here, behind the automatic pre-migration snapshot.
- Health-check the server until it returns
200, up to--health-timeout(default 60s).
http://127.0.0.1:<port>/healthz. The port is read
from the running unit’s environment (via systemctl), because sudo scrubs
your own CREWSHIP_PORT — so it works even on a non-default port. Override with
--port or the full --health-url when needed.
If the new binary fails to start or never becomes healthy, --systemd
automatically rolls back: it stops the new server, restores the previous
binary from .bak, starts it again, and health-checks the restored server
too. Because migrations run in step 4, that rollback can leave the old
binary crash-looping on the version-skew guard (it can’t boot against the
newer schema). When that happens --systemd says so explicitly and tells you
to restore the pre-migration snapshot now:
--systemd only applies to a self-installed binary (installer/tarball) that
a systemd unit runs. A Homebrew or .deb/.rpm install manages its own
service lifecycle, so --systemd refuses there and points you at brew upgrade
or apt/dnf (the package’s postinstall restarts a running service for you).Server installs on macOS: manual cutover
self-update --systemd is Linux-only — there is no launchd ServiceManager
yet, so on a long-running macOS server install self-update swaps the
binary but leaves the old process running until you restart it. Drive the
same prepare → stop → swap → start → health-check sequence by hand:
<binary>.bak, and — if migrations already ran in step 4 —
restore the pre-migration snapshot with crewship db restore-snapshot (see
“Rollback / downgrade” below). An orchestrated launchd flow that automates this
stop/swap/start/rollback (the macOS analogue of --systemd) is not yet
available.
Verify the upgrade actually landed
A binary swap that did not take, a service that was never restarted, or a deployment pinned to a branch that no longer exists all look identical from the outside: the server answers, and answers with the old code. Ask it what build it is running.commit line is the one that settles it — compare it against the commit
you expected to deploy. version cannot: every binary built without
-ldflags reports dev, so a tracking deployment looks unchanged forever.
This is also how you tell which side is stale when an API call fails on
an unknown field: a server older than the change, or a CLI older than the
change. The two blocks side by side answer it without guessing. Scriptable:
schema is the highest migration the server binary can apply — a
YYYYMMDDHHMMSS timestamp for anything past the legacy sequential block, so
it also dates the newest schema change that build carries. Compare it against
the local database’s applied version reported by
crewship doctor.
Install channels & their upgrade path
Beyond Homebrew and the installer, Crewship ships native Linux packages and an official container image. Each has its own upgrade command — andself-update
knows to step aside for the ones a package manager owns.
On a package-managed install,
crewship self-update refuses to overwrite the
binary (the package manager owns it) and prints the matching apt/dnf/
docker command instead — so you never end up with a binary your package
database doesn’t know about.
Debian / RPM packages + systemd
The.deb and .rpm install:
crewshipandcrewship-sidecarinto/usr/bin,entrypoint.shinto/usr/libexec/crewship/(autodetected — no config needed),- a systemd unit at
/usr/lib/systemd/system/crewship.service, - a config file at
/etc/crewship/crewship.env(optional overrides; your edits survive upgrades).
crewship auto-generates ENCRYPTION_KEY and
NEXTAUTH_SECRET on first start and persists them under /var/lib/crewship.
crewship user (created by the package)
and stores its database under /var/lib/crewship. On upgrade, the package
manager swaps the binary and the postinstall runs systemctl try-restart — it
restarts the service only if it was already running, so an upgrade applies
the new binary (and its automatic pre-migration snapshot) without ever starting
a service you’d chosen to keep stopped.
Rollback / downgrade
Migrations are forward-only. An older binary will refuse to start against a database a newer binary already migrated — with an error naming both versions — rather than silently corrupt data it doesn’t understand:crewship db restore-snapshot arrived on
2026-07-09, a day after the version-skew guard, so a binary older than that
has neither; run the restore from the newer binary that’s still installed,
before you replace it:
- Stop
crewshipd(a running server holds the database open). - Restore the matching pre-migration snapshot with the current binary:
--yes to skip the prompt in scripts (without it, a run with closed stdin
reads EOF and aborts with aborted (pass --yes to skip confirmation)).
- Reinstall the older binary. Homebrew:
brew install crewship-ai/tap/crewship@<version>(orbrew uninstall+ install the pinned tag). Installer:CREWSHIP_VERSION=vX.Y.Z curl -fsSL …/install.sh | bash. - Start the older binary — it boots against the restored schema.
restore-snapshot checks whether the database file itself is in use and
refuses if it is — twice, once up front and once immediately before the swap,
since the confirmation prompt is unbounded and a supervised crewshipd can
come back while you read it. Stop crewshipd yourself first regardless.
If the probe cannot answer at all — a database too corrupt to open, which is
a state you may well be rolling back from — the restore refuses and names
the error; --force overrides that case only, never a database that is
definitely in use. See crewship db.
It only accepts a genuine *.pre-migrate-*.bak snapshot of your database, and
copies the current database aside to <db>.before-restore-<ts> before the
swap, so the restore is itself reversible.
Skipping step 2 leaves the newer schema in place. A binary from 2026-07-08 or
later rejects that on start, by design — but that rejection is a safety net
under the procedure, not a substitute for it, and an older binary doesn’t have
it. Restore the snapshot every time. See Migrations for
the version-skew guard and snapshot naming.
Related
- Migrations — the forward-only guard + pre-migration snapshots.
- Backup & restore — full backup bundles vs. the automatic migration snapshots.
crewship doctor— reports when a newer release is available.