Upgrading
How upgrades work
Bayanat has two upgrade mechanisms, and which one applies depends on the version you are coming from.
| Coming from | Mechanism |
|---|---|
v5.0.0 or later, installed with the bayanat installer | sudo bayanat update does the whole thing |
| Anything earlier | One documented manual upgrade to v5, after which updates are automatic |
| Manual or Docker deployments, any version | Documented steps, no installer involved |
Releases before v5.0.0 do not ship an update command at all, so there is nothing on those hosts to run. The Upgrading to v5 section below covers that one-time step.
Rollbacks are manual
No upgrade path rolls a database back. Alembic migrations are not reversible in practice, so recovering from a bad upgrade means restoring the backup you took before it, not downgrading the schema. The updater takes that backup for you and tells you its name; keep your own as well.
Automatic updates (v5.0.0 and later)
Once you are on v5, upgrading is one command:
sudo bayanat update --check # show current vs latest, change nothing
sudo bayanat update # update to the latest release
sudo bayanat update v5.1.0 # or to a specific tagThe updater downloads the release as a signed tarball and verifies it against a pinned key before installing anything, takes a database snapshot, runs migrations, swaps to the new release and health-checks it. If the health check fails it reverts to the previous release on its own.
See the Auto-Update Runbook for phases, expected downtime, recovery states and snapshot handling, and Release Signing for how verification works.
Upgrading to v5
v5 changes how Bayanat is deployed, not only what it runs. Read this section fully before starting. Depending on your deployment this is a migration with a maintenance window, not a routine pull.
Which path applies to you
| Your current setup | Go to |
|---|---|
v4.x installed with the bayanat installer at /opt/bayanat | Path A |
| v4.x installed manually, in your own directory with your own service units | Path B |
| v4.x on Docker Compose | Path C |
| v3.x, any deployment | Upgrade to v4 first, then return here |
Upgrading straight from v3 to v5 is not supported. The v4 upgrade moves you to Alembic migrations, and v5 builds on that baseline.
Before you start, on every path
Back up the database. Use the custom format; it restores selectively and compresses:
bashpg_dump -Fc <your-database-name> > bayanat-$(date +%Y%m%d).dumpBack up your configuration:
.env,config.json, and your uWSGI and web server configuration.Check current health and fix any failures before upgrading:
bashuv run flask doctorNote the migration you are on, so you know what you are returning to:
bashuv run flask db current
What changes in v5
- Service accounts split. The web application runs as
bayanat-weband the worker asbayanat-celery, bothnologinsystem accounts in thebayanatgroup. Thebayanatuser remains as the deployment and database identity. - The release tree becomes read-only to the services.
/opt/bayanat,releases/andshared/are owned by root. Anything that wrote inside a release directory now writes intoshared/instead, andconfig.jsonmoves toshared/runtime/config.json, pointed at byBAYANAT_CONFIG_FILEin.env. - PostgreSQL local authentication changes to peer authentication with an ident map. The previous permissive rule for the application role is removed; only the deployment user and the two service accounts can connect as it over the local socket.
- Redis requires a password.
requirepassis set inredis.confandREDIS_PASSWORDin.env. - The uWSGI socket moves to
/run/bayanat/bayanat.sock. Update your web server configuration if you manage it yourself. Installs that predate this keep working through a fallback to the in-release socket. - Releases are verified before installation. The installer downloads a signed tarball and checks it against a pinned key instead of cloning over the network.
- Docker: PostgreSQL moves from 15 to 16, which requires dumping and restoring the database volume, and the Redis data volume path changes.
- OCR raw payloads are no longer stored and the text-map overlay is removed.
Path A: installer-managed install
Three steps: put the v5 CLI in place, update the code, then apply the v5 layout. In that order.
A1. Install the v5 CLI
Your current CLI has no update command, so install the v5 one once. Verify the signed release first, and take the script from the tree you verified rather than downloading it separately:
TAG=v5.0.0
cd /tmp
curl -fsSLO "https://github.com/sjacorg/bayanat/releases/download/$TAG/bayanat-$TAG.tar.gz"
curl -fsSLO "https://github.com/sjacorg/bayanat/releases/download/$TAG/bayanat-$TAG.tar.gz.minisig"
minisign -Vm "bayanat-$TAG.tar.gz" -P RWS7XvDVF0InHWTCh/86K8sXGcHU/PmzCl4uH9GUDjNnNzHhcX1BvGqZ
tar -xzf "bayanat-$TAG.tar.gz" "bayanat-$TAG/bayanat"
sudo install -m 0755 -o root -g root "bayanat-$TAG/bayanat" /usr/local/bin/bayanatIf minisign is not installed, sudo apt-get install -y minisign first. A failed verification means the download is not the published release: stop, do not install it.
This is a one-time step. After the first successful update the CLI refreshes itself from the deployed release.
A2. Update the code
sudo bayanat update --check # confirm what you are moving to
sudo bayanat update v5.0.0This takes a database snapshot, fetches and verifies the release, installs dependencies, runs flask db upgrade, relocates config.json into shared/runtime/, swaps the current symlink and restarts the services behind a health check.
Verify:
sudo bayanat status
sudo -u bayanat /opt/bayanat/current/.venv/bin/flask doctorIf the update fails on the way to v5
Because your previous release predates the health endpoint, the updater will not start it again against a database that has already been migrated. It reverts the symlink, leaves the services stopped, and prints the snapshot to restore. That is deliberate: an old release running against a new schema is worse than being down. Recover with sudo bayanat restore <snapshot>, which is also listed by sudo bayanat snapshots.
A3. Apply the v5 layout
Hardening is a separate command and deliberately not part of update: it rewrites PostgreSQL and Redis configuration, the service units and the web server configuration, and a code update must never be able to leave those half-written.
sudo bayanat hardenIt stops the services and confirms they stopped, backs up every file it will touch (recording ownership and mode, unit enablement state, and the exact statements needed to undo the database grant changes), applies the layout, then health-checks the application. If that check fails it puts every file back and restarts the services, leaving you on the working pre-harden configuration. The backup directory is printed either way.
Run it after the update, never before. The hardened layout runs the application as a separate account, and only v5 names the database role in its connection string; on an older release every query would fail. harden checks for this and refuses to start if the active release is too old.
Verify:
sudo bayanat status # Layout: hardenedDo not re-run the installer
bayanat install provisions a new machine. It is not a repair or an upgrade, and it refuses to run over an existing install. Use bayanat update to change version and bayanat harden to apply the layout.
Re-running harden on an already hardened install does nothing. harden --force re-applies it, preserving the existing Redis password and database rule.
Path B: manual install
For installs that do not use /opt/bayanat, with your own directory, service units and web server.
# From your installation directory, as the user that owns it
git fetch --tags
git checkout v5.0.0
uv sync --frozen
uv run flask db upgradeRestart the application and worker as you normally do, then verify with flask doctor and flask db current.
bayanat harden assumes the installer's layout, so it does not apply here. What it does is the reference for doing the equivalent by hand: separate web and worker accounts sharing one group, a root-owned release tree with every app-written path redirected outside it, peer authentication with an ident map admitting both accounts, a Redis password, service sandboxing, and the socket in /run/bayanat/.
Two things to carry over even if you keep a single service account:
- Your
.envneeds explicitPOSTGRES_USER,POSTGRES_DBandPOSTGRES_HOST. LeavePOSTGRES_PASSWORDempty to keep socket authentication. - Any app-written directory still inside the release tree has to move out before you make that tree read-only.
Path C: Docker
The PostgreSQL major version moves from 15 to 16. A PostgreSQL data directory is not compatible across major versions. Pulling the new image without migrating the volume leaves the container failing to start. Dump before you pull.
# 1. With the old stack still running, dump the database
docker compose exec -T postgres sh -c 'pg_dump -U "$POSTGRES_USER" -Fc bayanat' > bayanat-pre-v5.dump
# 2. Keep the v4 worker images. Compose names built images after the project, so
# the v5 build in step 5 rebuilds onto the same tags and the v4 ones become
# unreachable. Without this, rolling back is not possible on Docker.
docker image tag "$(docker compose config --images | grep -- '-celery$')" \
bayanat-rollback-celery
docker image tag "$(docker compose config --images | grep -- '-celery-ocr$')" \
bayanat-rollback-celery-ocr
# 3. Stop the stack
docker compose down
# 4. Remove the old database volume (you have the dump; do not skip step 1)
docker volume rm <project>_postgres_data
# 5. Pull the new code and images
git fetch --tags && git checkout v5.0.0
docker compose pull && docker compose build
# 6. Start PostgreSQL alone and let it initialize an empty cluster
docker compose up -d postgres
docker compose exec postgres pg_isready
# 7. Restore
docker compose exec -T postgres sh -c 'pg_restore -U "$POSTGRES_USER" -d bayanat --no-owner' < bayanat-pre-v5.dump
# 8. Bring up the rest; the entrypoint runs migrations
docker compose up -d
docker compose logs -f bayanatpg_restore prints a few errors during step 7 and they are expected:
pg_restore: error: could not execute query: ERROR: schema "tiger" already exists
pg_restore: error: could not execute query: ERROR: schema "tiger_data" already exists
pg_restore: error: could not execute query: ERROR: schema "topology" already existsThe PostGIS image creates those schemas when it initializes the empty cluster, so the dump cannot create them again. Your data is unaffected. Confirm the restore worked by checking a table you recognise rather than by the absence of errors:
docker compose exec -T postgres sh -c 'psql -U "$POSTGRES_USER" -d bayanat -c "SELECT count(*) FROM bulletin;"'Also note:
- The Redis data volume path changes. Redis holds sessions and queued tasks rather than durable data, so the simplest path is to let the old volume go and start clean. Users will need to log in again.
- Images are pinned by digest and run as non-root.
REDIS_PASSWORDmust be set in your.env.
See Docker Deployment for verification and rollback steps, and the rest of that page for the full guide.
After upgrading, on every path
Confirm the schema:
uv run flask db current
uv run flask check-db-alignment
uv run flask doctorClear historical OCR payloads. v5 no longer stores raw provider output. Old rows keep theirs until purged, and on large installs this reclaims significant space:
uv run flask ocr purge-raw --dry-run
uv run flask ocr purge-rawReview new settings. All are optional and have defaults; see Configuration for the full reference. The ones most likely to matter after this upgrade are SEARCH_TIMEOUT and BACKGROUND_SEARCH_TIME_LIMIT, the login throttles and SESSION_LIFETIME, and BAYANAT_CONFIG_FILE if you deploy releases as read-only trees.
Log in and check the dashboard footer, which shows the running version.
Rolling back
Rolling back is manual on every path, because migrations cannot be reversed. The database and the code have to move together: restore the backup taken before the upgrade and return the code to the tag that backup came from.
Between v5 releases
Supported by the CLI, on an installer-managed install:
sudo bayanat snapshots # find the pre-update snapshot
sudo bayanat restore <snapshot-name> # restores the database
sudo bayanat update <previous-tag> # returns the codeFrom v5 back to v4
Not a CLI operation. bayanat update v4.x cannot do it, and neither can bayanat restore on its own. Three things are in the way, and all three have to be dealt with by hand:
- The CLI cannot fetch a v4 release. Since v5 the updater installs a signed tarball and refuses anything it cannot verify. No 4.x release carries one, so the download fails.
bayanat restorestarts the release that is currently linked. Run it before the code goes back and it brings v5 up against a restored v4 schema. The code has to be in place first, with both services stopped across the whole operation.- The hardened layout locks v4 out of the database. After
bayanat hardenthe units run asbayanat-webandbayanat-celery, while a 4.x release authenticates to PostgreSQL as its own OS user. Those accounts are not thebayanatdatabase role, so v4 cannot connect until the service identities and the PostgreSQL authentication configuration are put back.
What a return to v4 therefore involves, in one stopped maintenance window: reinstall the 4.x release and its service units by hand, undo the hardening from the backup directory that bayanat harden reported (its REVERT-DB.txt carries the exact statements for the database role and grants, which the config restore does not cover), restore the pre-upgrade dump, and only then start the services.
Plan for this before upgrading rather than after. If reverting is a realistic possibility for your deployment, keep the pre-upgrade dump and the previous release directory, and treat the move to v5 as one-way otherwise.
Manual install
Check out the previous tag, run uv sync --frozen, and restore your dump with pg_restore, with the application and workers stopped throughout.
Docker
See Rolling Back on the Docker page. It is not a tag checkout on its own: v5 moves PostgreSQL from 15 to 16, so the volume has to be recreated, and the images the rollback needs must exist on the host already.
Upgrading to v4 (legacy)
Legacy path, kept for 4.x installations
This section is retained because the 4.x series is still supported. If you are upgrading a current installation, use Upgrading to v5 above instead. Do not follow the steps below to reach v5.
Before you start
- Back up your database:
pg_dump -Fc <your-database-name> > bayanat-backup-$(date +%Y%m%d).dump- Run diagnostics from your Bayanat directory to check current health:
uv run flask doctorReview the output. Fix any failures before proceeding.
TIP
Run the commands below from your Bayanat installation directory, as the user that owns the installation. Adapt paths and user context to match your setup.
Upgrade steps
# 1. Get the new code
git fetch --tags
git checkout v4.0.2
# 2. Install updated dependencies
uv sync --frozen
# 3. Run database migrations
uv run flask db upgrade
# 4. Restart your application and worker processesHow you restart depends on your setup:
- systemd:
sudo systemctl restart bayanat bayanat-celery - Docker: see Path C above
- Other: restart your WSGI server and Celery worker however you normally do
Verify
uv run flask doctor
uv run flask db currentLog in and verify the application works as expected.
What changed in v4
See the changelog for the full list. Key changes that affect the upgrade:
- Database migrations use Alembic.
flask db upgradereplaces the old manual SQL files. - New dependencies.
uv sync --frozeninstalls everything needed. - New CLI commands.
flask doctorchecks installation health;flask check-db-alignmentshows migration status.
Checking status
See which migration your database is on:
uv run flask db currentCheck whether your schema matches the models:
uv run flask check-db-alignmentRun full diagnostics:
uv run flask doctorTroubleshooting
flask db upgrade fails. Migrations run in a transaction, so a failure changes nothing. Fix the reported cause and run it again. If it reports multiple heads, stop: a merge revision is missing, and applying it blind will diverge the schema.
Services fail to start after hardening. Almost always a path that the sandboxing does not allow writes to. Check journalctl -u bayanat -n 50 for a read-only filesystem error, then either allow that path or move the write into shared/.
FATAL: Peer authentication failed. The operating system user you ran as is not in the ident map, or the rule landed below the catch-all in pg_hba.conf. Order matters; the first matching line wins.
Celery cannot reach Redis. REDIS_PASSWORD in .env does not match requirepass in redis.conf, or the worker was not restarted after the change.
502 from the web server. The socket moved to /run/bayanat/bayanat.sock. Check that the service unit creates its runtime directory and that your upstream points at the new path.
Application will not start after an upgrade. Check the application logs. Common causes are a missing dependency, fixed by re-running uv sync --frozen, or a configuration change, found by comparing .env against .env-sample.
flask doctor shows warnings. Warnings are non-critical. "No Celery workers responding" before you restart the worker and "MAIL_SERVER not configured" when email is not set up are both expected. Failures need attention.