Skip to content

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 /healthz on 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.

  1. 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:

    docker compose exec -T postgres \
      pg_dump -U "${AG_DB_USER:-ag}" -Fc "${AG_DB_NAME:-adminguide}" > ahegao-$(date +%F).dump
    
  2. Keep .env, and above all AG_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.

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

sed -i 's/^AG_\(TAG\|IMAGE_REPO\|MODULES\)=/#&/' .env

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:

docker compose ps
docker compose logs --tail 50 core

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