Upgrades
A self-hosted Piprio deployment is upgraded by moving every service to a new release version with one command. The upgrade is a rolling restart: it takes a backup first, pulls the new container images, applies any database changes automatically, and brings services back in an order that keeps the platform reachable for most of the window. An administrator runs it during a maintenance slot and watches the health check at the end confirm the new version is live.
This page describes how that upgrade behaves, how schema changes are handled, what the versioning policy commits to, and how to back out if something goes wrong.
Upgrade procedure
The whole upgrade is a single script that takes the target version as its argument:
bash scripts/upgrade.sh 1.5.0
Running it with no version argument targets the latest published images.
The script runs these steps in order:
- A pre-flight health check. If the current deployment is already unhealthy, the upgrade stops before touching anything, so a fresh problem is never blamed on the new version.
- A full backup, taken automatically. The backup bundle holds the database, the object store contents on on-premise deployments, and the configuration and secrets. This is the artifact a rollback restores from. See Backup and restore for what a bundle contains.
- A pull of the new container images for the target version.
- The new version is recorded in the deployment's environment file, so a later restart of the same configuration comes back on the new version rather than reverting.
- A rolling restart of the services, described below.
- A post-upgrade health check that confirms every service answered.
The rolling restart is ordered so the database-facing API moves first and the rest follow once migrations have applied cleanly:
- The background job scheduler is stopped first, so no new scheduled work starts while the database is changing.
- The API service is brought up on the new image. Database migrations run as that container starts. The script then polls the API's health endpoint for up to three minutes and will not continue until the API reports healthy. If the API never comes back, the upgrade stops here with a rollback hint rather than proceeding to a broken state.
- The background workers are replaced one at a time, with a short pause between each, so document crawling, AI extraction, and exports keep draining throughout.
- The scheduler is started again on the new image.
- The web frontend is replaced.
- The reverse proxy is reloaded to pick up any change to the services behind it.
Because the API and workers are cut over individually rather than all at once, the platform stays reachable for most of the upgrade. The only hard pause is the few seconds the API container takes to restart and run migrations.
Migration handling
Database schema changes ship as part of the release and apply themselves. There is no separate migration command for an operator to remember. When the API container starts, it runs the migration runner before it begins serving traffic, which is what step 5 above waits on.
# what the API container runs on startup, before serving:
python migrations/run_migrations.py
The runner is idempotent. It keeps a record of which migrations have already run, in a tracking table, and skips anything already applied. Running it twice, restarting the container, or re-running the whole upgrade does not double-apply a change or corrupt the schema. A migration that has been applied once is simply reported as skipped on the next start.
Migrations apply in a fixed order and each runs in its own transaction. If one fails, that migration is rolled back and the runner stops with a clear error, which is why the upgrade's three-minute health wait will catch a bad migration rather than leaving the API half-started. Earlier migrations that already succeeded stay applied. The tracking table records exactly how far the upgrade got.
Breaking-change policy
Piprio's published API lives under a version prefix and is versioned from the first release. Endpoints are sorted into two stability tiers:
- A stable tier, which receives backward-compatible changes only. Removing a field, removing an endpoint, changing a field's type, or making an optional request field required all count as breaking changes, and a breaking change to a stable endpoint comes with an advance deprecation notice before the old behavior is withdrawn.
- A preview tier, for newer endpoints whose shape may still evolve. Changes to preview endpoints are recorded in the changelog rather than held to the deprecation notice.
Adding a new optional field, adding a new endpoint, or fixing behavior to be more correct is never treated as a breaking change and can land in any release. When a genuinely incompatible API redesign is needed, the new shape is published under a new version prefix while the old one keeps running through its deprecation window, so an integration is never broken without warning.
The schema definitions a customer builds inside the product follow the same principle. Adding a required field, removing an option, or changing a field's type is a breaking change that requires an explicit new schema version, and existing labeled data keeps its reference to the version it was created under. Cosmetic edits, such as fixing a label or adding hint text, are allowed on a live version without a version bump.
TODO (gap to flag). The breaking-change policy above covers the HTTP API contract and in-product schema versioning. There is no documented platform-level compatibility policy for self-hosted operators, meaning no stated commitment about changes to the deployment's configuration file shape, environment variable names, or service topology between versions. A buyer planning an upgrade cadence should ask for that commitment in writing until it is formalized in the deployment documentation.
For the list of what actually changed in each release, the doc set keeps a separate changelog. This page does not duplicate per-release notes. The changelog is the place to look for "what changed." That changelog is not yet populated, so until it is, the release version tags themselves are the only record of what shipped when.
Rollback
Migrations are forward-only. There are no reverse or down migrations in the platform, so a rollback is not a schema rollback. Backing out an upgrade means returning to the previous version's images and restoring the database from the backup the upgrade took before it started.
If the upgrade fails its health check, the script prints the command to bring the old version back up. The two-step back-out is:
# 1. Point the deployment at the previous version and restart
sed -i 's/^PIPRIO_VERSION=.*/PIPRIO_VERSION=1.4.0/' .env
docker compose -f docker-compose.onprem.yml up -d
# 2. If a migration already changed the database, restore the
# pre-upgrade backup bundle taken at the start of the upgrade
bash scripts/restore.sh backups/piprio_<pre-upgrade-timestamp>/
The first step is enough on its own when the new version never reached the database, for example when image pulls or the pre-flight check failed. Once a migration has applied a schema change that the older images do not expect, the restore in the second step is required: it drops and recreates the database from the backup bundle, which puts both the schema and the data back to their pre-upgrade state. The restore is deliberately guarded. It refuses to run without a valid bundle and requires the operator to type the deployment's domain to confirm a destructive operation.
Because every upgrade backs up first, there is always a known-good restore point from immediately before the change. The practical limit is that a restore returns the deployment to its pre-upgrade state, so any work done between the upgrade and the rollback is lost. Rolling back quickly, before users resume work on the new version, keeps that loss small.