Backup & Restore¶
A complete HEMMS backup has two parts: the Postgres database (records, configuration, audit trail) and the Odoo filestore (attachments — scanned ปจป. PDFs, signed service contracts, equipment photos). If you only back up one of the two, you cannot fully restore.
This page covers what make backup-private actually does, how to add the filestore to that backup, how to restore, and the cadence Trinity Roots recommends for production deployments.
What gets backed up¶
| Asset | Lives in | What's lost if missing |
|---|---|---|
| Database | Postgres volume (db_private_data / db_gov_data) | All records: equipment, requests, contracts, audit log, users, permissions |
| Filestore | Odoo volume (odoo_private_data / odoo_gov_data), path /var/lib/odoo/.local/share/Odoo/filestore/<dbname>/ | Every attachment: signed PDFs, photos, imported XLSX files. The DB references these by hash — without the filestore, attachment downloads return 404 |
addons/ source | Git repo on disk | Custom code — already in version control on GitHub. Not part of the backup. |
config/*.conf | Repo on disk | Admin password, DB credentials — already in version control. Not part of the backup. |
The Makefile backup target captures the database only
make backup-private runs pg_dump and does not copy the filestore. That's fine for short-lived demo databases (the seed demo data fills the filestore on install). It is not enough for production. See Adding the filestore to the backup below.
Database backup — make backup-private¶
The headline command:
This invokes scripts/backup_db.sh which runs pg_dump inside the Postgres container and pipes the output through gzip:
docker exec hemms_db_private pg_dump -U odoo_private hemms_private \
| gzip > ./backups/private_<timestamp>.sql.gz
Output landing point:
The script creates ./backups/ if it doesn't exist. Files are timestamped to the second, so two backups taken in the same minute won't collide.
| Sandbox | Container | DB user | DB name | Backup filename |
|---|---|---|---|---|
private | hemms_db_private | odoo_private | hemms_private | private_<ts>.sql.gz |
gov | hemms_db_gov | odoo_gov | hemms_gov | gov_<ts>.sql.gz |
Adding the filestore to the backup¶
For production, wrap make backup-private with a filestore copy:
#!/usr/bin/env bash
set -euo pipefail
TS=$(date +%Y%m%d_%H%M%S)
SANDBOX=private
DB=hemms_private
CONTAINER_ODOO=hemms_odoo_private
# 1. DB dump (via the Makefile target)
make backup-${SANDBOX}
# 2. Filestore tarball — read from inside the Odoo container
docker exec ${CONTAINER_ODOO} tar -czf - \
-C /var/lib/odoo/.local/share/Odoo/filestore ${DB} \
> ./backups/${SANDBOX}_filestore_${TS}.tar.gz
echo "==> Filestore backup complete: ./backups/${SANDBOX}_filestore_${TS}.tar.gz"
Now each backup is two files with matching timestamps:
Keep them together — they must be restored as a pair.
Restore procedure¶
A clean restore drops the existing database, recreates it, replays the dump, and unpacks the filestore.
Restore is destructive
Restore replaces the live database. Take a fresh backup first (make backup-private) so you can roll forward if the restore goes wrong. Stop Odoo before restoring — concurrent writes during restore will corrupt the database.
Step 1 — Stop Odoo, keep Postgres up¶
Step 2 — Drop and recreate the database¶
docker exec -it hemms_db_private psql -U odoo_private -d postgres \
-c "DROP DATABASE IF EXISTS hemms_private;"
docker exec -it hemms_db_private psql -U odoo_private -d postgres \
-c "CREATE DATABASE hemms_private OWNER odoo_private;"
Step 3 — Replay the SQL dump¶
gunzip -c ./backups/private_20260523_020005.sql.gz \
| docker exec -i hemms_db_private psql -U odoo_private -d hemms_private
This restores every row but not the filestore.
Step 4 — Restore the filestore¶
# Wipe the existing filestore for this DB first
docker exec hemms_odoo_private rm -rf \
/var/lib/odoo/.local/share/Odoo/filestore/hemms_private
# Unpack the tarball into the same parent directory
docker exec -i hemms_odoo_private tar -xzf - \
-C /var/lib/odoo/.local/share/Odoo/filestore \
< ./backups/private_filestore_20260523_020005.tar.gz
Step 5 — Start Odoo, verify¶
Open the UI, log in, and spot-check:
- Maintenance → Equipment — record counts match the source DB.
- Open a signed service contract and click the attachment — it should download (proves the filestore is intact).
- HA Report → Annual Reports — open last year's report and verify the PDF renders.
If attachments 404, the filestore restore missed a step — repeat step 4.
Recommended cadence¶
| When | What |
|---|---|
| Nightly | DB + filestore backup. Cron the wrapped script. Retain 14 daily backups. |
| Weekly | Promote one nightly to weekly. Retain 8 weekly backups (≈ 2 months). |
| Monthly | Promote one weekly to monthly. Retain 12 monthly backups (≈ 1 year). |
| Before every module install / upgrade | make backup-private + filestore tarball. Manual, on-demand. |
| Before every restore test | Snapshot the live state first. |
| Annually — disaster recovery drill | Full restore on a non-prod host. Measure RTO. Document the result. |
Backups should be copied off the Docker host to object storage (S3, GCS, or an NFS share on a different machine). A backup on the same disk as the live DB does not survive disk failure.
Disaster recovery drill¶
Schedule annually. The drill answers two questions: Does the restore procedure actually work? and How long does it take?
- Pick a non-prod host (a laptop or a cheap VM).
- Clone the repo,
make up-private, but do notmake install-private. You want an empty DB to restore into. - Copy the most recent off-site backup pair to
./backups/. - Run the restore procedure above end-to-end. Time it.
- Verify the spot-checks in Step 5.
- Record:
- RTO (recovery time objective) — how long the restore took
- RPO (recovery point objective) — gap between last backup and the drill time
- Any steps that failed or surprised you — feed back into this doc
Keep the drill report with your HA Thailand documentation. HA reviewers look for evidence that disaster recovery is rehearsed, not just documented.
Common pitfalls¶
pg_dump version mismatch
If you upgrade Postgres on the host or in the container, an old dump file can still be replayed — but a new dump file cannot be replayed on an older Postgres. Match the dump-time and restore-time major versions (currently postgres:15).
Filestore disk usage grows silently
Every signed PDF, photo, and imported XLSX lives in the filestore. Monitor docker exec hemms_odoo_private du -sh /var/lib/odoo/.local/share/Odoo/filestore/ monthly. If a hospital uploads 4K equipment photos, the filestore can grow faster than the DB.
Encrypted backups for off-site storage
Patient-adjacent data sits inside HEMMS (equipment serial numbers linked to wards, attachments). Encrypt backups at rest before shipping them off the host — gpg --symmetric backup.sql.gz is the minimum.
Related pages¶
- Installation — initial deployment
- Upgrade Guide — always backup before upgrading
- Multi-company / Multi-hospital Setup — per-hospital backup boundaries