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.dbis SQLite in WAL mode, so a copy taken while the app is writing can miss committed data that still lives inapp.db-wal. Stopping is simpler than reasoning about it. -
.envmatters as much as the database — it holdsFERNET_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, skipdocker compose pulland rundocker compose up -d --buildinstead. The compose file carries bothbuild:andimage: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 psshould show the service ashealthy— the compose healthcheck polls/api/healthevery 30 seconds and gives the container 20 seconds to come up first. -
alembic currentshould 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 declaresbuild: .as well asimage:, sodocker compose up -d --buildbuilds the working tree and labels the result with whatever tag is pinned — leaving you with an image called:1.0.0that is really your checkout. If you build your own images (on ARM, say), pin by commit —git checkout v1.0.0before 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 agit pullis 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.