Backups
Crumb's Postgres database is the sole index mapping every recorded .mp4
file to its camera and time. Lose that database with no backup, and the
footage still sitting on disk becomes effectively unplayable, nothing else
knows what any of those files are.
Built into the API, on by default
The api service runs a nightly pg_dump on its own, no separate
container, no extra credentials, no setup required.
-
When: daily at 03:15 local time by default (
DB_BACKUP_SCHEDULE, timezone fromTZ), plus an immediate catch-up dump on boot whenever the newest backup is missing or older than about 25 hours. A fresh install gets its first backup within seconds of the API starting. -
What: a gzipped, plain-SQL dump (
--no-owner --no-privileges), written atomically, a temp file first, then renamed after an integrity check. -
Where:
DB_BACKUP_HOST_PATH(default./backups), with daily/weekly/monthly rotation:daily/crumb-YYYYMMDD-HHMMSS.sql.gzdaily/crumb-latest.sql.gz # symlink to the newest daily dumpweekly/crumb-<ISOyear><week>.sql.gzmonthly/crumb-YYYYMM.sql.gz # only if DB_BACKUP_KEEP_MONTHS > 0 -
Rotation keeps the newest N daily, weekly, and monthly dumps (configurable, see Environment reference). It never deletes the newest dump, and it only ever touches files matching its own naming pattern, a manual dump you drop in the same directory is never at risk.
-
Failures are reported directly: a failed run raises the
backup_failedalert immediately through whatever notification channels you've configured, and a separate freshness watchdog catches the case where the job silently stopped running at all.
The one thing to get right: permissions
The api container runs as uid 1001, so the host directory behind
DB_BACKUP_HOST_PATH needs to be writable by that uid:
sudo chown -R 1001:1001 "${DB_BACKUP_HOST_PATH:-./backups}"
scripts/setup-env.sh prepares the default ./backups directory for you
when it can. If the directory isn't writable, the API logs a clear
warning, raises one backup_failed alert, and disables backups without
affecting anything else, a broken backup path never takes the API down.
Verifying it's working
docker compose logs api | grep -i backup
ls -lh "${DB_BACKUP_HOST_PATH:-./backups}"/daily/
zcat "${DB_BACKUP_HOST_PATH:-./backups}"/last/crumb-latest.sql.gz | head -3
You should see a recent .sql.gz, a database backup written log line
(not a permissions warning), and output that looks like pg_dump SQL.
Off-host copies
The built-in job protects you against losing the database itself, a bad
migration, an accidental DROP TABLE, disk corruption. It does not
protect you against losing the whole host: the dumps still live on the
same box as the footage. For a single-box home install, that's a fine,
accepted posture if you've made your peace with "if the box dies, I've
lost the video anyway." If you want a copy that survives losing the host
entirely, pick one:
Point DB_BACKUP_HOST_PATH at a NAS or NFS mount. No extra container,
no new configuration, dumps land off-host the moment pg_dump finishes.
Or enable the optional backup-offsite service, an rclone sidecar
gated behind the offsite Compose profile, so it's absent from a stock
install unless you opt in:
docker compose --profile offsite up -d
It syncs DB_BACKUP_HOST_PATH outward to whatever remote you configure in
rclone.conf (SFTP, S3, another NAS, and roughly seventy other backends),
read-only against the local directory, so it only ever pushes, never
deletes or restores.
Restoring
The built-in job's dumps restore into an empty database, not one with the existing schema still in it:
docker compose stop recorder api
docker compose exec -T postgres psql -U "${POSTGRES_USER}" -d postgres -c \
"DROP DATABASE \"${POSTGRES_DB}\"; CREATE DATABASE \"${POSTGRES_DB}\" OWNER \"${POSTGRES_USER}\";"
gunzip -c "${DB_BACKUP_HOST_PATH:-./backups}/daily/<dump-file>.sql.gz" \
| docker compose exec -T postgres psql -U "${POSTGRES_USER}" -d "${POSTGRES_DB}"
docker compose up -d recorder api
Bringing recorder/api back up lets the recorder's startup
reconciliation re-index any segment files written after the restored
dump's timestamp. Run a restore drill on a spare host before you rely on
this in production, an untested backup isn't a backup.
Turning it off
Set DB_BACKUP_ENABLED=false and docker compose up -d api if you'd
rather run your own backup mechanism against the same database.