Upgrading
An Upgrade is one command for each way of running an Instance, and none of them stops a running
one. The new version migrates the database while the old one keeps serving, and the old one is
replaced only afterwards, so a failed or interrupted Upgrade leaves your Instance running on a
schema it can use. Customer sites keep sending events across the swap: a restart of a single host
takes seconds, and the tracker and /collect cover it (see
Retrying from a server client).
Migrations are embedded in the binary and forward-only: there is no down migration, so the way back is a restore, which is why every Upgrade starts with a backup.
Before you start
Section titled “Before you start”-
Take a database dump and confirm it finished. Keep
SPHERICON_ENCRYPTION_KEYandSPHERICON_JWT_SECRETunchanged; see Backup and restore.Terminal window pg_dump --format=custom --file="sphericon-pre-upgrade.dump" \"postgres://backup_user@db.example.com:5432/sphericon?sslmode=require" -
Note the version you run, so you can go back.
sphericon versionprints one line on stdout,sphericon v0.4.2 (commit abc1234, built 2026-10-18T10:00:00Z), andsphericon version --jsonprints the same as an object, for a script or monitoring. For the image usedocker run --rm ghcr.io/getsphericon/sphericon:<tag> version. -
Ask whether a newer release exists (
upgrade --check). -
Read the Upgrade notes of every release between yours and the target.
sphericon upgradeprints them for you; with Helm or Docker read the releases page.
Majors
Section titled “Majors”Releases between two Majors only add: a new table, a nullable column, an index. A removal or a
rename waits for a Major. Until 1.0 every 0.MINOR is a Major (v0.4.1 to v0.4.7 stays inside
one, v0.4.x to v0.5.0 crosses one). You may skip any releases inside a Major. Across Majors an
Upgrade goes through the last release of each Major it crosses: that release is the oldest one
that still runs on the next Major’s schema.
The database records its Major and that oldest release (the floor), and a binary refuses to start only when it cannot run on them; see When it refuses.
Bare binary: sphericon upgrade
Section titled “Bare binary: sphericon upgrade”One command downloads the release, verifies it, migrates, swaps the binary and, with --restart,
restarts the service:
sudo /usr/local/bin/sphericon upgrade --restartIn order, it:
- downloads
sphericon_<version>_<os>_<arch>.tar.gzandchecksums.txtfrom the GitHub Release for this OS and architecture and checks the archive’s sha256 againstchecksums.txt, so a damaged download is never installed (HTTP_PROXY,HTTPS_PROXYandNO_PROXYare honoured); - unpacks the new binary next to the installed one and prints the Upgrade notes
of every release between the two versions. If there are any it stops, having changed nothing,
until you rerun with
--yes; - runs the new binary’s
migratewhile the old one keeps serving. A failed migration stops here with nothing swapped; - swaps the file atomically and keeps the previous binary beside it as
sphericon.old; - restarts the service (
--restart), or prints the command that is left to you.
| Flag | Effect |
|---|---|
--version vX.Y.Z |
Install this release instead of the latest stable one. A release older than the running one is refused: the command never moves you backwards. |
--from <dir> |
Use an archive and checksums.txt already in this directory instead of downloading; no network call is made. See Offline. |
--yes |
Go past the Upgrade notes. Without it a release that has notes stops after printing them, so a script cannot walk past them. |
--restart |
systemd: migrate in a transient unit, swap, restart the service, and exit non-zero if it is not active afterwards. See With systemd. |
--service <name> |
The systemd unit --restart reads and restarts (default sphericon). |
--check |
Change nothing; only ask whether a newer release exists. Takes no other flag. See Ask first. |
Without --restart the command ends by printing the one step it leaves to you,
systemctl restart sphericon, and its migrate runs in your shell, so the shell needs the
service’s environment (at least DATABASE_URL and SPHERICON_JWT_SECRET, and SPHERICON_ENV=production).
On systemd use --restart and the environment comes from the unit.
The command replaces the file it runs from, so it needs write access to that directory. Only a
release build can use it: a build from source (go run, version dev) is not a release and the
command asks you to install a release binary first.
With systemd: --restart
Section titled “With systemd: --restart”--restart needs no second unit file. It reads the service’s User and EnvironmentFile with
systemctl show, runs the new binary’s migrate as a transient unit with those properties
(systemd-run --wait --collect), swaps the binary, runs systemctl restart <service> and then
checks systemctl is-active. If the service is not active it exits non-zero, naming the previous
binary and journalctl -u <service>. Run it as a user that may use systemctl and write the
binary’s directory, normally with sudo. The unit in Self-hosting is
the one it expects.
Ask first: --check
Section titled “Ask first: --check”sphericon upgrade --checkIt makes one request to GitHub and prints Up to date: <version> is the latest release. or
A newer release is available: <version> (running <current>). Run Sphericon upgrade.. The exit
status is 0 when you are current, 10 when a newer stable release exists and 1 on an error,
so cron or a monitoring check can ask:
sphericon upgrade --check; [ $? -ne 10 ] || echo "a sphericon release is available"Across Majors
Section titled “Across Majors”When the target lies beyond the next Major, upgrade stages the target, asks it which Majors lie
between (migrate --status --json, which reads your database) and installs the last release of
each Major in turn, then the target. Before the first migration or swap it prints the Upgrade
notes of every release it will install (the target’s changelog covers them all) and stops
without --yes; --yes covers the whole walk. Each step is then a full Upgrade (migrate, swap,
restart).
Crossing 2 Majors: installing 0.4.9, 0.5.6, 0.6.0 in turn.You run one command, and it needs nothing beyond the target’s own archive to plan the walk.
Offline: --from <dir>
Section titled “Offline: --from <dir>”On a host without internet access, fetch the files on another machine from the
releases page, copy them over and point
upgrade at the directory:
ls /srv/sphericon-release# checksums.txt sphericon_0.5.0_linux_amd64.tar.gzsudo /usr/local/bin/sphericon upgrade --from /srv/sphericon-release --restartThe directory holds checksums.txt and the archive for this host’s OS and architecture, named
sphericon_<version>_<os>_<arch>.tar.gz; the sha256 is still verified. Without --version the
newest archive in the directory is installed. When the Upgrade crosses a Major the directory needs
the last release of every Major in between as well. upgrade --from checks every archive of the
walk before it starts and, if one is missing, changes nothing and lists the file names it
needs:
this Upgrade needs 1 archive(s) the source does not have (nothing was changed): sphericon_0.4.9_linux_amd64.tar.gzVerify a release
Section titled “Verify a release”upgrade checks the archive’s sha256 against checksums.txt. Every release also carries a cosign
signature of checksums.txt and of the image, made keyless by the release workflow
(getsphericon/sphericon), so you can check who built them. Download checksums.txt and
checksums.txt.sigstore.json from the release, then:
cosign verify-blob checksums.txt \ --bundle checksums.txt.sigstore.json \ --certificate-identity-regexp '^https://github\.com/getsphericon/sphericon/\.github/workflows/release-please\.yml@' \ --certificate-oidc-issuer https://token.actions.githubusercontent.comsha256sum --check --ignore-missing checksums.txt # macOS: shasum -a 256 --check --ignore-missing
cosign verify ghcr.io/getsphericon/sphericon:0.5.0 \ --certificate-identity-regexp '^https://github\.com/getsphericon/sphericon/\.github/workflows/release-please\.yml@' \ --certificate-oidc-issuer https://token.actions.githubusercontent.comEach archive has an SPDX SBOM on the release (sphericon_<version>_<os>_<arch>.tar.gz.sbom.json),
and the image carries its SBOM as an attestation:
docker buildx imagetools inspect ghcr.io/getsphericon/sphericon:0.5.0 --format '{{ json .SBOM }}'That attestation is a BuildKit one, not covered by the cosign signature.
The chart is published as an OCI artifact at every release, with the chart version equal to the app version, so no clone of the repository is needed and one command is the whole Upgrade:
helm upgrade sphericon oci://ghcr.io/getsphericon/charts/sphericon \ --version 0.5.0 --reuse-valuesThe chart runs sphericon migrate in a pre-upgrade hook Job, and the Deployment keeps
SPHERICON_DB_AUTO_MIGRATE=false. A failed migration therefore fails the release before any pod
is replaced: the old pods keep serving. Once the hook succeeds the Deployment rolls its replicas
behind the readiness probe (GET /readyz) and the disruption budget. To see why a hook failed,
read the logs of the release’s -migrate Job.
--reuse-values keeps your values, including existingSecret (the Secret with DATABASE_URL,
SPHERICON_JWT_SECRET and SPHERICON_ENCRYPTION_KEY). A pinned image.tag is kept too, so either
leave it unset (it follows the chart’s app version) or set it in the same command
(--set image.tag=0.5.0). Across a Major, run the command once for each release a
refusal names, oldest first. Offline, mirror the image and the chart into your own
registry and use that path. sphericon upgrade refuses to run on Kubernetes and names this command.
Docker
Section titled “Docker”sphericon upgrade never replaces a binary inside an image: it refuses and names this command.
Pull the new tag, migrate with it while the current container keeps serving, then recreate:
docker pull ghcr.io/getsphericon/sphericon:0.5.0
docker run --rm \ -e SPHERICON_ENV=production \ -e DATABASE_URL="postgres://app@db.example.com:5432/sphericon?sslmode=require" \ -e SPHERICON_JWT_SECRET="$SPHERICON_JWT_SECRET" \ ghcr.io/getsphericon/sphericon:0.5.0 migrate
# then recreate the containerWith the Compose file, set the new SPHERICON_VERSION in .env, then
docker compose pull and docker compose up -d: its migrate service runs before the app is replaced.
Use an explicit version tag in production, never latest, so a restart does not upgrade you by
accident. With several replicas leave SPHERICON_DB_AUTO_MIGRATE off, so a heavy migration never
runs under an orchestrator’s liveness probe, and roll the replicas one at a time, waiting for
GET /readyz to return 200 on each. Across a Major, migrate and recreate on each release a
refusal names, oldest first. Offline, move the image with docker save and
docker load.
After an upgrade or a change of configuration, sphericon doctor checks the whole Instance.
To look before you act, migrate --status prints the schema’s current Major and release, the
newest the binary knows and the migrations that would run, and changes nothing (./sphericon migrate --status for a binary, or the same docker run ... migrate --status):
schema: 42binary: 45state: Major 1, oldest release that runs: v0.0.0 (this binary: Major 1)pending: 3 ...When it refuses
Section titled “When it refuses”A binary refuses to start, and upgrade stops, only when it cannot proceed safely. A refusal stops
the new binary, never the one that serves. The three guard messages each name the version, the
commit and a link to this page:
- “is older than the database allows … needs vX.Y.Z or newer.” This binary is older than the database’s floor, usually a rollback past a removal. Start the release it names or a newer one. Do not try to migrate down: restore the backup if you must go back.
- “skipped a Major … install vX.Y.Z first, then upgrade again.” The database is two or more
Majors behind this binary. Install the release it names (the last of your database’s Major), start
it, then upgrade again.
sphericon upgrademakes this walk itself; an image tag or a manual install skipped it. - “is one Major ahead of the database …: run
sphericon migrate, or set SPHERICON_DB_AUTO_MIGRATE=true.” A newer binary on a database that has not been migrated. Runsphericon migrate(orsphericon upgrade), or turn onSPHERICON_DB_AUTO_MIGRATEto migrate on start (a start waits for the lock for at mostSPHERICON_DB_MIGRATE_TIMEOUT). - “needs N migration(s) the database has not run: run
sphericon migrate…” A binary of the database’s own Major whose newer, add-only migrations have not run yet, for example a fresh build started beforemigrate. Runsphericon migrate(orsphericon upgrade), or turn onSPHERICON_DB_AUTO_MIGRATE. The binary refuses instead of starting, because its queries name columns the database does not have yet.
A binary of the database’s own Major starts, and so does the last release of the previous Major
(the floor of the next), which is why an interrupted walk leaves the Instance running. A build
with no release number (dev) is never refused as too old.
sphericon upgrade itself stops with:
- “the release has Upgrade notes: read them above and rerun with –yes.” Do what the notes say,
then rerun with
--yes. - “refusing to downgrade.”
--versionnamed a release older than the running one.upgradeonly moves forward; to go back, restore a backup and start the older release. - “this Instance runs in a container” or “runs on Kubernetes”. Use Docker or Helm; the message names the command.
- “this build is … not a release.” Built from source; install a release binary first.
- “checksum mismatch” or “has no entry in checksums.txt”. The archive is damaged or is not the
one
checksums.txtlists. Download it again; nothing was installed. - “migrate with X (nothing was swapped)”. The migration failed and your Instance is exactly as
it was. Fix the cause the error names (database reachable, privileges, disk) and run the command
again:
migratetakes a database lock, so repeating it is safe. - “this Upgrade needs N archive(s) the source does not have.” With
--from, a Major crossing needs more archives than the directory holds. Add the files it lists and run again. <service> is <state> after the restart.The service did not come up on the new binary. Readjournalctl -u <service>; the previous binary is kept assphericon.old.
Roll back
Section titled “Roll back”Migrations only go forward, so a rollback means returning to the pre-upgrade state:
- Stop every replica.
- Restore the pre-upgrade dump into an empty database (see the
restore drill) and point
DATABASE_URLat it, or drop and recreate the database you are restoring over. - Start the previous binary or image. Do not point it at the migrated database: after a Major it refuses, as above.
Anything written after the dump is lost, and the points in After a restore apply. If the Upgrade is only minutes old, weigh whether fixing forward is cheaper than losing that window.
The kept sphericon.old shows what you replaced. It also covers one narrow case: the new release
would not start and the Upgrade stayed inside one Major, where the old binary still runs on the
migrated schema. Anything else is a restore.
Upgrade notes
Section titled “Upgrade notes”Sphericon is versioned with release-please from Conventional Commits. The changes of each release
are on the GitHub releases page. A commit that
breaks something carries an Upgrade: line saying what a Self-hoster must do, and those lines are
where a new required setting, a removed one or a changed default is recorded; there is no
per-version table to keep in step.
The binary carries its own changelog, so the notes of the release you are about to install are
there offline and match it exactly. sphericon upgrade prints them before it changes anything. To
read them on their own, with the new binary:
sphericon upgrade-notes --since v0.4.2 # the Upgrade: entries of every release after v0.4.2It prints nothing when there are none.
PostgreSQL major upgrades
Section titled “PostgreSQL major upgrades”A PostgreSQL major upgrade (for example 16 to 17) is a database administration task, independent of Sphericon releases. Do not combine it with an application upgrade: change one thing at a time, so a failure has one suspect.
- Check that the target major version is supported (see Self-hosting for the supported range).
- Take a fresh dump and rehearse the whole procedure on a scratch copy first.
- Upgrade in place with
pg_upgrade, or dump from the old server and restore into a new server on the new major version (pg_dumpthenpg_restore). Managed services offer their own major-version upgrade. - Stop Sphericon during the switch, repoint
DATABASE_URLif the server changed, start it, and checkGET /readyz. - Run
ANALYZEon the new cluster: planner statistics are not always carried over.
A physical backup or WAL archive from the old major version cannot be restored onto the new
one. Take a fresh base backup right after the upgrade, and keep the pre-upgrade pg_dump until
you are confident in the new cluster.