SongCollect writes a backup bundle with songcollect-service backup create. A
bundle is one tar file named songcollect-<UTC time>.tar, for example
songcollect-20261020T020000Z.tar. It contains:
| Entry | Content |
|---|---|
database.db | A consistent snapshot of the database, taken by the engine with VACUUM INTO while the service keeps running. |
assets/churches/<church>/sha256/<digest> | Every stored file the snapshot references. Each file's SHA-256 is checked against the database before it is added. |
manifest.json | Bundle format, application version and source commit, applied migrations with checksums, and the size and SHA-256 of the database and of every file. |
Published files are immutable and never deleted, so a snapshot's files stay readable while the bundle is written. Temporary uploads are not backed up. Bundles are not encrypted yet and secrets are not included. Keep the destination private.
Backups require a local file: database. A Turso Cloud database needs the
provider's export instead.
Configure๐
| Variable | Meaning |
|---|---|
BACKUP_STORE_URL | Destination: s3://bucket/prefix or file:/directory. The directory must be on another disk or host than the data volume. |
BACKUP_AWS_ENDPOINT, BACKUP_AWS_REGION, BACKUP_AWS_ACCESS_KEY_ID, BACKUP_AWS_SECRET_ACCESS_KEY | S3 settings. Any object_store S3 option works with the BACKUP_AWS_ prefix. They are separate from the asset store's AWS_* variables. |
BACKUP_RETENTION_DAYS | Bundles older than this are deleted after a successful backup. Default 30. |
BACKUP_MAX_AGE_HOURS | backup status fails when the newest bundle is older. Default 26. |
The S3 user needs ListBucket, GetObject, PutObject, DeleteObject, and the
multipart upload actions on the bucket. Uploads are multipart, so a bundle
appears only once it is complete. Pruning only touches objects named like a
bundle at the top of the prefix, and never the bundle just written.
Production๐
Production is the Coolify application app (axiwbus6rcnm6gpacfbafzhw, team
SongCollect Devs, environment production) on songcollect.schmiednet.de.
Bundles go to a different machine: MinIO on beta2, bucket
songcollect-backups, prefix production.
- The
BACKUP_*variables are production-only runtime variables ofapp. The MinIO usersongcollect-productionmay only use thesongcollect-backupsbucket (policysongcollect-backups). - MinIO is the
minioapplication in Coolify's Private Apps project. Its Coolify domain only serves the console, so the S3 API is published athttps://s3.beta2.schmiednet.deby the Traefik file/data/coolify/proxy/dynamic/songcollect-backup-s3.yamlon beta2. - The nightly job is a Coolify Scheduled Task on
app: namenightly-backup, commandsongcollect-service backup create, frequency0 2 * * *(02:00 UTC), containerapp. A non-zero exit marks the run as failed in Scheduled Tasks. Coolify sends its scheduled-task failure notification to the team's configured notification channels.
Check backup age๐
songcollect-service backup status
The command lists every bundle with its size and upload time, then prints the
age of the newest one. It exits non-zero when there is no bundle or the newest
is older than BACKUP_MAX_AGE_HOURS. In Coolify, run it from the app
terminal, or check the latest nightly-backup run in Scheduled Tasks.
Restore๐
songcollect-service backup restore <bundle name> restores a bundle into the
database at TURSO_DATABASE_URL and the asset store. It refuses to run when
that database file exists. Before writing anything it checks:
- the database snapshot and every file against the manifest's SHA-256 and size;
- that the bundle contains exactly the listed files, and that the list matches
the snapshot's
asset_blobs; - the schema. A bundle from an older release is migrated to the running release, and an altered migration is refused.
It then writes the files, then the database, and revokes all sessions so everyone signs in again. It prints the number of users, churches, songs, and verified files.
Restore drill in an isolated instance๐
Restore into a fresh Docker volume on any machine that can reach the bucket, using the production image or a build of the same commit:
export BACKUP_STORE_URL=s3://songcollect-backups/production BACKUP_AWS_ENDPOINT=https://s3.beta2.schmiednet.de BACKUP_AWS_REGION=us-east-1
export BACKUP_AWS_ACCESS_KEY_ID=... BACKUP_AWS_SECRET_ACCESS_KEY=... # from Coolify
image=songcollect:restore-drill; docker build -t "$image" .
run() { docker run --rm -v songcollect-restore:/data -e TURSO_DATABASE_URL=file:/data/songcollect.db \
-e BACKUP_STORE_URL -e BACKUP_AWS_ENDPOINT -e BACKUP_AWS_REGION -e BACKUP_AWS_ACCESS_KEY_ID -e BACKUP_AWS_SECRET_ACCESS_KEY "$image" "$@"; }
run songcollect-service backup status
run songcollect-service backup restore songcollect-<time>.tar
docker run --rm -d --name songcollect-restore -p 127.0.0.1:3000:3000 -v songcollect-restore:/data \
-e TURSO_DATABASE_URL=file:/data/songcollect.db -e APP_ORIGIN=http://localhost:3000 "$image"
Then open http://localhost:3000 and verify:
- Sign-in works with a production account.
- The song list and search match production.
- A song's lyrics and its arrangements show, and a sheet-music PDF opens.
Record the date, bundle, and the time from start to finished checks (the target
is four hours). Remove the instance afterwards with docker rm -f songcollect-restore && docker volume rm songcollect-restore, because it holds
production data.
Restore production๐
- Stop
appin Coolify. Keep the old volume until the restored instance is verified. - Point
TURSO_DATABASE_URLat a new path on the volume, for examplefile:/data/restored/songcollect.db, and setASSET_STORE_URL=file:/data/restored/assets. - Run
songcollect-service backup restore <bundle name>once, for example as a one-off Coolify scheduled task or withdocker runagainst the volume. - Start
app, verify as in the drill, then remove the old files.