Upgrading
A Liberty upgrade is a tag swap. Every layout — Light / Full Compose, Docker Swarm, pipx — boils down to the same three moves: snapshot the volumes, fetch the new image (or wheel), let the stack reconcile. Schema migrations ride inside the new image; the entrypoint applies them on boot. No manual migrate-db, no source checkout, no rebuild.
This page covers the versioning contract, the upgrade procedure per layout, the post-upgrade smoke test and the rollback path.
At a glance
Versioning contract
Liberty follows semver-ish:
| Bump | What to expect |
|---|---|
Patch (0.42.0 → 0.42.1) | Bug fixes only. No config change. No database migration. |
Minor (0.42.x → 0.43.0) | New features. Backward-compatible config. Additive database migrations only — new framework tables / columns; existing rows are left alone. |
Major (0.x → 1.0) | Possible breaking changes. Called out individually in the release notes; a migration guide is published alongside. |
The release notes for every version live alongside the image tag — read them before bumping a minor or a major.
How database migrations work
The container entrypoint (and the pipx systemd unit, if you wired one) runs liberty-admin init-db on every boot. The command is:
- Idempotent. Running it twice does nothing the second time.
- Additive. It creates new framework tables a newer release brings, adds missing columns, and leaves existing rows alone.
- Bundled. Schema deltas ship inside the image / wheel — there is no separate
migrate-dbstep to run, no SQL file to apply, no migration job to schedule.
Pull a newer image, restart the stack — the schema follows.
The historical liberty-admin migrate-db command is gone. Everything it used to do is now part of the init-db that runs at startup.
Pin the image tag in production
The shipped compose files default to :latest so a fresh install lands on the newest release. Production should pin a specific tag in .env so an unexpected pull doesn't roll the stack forward to a new minor without warning:
LIBERTY_IMAGE_TAG=0.2.0
Roll forward by bumping the value and re-running pull && up -d. Roll back by setting the previous tag and doing the same.
Tag in .env | Behaviour |
|---|---|
latest | Each pull may move to the newest published image — convenient for staging, risky for prod. |
0.2.0 (pinned) | pull is a no-op once the local cache has the tag. Upgrades happen only when you edit .env. |
Always backup first
backup.sh tar-snapshots every Liberty named volume into a timestamped directory. It takes seconds and is the difference between a botched upgrade and a botched upgrade you can undo.
cd /opt/liberty-next/release
./backup.sh # → ./backups/YYYY-MM-DD_HHMMSS/
./backup.sh /mnt/nas/liberty # to an off-host location
./backup.sh --layout full # only the full layout's volumes
Full details on the backup format, the restore commands and a weekly cron entry: Docker → Backups.
Upgrade procedure — Light / Full (Compose)
Identical for both layouts. Two equivalent commands; pick by taste.
Option A — re-run ./install.sh <layout> (recommended)
cd /opt/liberty-next/release
./backup.sh # 1 — snapshot (always)
./install.sh full # 2 — sees existing .env, skips secret generation,
# pulls new images, runs `docker compose up -d`,
# waits for healthy, prints the summary
install.sh is idempotent on re-runs. When .env already exists it:
- Logs
.env already exists — keeping itand skips secret generation (no risk of regeneratingLIBERTY_MASTER_KEYand losing every encrypted value). - Runs
docker compose pullthendocker compose up -dagainst theCOMPOSE_FILEchain in.env. - Waits for
liberty-next's healthcheck (/info) to report healthy. - Prints the summary — the Sign in to Liberty as line now reads password unchanged from the original install (the first-boot admin password was a one-shot — your existing login still works). The pgAdmin password is re-printed from
.envas a reminder.
Use the same layout you installed with — passing the wrong one (./install.sh light on a full install or vice-versa) starts the wrong compose file.
Option B — bare docker compose (same effect)
cd /opt/liberty-next/release
./backup.sh # 1 — snapshot
docker compose pull # 2 — fetch new images (COMPOSE_FILE picks the right files)
docker compose up -d # 3 — recreate containers; entrypoint runs init-db
# 4 — smoke test
curl -s http://127.0.0.1:8000/info
Identical effect, less output, no summary printout. Use this in CI or whenever install.sh's interactive niceties would get in the way.
-f after installCOMPOSE_FILE in .env carries the full chain (docker-compose.full.yml:docker-compose.tls-letsencrypt.yml:docker-compose.apps.yml after install.sh --ssl letsencrypt --apps ...). Passing -f docker-compose.full.yml manually overrides that chain and silently drops the TLS + apps overlays — the next up -d removes them. Stick to bare docker compose pull / up -d, or use ./install.sh. See Docker → COMPOSE_FILE discipline.
What up -d does: Compose sees the image digest changed, recreates the liberty-next container in place, mounts the same named volumes, restarts it. The new entrypoint runs liberty-admin init-db, then starts serving. Total downtime: ~30 s on a warm host.
Other services in the stack (Postgres, pgAdmin, Portainer, Traefik) are not recreated unless their own image tag moved — pull is per-service and up -d only recreates the ones whose spec changed.
What survives the upgrade
Everything operator-touched. Compose recreates the liberty-next container; the volumes (and the apps bind mount when the apps overlay is on) reattach to the new container intact.
| State | Lives in | Preserved? |
|---|---|---|
.env (master key, JWT secret, Postgres / pgAdmin passwords) | release/.env on the host | ✔ Untouched. |
| Framework DB (auth, Nomaflow run history, licensed-app data) | pg-data volume | ✔ Postgres is not recreated unless its image tag moved. |
Operator-edited TOMLs (app.toml + connectors.toml + screens.toml + menus.toml + dashboards / charts / dictionary) | liberty-config volume | ✔ Mounted at /app/config in the new container. |
| License key, Anthropic API key, OIDC client secret | app.toml (encrypted ENC: values with the install master key) | ✔ Lives in liberty-config. The new container decrypts with the unchanged LIBERTY_MASTER_KEY in .env. |
| Let's Encrypt cert + ACME state | traefik-acme volume | ✔ No fresh ACME round-trip; LE rate limits aren't burned. |
| pgAdmin server registrations + preferences | pgadmin-data volume | ✔ |
| Portainer state | portainer-data volume | ✔ |
| Licensed-apps bundle (Nomasx-1 / Nomajde TOMLs + plugin code) | ./apps/ bind mount on the host (set as APPS_HOST_PATH in .env) | ✔ The mount reattaches to the new container at /apps:ro. |
First-boot LIBERTY_ADMIN_PASSWORD | Was shown once during the original install, never written to .env | ✔ Existing admin user keeps its prior password. Lost it? docker exec liberty-next liberty-admin reset-admin-password. |
:latest vs pinned tag
.env line | What pull fetches |
|---|---|
# LIBERTY_IMAGE_TAG=latest (commented — default) | Compose defaults to :latest. Every pull may move to the newest published image — fine for staging, risky for prod. |
LIBERTY_IMAGE_TAG=7.0.21 (pinned) | pull is a no-op once the local cache has the tag. Upgrades happen only when you bump the value + pull && up -d. |
To pin: uncomment the line in .env, set the version, run ./install.sh full (or docker compose pull && up -d).
When the licensed-apps wheel changes too
./install.sh full (or docker compose pull) only refreshes the liberty-next image — not the apps bundle on the ./apps/ bind mount. For a liberty-apps wheel update (new Nomasx-1 / Nomajde release), use the dedicated installer:
./install-apps.sh /path/to/new-liberty_apps-X.Y.Z.whl
docker compose restart liberty-next # picks up the refreshed TOMLs
The wheel installer is idempotent — operator-edited TOMLs in ./apps/config/ are preserved unless --force-config is passed. See Deploy prebuilt apps → Updating the apps later.
Most framework upgrades are liberty-next-only — no apps wheel to refresh.
Upgrade procedure — Docker Swarm
Two ways. Pick one.
Option A — rolling update on the liberty-next service only
./backup.sh
docker service update --image ghcr.io/fblettner/liberty-next:0.2.0 liberty_liberty-next
Swarm rolls the service per its update_config (order: start-first, parallelism: 1). The new task is started, health-checks, then the old task is stopped:
liberty-next replicas | Downtime |
|---|---|
> 1 | Zero. Swarm starts a new task, waits for it to be healthy, then drains the old one. |
1 (default) | A brief window (~10 s) while the new task warms up — start-first minimises but cannot eliminate it. |
The other services in the stack (pg, pgadmin, portainer, traefik) are untouched.
Option B — bump .env + re-run the deploy script
./backup.sh
# edit .env: LIBERTY_IMAGE_TAG=0.2.0
./deploy-swarm.sh
docker stack deploy reconciles the full spec — every service whose image or env changed is rolled. Re-running ./deploy-swarm.sh IS the update mechanism in Swarm; it's the same script you used to install.
Use Option B when you bump multiple service tags at once or when you've changed any other service's env.
Rollback in Swarm
Swarm keeps the previous spec of every service:
docker service rollback liberty_liberty-next
The rollback reverses the last service update (or the last stack deploy's effect on that service). If the new image applied a schema delta and you've been writing to it, restore from the backup.sh snapshot before rolling back — see Rollback below.
Upgrade procedure — pipx
./backup.sh # if you keep a backup script alongside (recommended)
pipx upgrade liberty-next
sudo systemctl restart liberty-next # if running under systemd
pipx upgrade swaps the wheel in the isolated venv. The restarted service runs liberty-admin init-db on boot — same idempotent schema sync as the container path.
For the systemd unit definition, the EnvironmentFile, and the post-install verification: Python server → Run under systemd.
Smoke test checklist
Five minutes, every upgrade.
| Check | How | What to confirm |
|---|---|---|
| Version bump | curl http://<host>/info | "version" matches the tag you just pulled. |
| Sign-in | Sign in as admin. | Local auth still works. |
| Screen loads | Open at least one app screen. | Grid populates, no 500s. |
| Pools connected | Settings → Pools | Every pool shows as connected. |
| Scheduler caught up | Settings → Jobs — pick a scheduled job. | Its last run is succeeded (or pending — not failed). |
License accepted (when LIBERTY_LICENSE_KEY is set) | Settings → License | Shows accepted with the right customer name. |
A failed check should hold the rollout and trigger the rollback procedure.
Rollback
The same simplicity applies in reverse: point back at the previous tag, restart. The wrinkle is the database — additive schema deltas are not auto-rolled-back. In practice:
| New schema is… | What to do |
|---|---|
| Additive only (new tables / columns — the common case) | Roll the image back. The old framework ignores the extra columns and keeps working. No DB restore needed. |
| Non-additive (column rename, constraint added, value migrated) | Restore the volume from the backup.sh snapshot before rolling the image back. The release notes call out every non-additive migration. |
Light / Full (Compose)
# edit .env: LIBERTY_IMAGE_TAG=0.1.0 (the previous tag)
docker compose pull # COMPOSE_FILE picks the right files
docker compose up -d
If the schema moved forward and you need to roll back the data too, restore the relevant volume from ./backups/<timestamp>/ first — see Docker → Backups for the per-volume restore command.
Swarm
docker service rollback liberty_liberty-next
Same caveat: if the schema diverged, restore the pg-data volume (after ./deploy-swarm.sh --rm) before rolling back the service, then re-deploy.
pipx
pipx install liberty-next==0.1.0 --force
sudo systemctl restart liberty-next
Tips
- Stage the upgrade. Run the new tag against a copy of production for a day before rolling to prod. The cost of dragging a known-bad rollout is much higher than the cost of one extra restart.
- Read the release notes. Most upgrades are silent —
pull && up -dand the smoke test passes. The few that aren't are called out explicitly. Two minutes of reading saves an hour of debugging. - Don't skip multiple minors at once if you can avoid it. Going
0.40 → 0.43may chain deprecations that disappeared along the way. Going version by version keeps the warnings tractable. - Pin in production.
:latestis fine for staging; production should pinLIBERTY_IMAGE_TAGso a routinepullcannot surprise you with a new minor. - Watch the logs after restart. A
WARNline about a deprecated field is the heads-up that the next minor will break — fix it now, not later.
What's next
- Docker → Backups — the snapshot format, restore commands and weekly cron entry referenced above.
- Python server — the pipx install + systemd recipe the upgrade lands on.
- Production — hardening, OIDC, scheduler pin — the deployment shape upgrades land into.