Upgrade and backup
What a backup is
A complete backup is a
pg_dumpof the database, a copy of the/datavolume, and yourSECRET_KEY.
All three, in that one sentence, because two of them restore an instance that cannot reach its
own mailbox. Mailbox credentials, AI keys and Integration tokens are encrypted under a key
derived from SECRET_KEY; keep the key with the database dump because neither can restore those
connections by itself. A dump restored without it comes up looking healthy while those
connections cannot work.
There is no built-in backup feature — no scheduler, no download button in the admin UI, no sidecar container. Two commands and whatever you already run for the rest of your server.
# 1. the databasedocker compose exec -T db pg_dump -U kiku kiku > kiku-$(date +%F).sql
# 2. the attachmentsdocker run --rm -v kiku_kiku-data:/data -v "$PWD:/backup" alpine \ tar czf /backup/kiku-data-$(date +%F).tar.gz -C /data .The attachment store is content-addressed and therefore append-only, so a file-level tar
or rsync of it is consistent without stopping the container. That is the only reason this
story is two commands long.
The volume name is prefixed with your Compose project name — kiku_kiku-data if the directory
is called kiku. docker volume ls if you are not sure.
Restore
docker compose downdocker compose up -d db
docker compose exec -T db psql -U kiku -d kiku < kiku-2026-08-01.sql
docker run --rm -v kiku_kiku-data:/data -v "$PWD:/backup" alpine \ tar xzf /backup/kiku-data-2026-08-01.tar.gz -C /data
docker compose up -dPut the same SECRET_KEY back in the Compose file before starting the app. If you cannot, the
app still boots and Settings reports that the saved credentials could not be decrypted; re-enter
the mailbox, AI and Integration credentials and save them again.
Upgrading
docker compose pulldocker compose up -dMigrations run automatically at boot. The tag in the shipped Compose file is :1, the major,
so pull takes every patch and minor and can never cross a breaking change unattended.
Take a pg_dump before a major. That dump is the entire rollback story — see below.
A failed migration is loud on purpose: the app logs the pending migrations and the Postgres
error, exits non-zero, and restart: unless-stopped turns that into a restart loop rather
than a half-migrated schema serving a support inbox. The whole pending batch runs in one
transaction, so a failure leaves the schema where it was. Boot also logs the app version and
the number of migrations applied, so “what am I running” needs no shell.
Skipping versions is fine. Migrations are cumulative from empty and every file is retained, so 1.0 → 1.9 is the same code path as nine hops.
There is no downgrade
No down migrations, in any version. Untested reverse migrations are theatre, and half of
them cannot be honest once the data they dropped is gone.
Recovery from a bad upgrade is: stop the app, restore the pg_dump you took, pin the previous
tag (ghcr.io/falcondev-oss/kiku:1.4.2), start. Without that dump there is no way back.
Versioning
SemVer, with the self-hoster’s definition rather than the library one — nobody imports Kiku:
- Major — you must do something. An environment variable renamed, a manual step, a raised minimum Postgres version. Read the release notes; take a dump first.
- Minor — features.
pullis safe. - Patch — fixes.
pullis safe.
Only the current major is supported. No backports, no LTS, no security window on old
majors. When 2.0 lands, getting fixes means taking the major — which for a two-service Compose
app with automatic migrations is a pull and a restart.
There is no release cadence. Releases are published to GitHub Releases with notes generated from the commit history.
Locked out of your own instance
/setup returns 404 once any user exists, so an instance whose only admin account is
unreachable has no recovery path in the product. Promote someone with SQL directly (user is
a reserved word in Postgres, so the quotes matter):
docker compose exec -T db psql -U kiku -d kiku \ -c "UPDATE \"user\" SET role = 'admin' WHERE email = 'you@example.com';"There is no rescue CLI. You already have the database.