Skip to main content

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

Upgrade sequence — same shape for every layout1 · BACKUP./backup.sh — 5 s insurance2 · PIN + PULLLIBERTY_IMAGE_TAG + pull3 · UP -dentrypoint runs init-db4 · SMOKE TEST/info + sign-in + poolsA clean Compose upgrade is ~30 s of downtime. Swarm rolling = zero downtime when replicas > 1.

Versioning contract

Liberty follows semver-ish:

BumpWhat to expect
Patch (0.42.00.42.1)Bug fixes only. No config change. No database migration.
Minor (0.42.x0.43.0)New features. Backward-compatible config. Additive database migrations only — new framework tables / columns; existing rows are left alone.
Major (0.x1.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-db step to run, no SQL file to apply, no migration job to schedule.

Pull a newer image, restart the stack — the schema follows.

No manual migration step

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:

.env
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 .envBehaviour
latestEach 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.

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 it and skips secret generation (no risk of regenerating LIBERTY_MASTER_KEY and losing every encrypted value).
  • Runs docker compose pull then docker compose up -d against the COMPOSE_FILE chain 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 .env as 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.

Never pass -f after install

COMPOSE_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.

StateLives inPreserved?
.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 secretapp.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 statetraefik-acme volume✔ No fresh ACME round-trip; LE rate limits aren't burned.
pgAdmin server registrations + preferencespgadmin-data volume
Portainer stateportainer-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_PASSWORDWas 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 lineWhat 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 replicasDowntime
> 1Zero. 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.

CheckHowWhat to confirm
Version bumpcurl http://<host>/info"version" matches the tag you just pulled.
Sign-inSign in as admin.Local auth still works.
Screen loadsOpen at least one app screen.Grid populates, no 500s.
Pools connectedSettings → PoolsEvery pool shows as connected.
Scheduler caught upSettings → Jobs — pick a scheduled job.Its last run is succeeded (or pending — not failed).
License accepted (when LIBERTY_LICENSE_KEY is set)Settings → LicenseShows 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 -d and 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.43 may chain deprecations that disappeared along the way. Going version by version keeps the warnings tractable.
  • Pin in production. :latest is fine for staging; production should pin LIBERTY_IMAGE_TAG so a routine pull cannot surprise you with a new minor.
  • Watch the logs after restart. A WARN line 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.