OBM

Upgrading OBM CE

Read the changelog, snapshot ./data and .env, pull the new image — migrations run themselves on start. Plus how to verify the upgrade, how to roll back, and why production should pin a version tag instead of latest.

Upgrading CE is a two-command job on a good day, and the good day is the normal one: pull the new image, restart, migrations apply themselves. This page is about the other five minutes — the snapshot you take first, the checks you run after, and the way back if you need one.

How you find out there's a new version

Once a day CE asks obm.projomania.com whether a newer release exists, sending nothing but its edition and version string. Be aware of what that check does and doesn't do: it writes a line to the log, and nothing else. There is no banner in the UI, no e-mail, no version number on the dashboard. The line looks like this:

OBM CE 1.1.0 is available (you are running 1.0.0) — https://obm.projomania.com/download

So the reliable habit is to watch the changelog, or to grep for it when you're already in the shell:

docker compose exec app python -c "import app; print(app.__version__)"
docker compose logs app | grep "is available"    # the daily update check, if it fired

(VERSION_CHECK=false in .env turns the check off entirely — see CE operations.)

1. Read the changelog first

Check the changelog and the repository's CHANGELOG.md for every version between the one you run and the one you're moving to — not just the newest entry. That is where a required .env variable, a changed default or a one-way migration would be called out. If nothing there mentions your setup, the upgrade is routine.

2. Snapshot the data directory and .env

Migrations run automatically and they run forward only. The snapshot is your rollback plan; there is no alembic downgrade path back (more on that below).

cd /path/to/obm-ce
SNAP=../obm-ce-preupgrade-$(date +%F)
mkdir -p "$SNAP"

docker compose stop app          # app.db is WAL-mode — quiesce before copying it
cp -a .env "$SNAP"/
cp -a data/app.db* "$SNAP"/      # the DB plus its -wal / -shm sidecars

Three things worth knowing about what you just copied:

  • Stop the container first. data/app.db is SQLite in WAL mode, so a copy taken while the app is writing can miss committed data that still lives in app.db-wal. Stopping is simpler than reasoning about it.
  • .env matters as much as the database — it holds FERNET_KEY, without which every stored master password is unreadable. Keep a copy in your password manager as well as in the snapshot.
  • You do not need to copy data/backups/. The archives themselves are not touched by an upgrade, and copying a few hundred gigabytes to protect against a five-minute rollback is a bad trade. (Backing them up for their own sake is a different question — see CE operations.)

3. Pull and restart

git pull                # the compose file lives in the repo too
docker compose pull     # fetch the new image
docker compose up -d    # recreate; migrations run during startup
docker compose logs -f app

That's the whole upgrade. docker compose up -d notices the new image and recreates the container; on start the entrypoint runs alembic upgrade head before the web server, so the schema is always current by the time anything can serve a request. The startup is strict about it: if a migration fails, the container exits rather than running against a half-migrated database. A container that keeps restarting right after an upgrade is telling you to read docker compose logs app, and the answer will be near the top.

Because ./data is a bind mount next to the compose file — not a named volume — there is nothing to migrate or re-attach, and your login sessions survive (the JWT secret persists at data/.jwt_secret).

If you build the image yourself rather than pulling ours, skip docker compose pull and run docker compose up -d --build instead. The compose file carries both build: and image: under the same tag, so a pull will happily replace your locally built image with ours.

4. Verify it worked

docker compose ps                             # app should say (healthy)
curl -fsS http://localhost:8300/api/health    # {"ok":true}

docker compose exec app python -c "import app; print(app.__version__)"
docker compose exec app alembic current       # e.g. 5a833287c707 (head)
  • docker compose ps should show the service as healthy — the compose healthcheck polls /api/health every 30 seconds and gives the container 20 seconds to come up first.
  • alembic current should print a revision with (head) after it. If it doesn't, the migration didn't finish.
  • Then use it. Open the dashboard, confirm your customers and schedules are all there, and run one backup by hand rather than waiting for the schedule to prove it at 3 AM.

Pin a version for production

The docker-compose.yml in the repository asks for ghcr.io/projomania/obm-ce:latest, because that is the friendliest default for a first install and it is what docker compose pull follows. For anything you'd be annoyed to have change under you, pin the exact tag instead, so an upgrade only ever happens when you edit this file. The current release is 1.0.0:

# docker-compose.override.yml — gitignored, so it survives a git pull
services:
  app:
    image: ghcr.io/projomania/obm-ce:1.0.0

Put that in docker-compose.override.yml, which Compose merges automatically and the repository already gitignores — so git pull never fights you over it, and the shipped file keeps saying :latest for everyone else. Every release publishes exactly two tags: the full semver version and latest. There are no floating :1 or :1.x tags, so pin the whole version. Upgrading then means: change the tag, docker compose pull, docker compose up -d.

A pin only holds if you pull. The compose service declares build: . as well as image:, so docker compose up -d --build builds the working tree and labels the result with whatever tag is pinned — leaving you with an image called :1.0.0 that is really your checkout. If you build your own images (on ARM, say), pin by commit — git checkout v1.0.0 before building — rather than by tag.

Rolling back

First, the thing not to do: alembic downgrade is not a rollback. The schema history currently starts from a single baseline revision, whose downgrade drops every table — customers, instances, schedules and the entire backup catalog. It is data destruction wearing the word "downgrade".

The real rollback is the image tag plus the snapshot you took in step 2:

# 1. pin the previous tag (see above), then:
docker compose stop app

# 2. put the old state back — remove the migrated DB first, don't merge
rm -f data/app.db data/app.db-wal data/app.db-shm
cp -a "$SNAP"/app.db* data/
cp -a "$SNAP"/.env .

docker compose up -d

Restore the database and pin the old image together. An old image against a newly-migrated database is the one combination nobody tests: the schema has columns the old code doesn't know and may be missing ones it requires. Your backup archives under data/backups/ are untouched by all of this — the catalog rows pointing at them come back with the database.

Notes for less common setups

  • ARM hosts. Published images are built for linux/amd64. On ARM you'll want to build locally (docker compose up -d --build) instead of pulling.
  • Old Compose. The compose file uses the newer env_file: long form, which needs Docker Compose v2.24 or later. If a git pull is suddenly followed by a parse error, your Compose is the thing that's out of date.
  • In-flight backups. Restarting cancels any backup that was running; its partial scratch file is purged on the next start and the job is marked failed. Upgrade at a quiet hour, or just let the schedule catch up.

Next steps

  • Troubleshooting — including what to do when the container won't start or you're locked out of the admin account.
  • Operate OBM CE well — retention, verification, HTTPS, and backing up OBM itself.
  • Changelog — what shipped, and when.