Skip to main content

Upgrading & Downgrading

Crewship is a single binary plus a crewship-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 tracking main 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-update reads 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.txt files are signed by the nightly.yml workflow, not release.yml, so the Sigstore identity pin is per-channel: a nightly upgrade verifies against the nightly.yml workflow identity, and a stable upgrade still accepts only release.yml — neither channel accepts the other’s signatures.
Passive update checks (the CLI boot banner and the dashboard’s version badge via /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.
The sequence is prepare → stop → swap → start → health-check:
  1. Prepare — download the new release and verify its checksum before touching the running server, so a bad download never causes downtime.
  2. Stop the systemd unit (so the binary is swapped without a live process).
  3. Swap the binary + companions in place (the previous binary is kept as <binary>.bak).
  4. Start the unit on the new binary — migrations run here, behind the automatic pre-migration snapshot.
  5. Health-check the server until it returns 200, up to --health-timeout (default 60s).
The health URL defaults to 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:
If the new server fails to start or never becomes healthy, restore the previous binary from <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.
The 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 — and self-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:
  • crewship and crewship-sidecar into /usr/bin,
  • entrypoint.sh into /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).
The package does not enable or start the service — that’s your call. There are no secrets to set first: crewship auto-generates ENCRYPTION_KEY and NEXTAUTH_SECRET on first start and persists them under /var/lib/crewship.
The service runs as the unprivileged 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:
The guard lives in the binary you roll back to, and it was added on 2026-07-08. Builds older than that have no version-skew check at all: they boot straight over a newer schema, start serving, and report nothing. So does crewship db restore-snapshot, added 2026-07-09 — an older binary may not have the command either.This matters most in exactly the case you’d reach for it: upgrading from a long-stale install and then needing to go back. If the version you are returning to predates 2026-07-08, nothing will stop it from writing into a schema it doesn’t understand. Treat step 2 below as mandatory, not as a fallback the guard will enforce for you.
Do it in this order — restore the snapshot with your CURRENT binary first, then reinstall the older one. 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:
  1. Stop crewshipd (a running server holds the database open).
  2. Restore the matching pre-migration snapshot with the current binary:
The restore prompts for confirmation before touching the database; pass --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)).
  1. Reinstall the older binary. Homebrew: brew install crewship-ai/tap/crewship@<version> (or brew uninstall + install the pinned tag). Installer: CREWSHIP_VERSION=vX.Y.Z curl -fsSL …/install.sh | bash.
  2. 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.
  • 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.