Skip to content

Upgrade and backup

What a backup is

A complete backup is a pg_dump of the database, a copy of the /data volume, and your SECRET_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.

Terminal window
# 1. the database
docker compose exec -T db pg_dump -U kiku kiku > kiku-$(date +%F).sql
# 2. the attachments
docker 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

Terminal window
docker compose down
docker 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 -d

Put 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

Terminal window
docker compose pull
docker compose up -d

Migrations 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:

  • Majoryou 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. pull is safe.
  • Patch — fixes. pull is 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):

Terminal window
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.