-
Notifications
You must be signed in to change notification settings - Fork 1
Backup and Recovery
Roomer's persistent state lives in two places, both of which need backing up:
- PostgreSQL — every booking, user, asset, floor plan record, webhook config, etc.
-
The uploads volume (
uploads_datain Compose,/app/uploadsin the container) — floor plan images/PDFs/DXF files and lease documents.
Losing the database loses all structured data. Losing the uploads volume leaves every floor plan and lease document referenced-but-missing — the database still has the file paths, just nothing behind them. Back up both, on the same schedule, so a restore always pairs a DB snapshot with the uploads that existed at the same point in time.
This page covers the Docker Compose deployment (see [[Production Deployment]]) as the primary case, with a section for [[Kubernetes Deployment]] below.
docker compose exec -T postgres pg_dump \
-U "${POSTGRES_USER:-roomer}" \
-d "${POSTGRES_DB:-roomer}" \
--format=custom \
> roomer-$(date +%Y%m%d-%H%M%S).dump--format=custom supports pg_restore's selective/parallel restore and is
already compressed. (The Production Deployment 0.3.0 upgrade notes use a
plain-SQL pg_dump -U roomer roomer > file.sql for a quick pre-upgrade
snapshot — either format works for a one-off; --format=custom is the
better default for a scheduled backup you may need to restore later.)
Schedule it with cron or a systemd timer on the Docker host:
# /etc/cron.d/roomer-backup — daily at 02:00
0 2 * * * root cd /path/to/roomer && docker compose exec -T postgres pg_dump -U roomer -d roomer --format=custom > /backups/roomer-$(date +\%Y\%m\%d).dump 2>> /var/log/roomer-backup.logShip the resulting file off the Docker host — a local-disk-only backup doesn't survive a host failure.
A daily pg_dump risks losing up to a day of data if the DB fails right
before the next scheduled dump. If that's not acceptable, enable WAL
archiving so you can replay to any point in time instead of just the last
dump. This needs config changes the default compose file doesn't make —
somewhere to put WAL segments:
# docker-compose.yml — postgres service
services:
postgres:
volumes:
- postgres_data:/var/lib/postgresql
- wal_archive:/wal-archive # add
command: >
postgres
-c wal_level=replica
-c archive_mode=on
-c archive_command='test ! -f /wal-archive/%f && cp %p /wal-archive/%f'
volumes:
postgres_data:
uploads_data:
wal_archive: # addWith this in place, a full PITR setup is a periodic base backup
(pg_basebackup — a physical copy of the data directory, not pg_dump)
plus the continuously-archived WAL segments; restoring starts Postgres from
the base backup with a recovery_target_time and replays WAL up to that
point. This is meaningfully more operational complexity than pg_dump —
only take it on if your RPO actually requires it. For most self-hosted
Roomer instances, a daily pg_dump plus the uploads backup below is enough.
# Find the actual volume name first — it's prefixed with your compose
# project name, which defaults to the directory docker-compose.yml lives in
# ("roomer" if you cloned the repo as-is, but check rather than assume):
docker volume ls --filter name=uploads_data
MOUNTPOINT=$(docker volume inspect roomer_uploads_data --format '{{ .Mountpoint }}')
rsync -a "$MOUNTPOINT/" /backups/roomer-uploads/
# Or off-host via rclone, to any of its 70+ supported backends
rclone sync "$MOUNTPOINT/" remote:roomer-backups/uploads/If durability matters more than the simplicity of a local volume, point Roomer's file storage at S3-compatible storage instead of a local path and let the storage backend's own durability/replication do the work — see Configuration Reference for current file-storage backend options before relying on this, since they may have changed since this page was written.
The chart runs Postgres as a StatefulSet (via the postgresql.enabled
bitnami sub-chart — see Kubernetes Deployment) with its own PVC, plus a
separate PVC for uploads.
# Postgres dump — replace <release> with your Helm release name.
# Connect as the app's own DB user (POSTGRES_USER on the postgres pod's env,
# "roomer" by default) — NOT the "postgres" superuser. This chart's bitnami
# postgres sub-chart doesn't enable password auth for "postgres" over the
# local socket, only for the app user; `pg_dump -U postgres` fails with
# "password authentication failed" even with the correct postgres-password
# secret. `-U <POSTGRES_USER>` with the `password` secret key works.
kubectl exec -n roomer <release>-postgresql-0 -- \
bash -c 'PGPASSWORD="$(cat /opt/bitnami/postgresql/secrets/password)" pg_dump -U roomer -d roomer --format=custom' \
> roomer-$(date +%Y%m%d-%H%M%S).dump
# Uploads — copy out of the PVC via the running API pod
kubectl cp -n roomer <api-pod-name>:/app/uploads ./roomer-uploads-backup/If you already run Velero in the cluster, hook
Roomer's backups into your existing Velero schedule instead of a separate
cron job. A raw PVC snapshot of a live Postgres data directory isn't safely
restorable on its own — Velero's pattern is a
pre-backup hook that runs
pg_dump to a file just before the snapshot/restic backup captures the
volume, so what's captured is always a consistent dump rather than a
mid-write snapshot. Add a pre.hook.backup.velero.io/command annotation
(running the pg_dump command above) via the postgres sub-chart's
postgresql.primary.podAnnotations value, and a
backup.velero.io/backup-volumes annotation on the uploads volume's pod.
This isn't a first-class values.yaml toggle in the chart — most
self-hosted Roomer deployments don't run Velero, and the annotations above
are enough to wire in for the ones that do.
-
Stop the API so nothing writes to the DB mid-restore:
docker compose stop api # Compose kubectl scale deploy roomer-api -n roomer --replicas=0 # Kubernetes
-
Restore the database into a fresh/empty DB:
docker compose exec -T postgres pg_restore \ -U "${POSTGRES_USER:-roomer}" -d "${POSTGRES_DB:-roomer}" \ --clean --if-exists < roomer-20260101-020000.dump
--clean --if-existsdrops existing objects first so the restore doesn't collide with whatever's currently in the target DB. -
Restore the uploads volume — copy the backed-up files back into the
volume's mountpoint (or
docker cp/kubectl cpinto the running container if the volume itself wasn't recreated). -
Start the API back up and watch the logs — it runs
prisma migrate deployon boot, which no-ops if the restored DB is already current, or applies any migrations newer than the backup if you're restoring an older dump onto a newer image. - Verify data integrity — log in, spot-check a few floor plans actually render (confirms the uploads restore matches the DB restore), check the most recent bookings/audit log entries match what you'd expect from the backup's timestamp.
-
Re-seed only if restoring into a genuinely empty environment (no
backup at all) — the idempotent seed (
seed.js) that creates the admin account runs on every boot and doesn't touch pre-existing rows, so it's safe to leave running against a freshly-restored DB regardless.
Scale this to how much data loss your organisation can tolerate and how large the deployment is. A reasonable starting point:
| Frequency | Retention | |
|---|---|---|
pg_dump |
Daily | 30 days local, 90 days offsite |
| Uploads sync | Daily (paired with the DB dump) | Same as DB — mismatched ages between the two make restores confusing |
| Base backup + WAL (if using PITR) | Weekly base backup, continuous WAL | 7–14 days of WAL |
Test a restore periodically — a backup you've never restored from is a hope, not a plan. Quarterly is a reasonable cadence for most deployments; do it in a scratch environment, not production.
Install & Run
- Getting Started
- Development Setup
- Production Deployment
- Kubernetes Deployment
- Backup and Recovery
- TLS Configuration
- Configuration Reference
Using Roomer
- Buildings and Floors
- Zones and Assets
- Users, Groups and Permissions
- Booking and Queue
- Bulk CSV Import
- Enterprise Auth
- Email Notifications
- Webhooks
- Reports and Leases
Developer