Upgrading and backups¶
This page answers one question: if you deploy this today, can you take the next release and keep your data?
The short answer is yes, with one honest caveat stated at the bottom. The longer answer is that the platform now refuses to start rather than run against a database whose history it cannot account for — which is the only kind of promise worth making about somebody else's data.
How your data is kept¶
Every schema — the core's and one per kit — is a numbered list of migrations, applied in order, once each, and a released migration is never edited. Rolling one back is not a thing the platform does: the way back from a migration is a restore.
That rule is now enforced rather than trusted. Each applied migration is recorded with a SHA-256 of its text, and a service refuses to start when the recorded history and its own build disagree:
| What it found | What it does |
|---|---|
| The database is at a version this build does not have | Refuses. You are starting an older build than the data; run the newer one, or restore from before the upgrade. |
| A gap in the applied versions | Refuses. This is what a migration inserted into the middle of the list looks like from the database's side, and the schema is not the shape either side thinks it is. |
| An applied migration's text is not this build's text | Refuses, naming the migration. A released migration was edited, so this database is in the old shape and no later migration will fix it. |
A service that will not start is a bad morning. A service that quietly runs against a schema nobody can describe is an unrecoverable afternoon, usually several weeks later and on somebody else's machine.
Installations deployed before this was added
Rows applied before checksums existed have none. They are adopted — the current text's checksum is recorded — and the log says so once per schema. Adopting is not verifying: what those installations actually ran cannot be recovered, only assumed. This is a one-time event and it does not repeat.
How to find your version¶
- the Overview screen shows "The source of this version: … (build
<version>)"; GET /healthzon the core returns{"ok": true, "version": "…"}.
Before you upgrade¶
Two of these are not optional, and the reason is the same for both: they are the things that cannot be reconstructed from anywhere else.
-
Back up the database. Everything the platform knows is in it: users and rights, the resource registry, schedules, job history, the metric history, encrypted secrets, and the licence. One command, and it is the whole installation:
-
Keep
.env, and above allAG_SECRET_KEY. Secrets are encrypted at rest with it, and the platform cannot open them without it. A database backup taken without the key is a backup of ciphertext. Keep the key somewhere that is not the same disk as the database — that is what makes it a backup rather than a copy. -
Read the release notes for anything between your version and the one you are moving to. Where a release needs more than
up -d, it says so there and nowhere else.
How to upgrade¶
git pull # the compose file of the new release names its images
docker compose pull # the images come from Docker Hub
docker compose up -d
Nothing in .env needs editing. The compose file of each release carries
its own version as the default for AG_TAG and its own list of kits as the
default for AG_MODULES; git pull brings that file, and pull fetches
what it names. Built from source? Rebuild into your namespace with
tools/images.sh and up -d.
Coming from 0.1.0: comment three lines out first. That release's
.env.example shipped AG_TAG, AG_IMAGE_REPO and AG_MODULES live, and
an .env copied from it still has them — nobody set them on purpose, and
every one now overrides the release. AG_TAG= pins the installation to one
version while the tree moves on, so pull fetches the old tag.
AG_MODULES= replaces the kit list entirely, so a kit added by a release
starts as a container the core never hears of — no menu entry, nothing on
the overview — until its id=address is added there (for 0.2.0,
nexus=http://kit-nexus:8106). Reported from a real upgrade in exactly
that shape.
Keep a line live only when you mean it — a pinned version, an image namespace of your own, a kit deliberately left out — and then it is yours to keep current; the changelog says what each release adds. From 0.2.0 on the example ships all three commented out, so this is a one-time step.
Migrations run at each service's startup, each against its own schema. Read
the logs once afterwards — a service that refused to start says why in plain
words, and docker compose ps will show it as not running:
The core and the kits are upgraded together. They are separate
containers, so for a few seconds during up -d a new core may be talking to
an old kit or the reverse. That window is tolerated, not designed for:
upgrade them in one command rather than one at a time.
How to roll back¶
By restoring, and only by restoring. There is no downgrade path: the older build will refuse to start against the newer database, deliberately and with a message saying so.
docker compose down
git checkout <the previous tag>
docker compose pull # the previous release's images are still on Docker Hub
# restore the dump taken before the upgrade, then:
docker compose up -d
This is why the dump above is not optional. It is the rollback plan.
The platform database's major version¶
Everything above is git pull && docker compose pull && up -d. The one thing that
procedure cannot change is the major version of the platform's own
PostgreSQL: a data directory belongs to its major, and a newer server
refuses to start on an older directory. The compose file ships the newest
major available at the release (postgres:18-alpine), so this comes up
rarely — when the compose file moves to a newer major, or when an
installation was deployed from the repository before the release and is
still on 17.
The procedure is a dump and a restore, and it is the same one the platform's own database kits perform every night, done by hand once:
# 1. a full dump from the running server, with roles
docker compose exec -T postgres pg_dumpall -U "${AG_DB_USER:-ag}" > platform-dump.sql
# 2. stop everything; the data directory is about to be replaced
docker compose down
# 3. the old directory goes — it is unreadable by the new server anyway.
# `docker volume ls` shows the exact name: <project>_pgdata.
docker volume rm adminguide_pgdata
# 4. the new server, alone, so it can create an empty directory. Coming from
# 17 or older, pull the new compose file first: the 18+ images keep their
# data under /var/lib/postgresql/<major>/ and the volume is mounted one
# level up (`pgdata:/var/lib/postgresql`), not at the old .../data — a
# server started with the old mount refuses to start and says so.
docker compose up -d postgres
# wait for "healthy" in `docker compose ps`
# 5. the dump back in. The role and the database already exist on the fresh
# server (the compose file creates them), so two "already exists" errors
# at the top are expected and harmless.
docker compose exec -T postgres psql -U "${AG_DB_USER:-ag}" -d postgres < platform-dump.sql
# 6. the rest
docker compose up -d
Keep platform-dump.sql until the core and every kit have started and their
logs show no migration refusal; it is the way back. The schema versions
recorded in the dump travel with it, so the checksum guarantee above holds
across the move: a kit that finds its history intact simply continues.
Backing up the platform itself¶
The platform backs up other people's databases; its own is the one it has no button for. Two ways, and they are not alternatives:
- The dump above, on a cadence of your own. Simple, outside the platform, and it still works when the platform does not — which is the property that matters in the case you are keeping it for.
- Point the PostgreSQL Kit at the platform's own database. Then it is a target like any other: schedule, retention, verification by restoring, and its copies land in a storage you chose. Do this as well as, never instead of: a platform that only backs itself up through itself has one failure that takes both.
Either way, back up .env separately and keep AG_SECRET_KEY somewhere
else again. A restore of the database alone gives you an installation whose
every secret is unreadable.
What is tested, and what is not¶
The refusals above are verified on a live installation by the project's own check, which applies migrations to a throwaway schema, then edits one, removes one from the middle, and downgrades — and checks that each is refused, named, and that nothing was applied on the way to refusing. It also checks every schema in the running installation.
The caveat, stated plainly: what is not yet automated is a full
end-to-end upgrade — deploy the previous release, fill it with data through
the API, upgrade to the current one and compare. That test is planned and
named in the repository's development notes (Б2.6). Until it exists, the guarantee this page
describes is "the platform refuses to run against a history it cannot
account for", which is a real guarantee and a narrower one than "every
upgrade path has been rehearsed".