-
Notifications
You must be signed in to change notification settings - Fork 6
backup and restore
barakoCMS does not back itself up. The database is yours, and so is the backup. This page is what runs, where it writes, and how to restore, with the restore proved rather than assumed.
scripts/restore-check.sh runs this whole procedure in CI on every pull request: it takes a backup
with the same script the deployments run, destroys the database, restores it, and boots the app
against the result.
Every deployment path takes a nightly backup that is checked before it is published.
| Deployment | Service | Database | Where backups go |
|---|---|---|---|
docker-compose.yml (development) |
app |
barako_cms |
./backups on the host |
docker-compose.prod.yml |
app |
${DB_NAME:-barako_cms} |
the postgres_prod_backups volume |
quickstart/docker-compose.yml |
api |
${DB_NAME:-barakocms} |
the backups volume |
k8s/ |
Deployment barako-cms
|
barako_cms |
the barako-backups PVC |
The three compose stacks run scripts/backup-cron.sh. The quickstart runs its own copy,
quickstart/scripts/backup-cron.sh, because that folder is meant to be copied out of the
repository; scripts/check-quickstart-backup-script.sh fails CI when the two differ. The Kubernetes
CronJob carries the same logic inline, because a CronJob has no repository to mount; if you change
one, change both.
Defaults: 02:00 daily, 14 days retained in production and 7 elsewhere. Override with
BACKUP_CRON_SCHEDULE and BACKUP_KEEP_DAYS.
The service name, database name and backup location all differ between these stacks. That is why the restore section below is split by deployment rather than given once: a restore command copied from the wrong row silently targets the wrong database.
Because that reports success when it has failed. gzip succeeds on empty input, and in a pipeline
its exit code is the one you get, so a dump that could not connect leaves a 20-byte archive and a
green log. That is not hypothetical here: the credentials were wrong for months and every nightly
backup was an empty gzip claiming to be fine.
So the script dumps to a plain file first, checks pg_dump's own status, requires the result to
exceed a minimum size, greps it for the PostgreSQL database dump header, gzips it, tests that the
archive decompresses, and only then moves it to its final name. A failed run leaves nothing behind
that looks like a backup. Rotation happens only after a success, so a bad night never deletes the
last good copy.
Stop the application first. A restore into a database the app is writing to will not be the database you backed up.
docker compose, development stack
docker compose stop app
gunzip -c ./backups/barako_backup_2026-08-30_02-00-00.sql.gz \
| docker compose exec -T postgres psql -U postgres -d barako_cms -v ON_ERROR_STOP=1
docker compose start appdocker compose, production stack
Archives live in a named volume rather than a host directory, so read them from inside the backup container:
docker compose -f docker-compose.prod.yml stop app
# List what is there.
docker compose -f docker-compose.prod.yml exec db-backup ls -1 /backups
docker compose -f docker-compose.prod.yml exec -T db-backup \
sh -c 'gunzip -c /backups/barako_backup_2026-08-30_02-00-00.sql.gz' \
| docker compose -f docker-compose.prod.yml exec -T postgres \
psql -U "${DB_USER:-postgres}" -d "${DB_NAME:-barako_cms}" -v ON_ERROR_STOP=1
docker compose -f docker-compose.prod.yml start appquickstart stack
The service is api, not app, and the database defaults to barakocms rather than barako_cms:
cd quickstart
docker compose stop api
docker compose exec -T db-backup sh -c 'gunzip -c /backups/barako_backup_2026-08-30_02-00-00.sql.gz' \
| docker compose exec -T postgres psql -U "${DB_USER:-postgres}" -d "${DB_NAME:-barakocms}" -v ON_ERROR_STOP=1
docker compose start apiKubernetes
kubectl -n barako-cms scale deployment/barako-cms --replicas=0
kubectl -n barako-cms run restore --rm -it --restart=Never \
--image=postgres:16-alpine \
--overrides='{"spec":{"volumes":[{"name":"b","persistentVolumeClaim":{"claimName":"barako-backups"}}],
"containers":[{"name":"restore","image":"postgres:16-alpine","stdin":true,"tty":true,
"volumeMounts":[{"name":"b","mountPath":"/backups"}]}]}}' \
-- sh -c 'gunzip -c /backups/$(ls -1 /backups | tail -1) \
| PGPASSWORD=$PGPASSWORD psql -h postgres-service -U postgres -d barako_cms -v ON_ERROR_STOP=1'
kubectl -n barako-cms scale deployment/barako-cms --replicas=2ON_ERROR_STOP=1 is not optional. Without it psql reports success after skipping every statement
it could not apply, which is the same failure shape as the empty gzip.
Do not. Restoring over an existing schema can appear to work because the objects the dump failed to carry are already there, so you learn nothing about whether the archive is complete.
Drop and recreate first:
drop database barako_cms with (force);
create database barako_cms;The CI drill does exactly this, and asserts the database has zero tables before restoring, so a restore that quietly relies on leftovers fails there rather than in front of a customer.
Check three things, in this order:
-
The app boots. It applies the Marten schema at startup under
CreateOnly, so a restore that lost an object fails loudly here rather than later. If it exits non-zero, read the error: it names the object. - You can sign in. Proves the users, roles and password hashes came back.
-
Content is served.
GET /api/contentsshould report the row count you expect.
The projection daemon resumes from mt_event_progression, which the dump carries, so workflows do
not re-fire for events that were already processed. If that table were lost, the first boot would
replay every event and send every email again, which is why the drill checks the app rather than
just the row counts.
- Point-in-time recovery. These are nightly snapshots. Losing up to a day of writes is the stated exposure, and closing it means WAL archiving, which is a Postgres decision rather than a barakoCMS one.
- Files. The Files module stores bytes in Postgres by default, so they are inside the dump. With the S3 module configured they are not, and that bucket needs its own backup.
- An RPO or RTO. There is no measured recovery time to quote in a contract. The CI drill gives a lower bound on a small database and nothing more.
Generated from docs/backup-and-restore.md by scripts/wiki-sync.sh. Edit the doc in the repository, not this page.
Releases
Start here
- Approval by configuration
- Configuring email
- Delivering a client project on barakoCMS
- Deploying barakoCMS on a VM
- Deploying barakoCMS on a managed platform
- Upgrading from 3.x to 4.0
- Your first module
Content
- Content type blueprints
- Choice fields
- Pushing entries to a collection
- Collections filled from outside
- Public delivery API
- Event-sourced content types
- Image variants
- Scheduling publish, unpublish and sensitivity
- SEO fields
- Site settings
- URL redirects
Security and access
- Security and compliance posture
- Scanning uploads for malware
- Where the admin keeps your session, and why
Tenancy
Operations