From 047f06f5783e6c4982cbbfb25fed1bda77542ef0 Mon Sep 17 00:00:00 2001 From: Tarek Lokal Date: Sat, 11 Jul 2026 16:18:42 +0200 Subject: [PATCH 1/3] feat(docker): add OpenSPP filestore backup (#66) Archive filestore alongside pg_dump when odoo_data is mounted on the backup container; document restore expectations in docker README. Co-authored-by: Cursor --- docker/README.md | 1 + docker/backup.sh | 22 ++++++++++++++++++++++ docker/docker-compose.nginx.yml | 1 + docker/docker-compose.production.yml | 1 + 4 files changed, 25 insertions(+) diff --git a/docker/README.md b/docker/README.md index 5f6cef072..552515283 100644 --- a/docker/README.md +++ b/docker/README.md @@ -217,6 +217,7 @@ The production stack includes automated PostgreSQL backups: - **Schedule:** Daily at 2am (configurable via `BACKUP_SCHEDULE`) - **Retention:** 7 daily, 4 weekly, 6 monthly - **Location:** `backup_data` Docker volume +- **Filestore:** When the `odoo_data` volume is mounted on the backup service, attachments under `/odoo_data/filestore/` are archived daily as `*_filestore_*.tar.gz` alongside the database dump To restore a backup: diff --git a/docker/backup.sh b/docker/backup.sh index 12f3c3e19..14e0de8fa 100755 --- a/docker/backup.sh +++ b/docker/backup.sh @@ -11,6 +11,7 @@ # Environment variables: # PGHOST, PGPORT, PGUSER, PGPASSWORD, PGDATABASE (standard PostgreSQL vars) # BACKUP_DIR (default: /backups) +# FILESTORE_SRC (default: /odoo_data/filestore/$PGDATABASE) # BACKUP_KEEP_DAYS (default: 7) # BACKUP_KEEP_WEEKS (default: 4) # BACKUP_KEEP_MONTHS (default: 6) @@ -52,15 +53,33 @@ ln -sf "${BACKUP_FILE}" "${DAILY_DIR}/${PGDATABASE:-openspp}_latest.dump" echo "[$(date -Iseconds)] Daily backup complete: ${BACKUP_FILE}" +# Filestore backup (attachments, documents) when odoo data volume is mounted +FILESTORE_SRC="${FILESTORE_SRC:-/odoo_data/filestore/${PGDATABASE:-openspp}}" +FILESTORE_BACKUP_FILE="${PGDATABASE:-openspp}_filestore_${TIMESTAMP}.tar.gz" +if [ -d "${FILESTORE_SRC}" ]; then + echo "[$(date -Iseconds)] Starting filestore backup from ${FILESTORE_SRC}..." + tar -czf "${DAILY_DIR}/${FILESTORE_BACKUP_FILE}" -C "$(dirname "${FILESTORE_SRC}")" "$(basename "${FILESTORE_SRC}")" + ln -sf "${FILESTORE_BACKUP_FILE}" "${DAILY_DIR}/${PGDATABASE:-openspp}_filestore_latest.tar.gz" + echo "[$(date -Iseconds)] Filestore backup complete: ${FILESTORE_BACKUP_FILE}" +else + echo "[$(date -Iseconds)] Filestore not found at ${FILESTORE_SRC}; skipping filestore backup" +fi + # Weekly backup (Sunday) if [ "${DAY_OF_WEEK}" = "7" ]; then cp "${DAILY_DIR}/${BACKUP_FILE}" "${WEEKLY_DIR}/" + if [ -f "${DAILY_DIR}/${FILESTORE_BACKUP_FILE}" ]; then + cp "${DAILY_DIR}/${FILESTORE_BACKUP_FILE}" "${WEEKLY_DIR}/" + fi echo "[$(date -Iseconds)] Weekly backup saved" fi # Monthly backup (1st of month) if [ "${DAY_OF_MONTH}" = "01" ]; then cp "${DAILY_DIR}/${BACKUP_FILE}" "${MONTHLY_DIR}/" + if [ -f "${DAILY_DIR}/${FILESTORE_BACKUP_FILE}" ]; then + cp "${DAILY_DIR}/${FILESTORE_BACKUP_FILE}" "${MONTHLY_DIR}/" + fi echo "[$(date -Iseconds)] Monthly backup saved" fi @@ -69,12 +88,15 @@ echo "[$(date -Iseconds)] Cleaning up old backups..." # Remove daily backups older than BACKUP_KEEP_DAYS find "${DAILY_DIR}" -name "*.dump" -type f -mtime +${BACKUP_KEEP_DAYS} -delete 2>/dev/null || true +find "${DAILY_DIR}" -name "*_filestore_*.tar.gz" -type f -mtime +${BACKUP_KEEP_DAYS} -delete 2>/dev/null || true # Remove weekly backups older than BACKUP_KEEP_WEEKS weeks find "${WEEKLY_DIR}" -name "*.dump" -type f -mtime +$((BACKUP_KEEP_WEEKS * 7)) -delete 2>/dev/null || true +find "${WEEKLY_DIR}" -name "*_filestore_*.tar.gz" -type f -mtime +$((BACKUP_KEEP_WEEKS * 7)) -delete 2>/dev/null || true # Remove monthly backups older than BACKUP_KEEP_MONTHS months (approximate: 30 days per month) find "${MONTHLY_DIR}" -name "*.dump" -type f -mtime +$((BACKUP_KEEP_MONTHS * 30)) -delete 2>/dev/null || true +find "${MONTHLY_DIR}" -name "*_filestore_*.tar.gz" -type f -mtime +$((BACKUP_KEEP_MONTHS * 30)) -delete 2>/dev/null || true # Report disk usage echo "[$(date -Iseconds)] Backup sizes:" diff --git a/docker/docker-compose.nginx.yml b/docker/docker-compose.nginx.yml index ff91c81fa..6211370f1 100644 --- a/docker/docker-compose.nginx.yml +++ b/docker/docker-compose.nginx.yml @@ -427,6 +427,7 @@ services: - ./backup.sh:/backup.sh:ro,z - ./backup-entrypoint.sh:/backup-entrypoint.sh:ro,z - backup_data:/backups:rw,z + - odoo_data:/odoo_data:ro,z networks: - openspp-prod restart: unless-stopped diff --git a/docker/docker-compose.production.yml b/docker/docker-compose.production.yml index 0eba74f2c..ab277f268 100644 --- a/docker/docker-compose.production.yml +++ b/docker/docker-compose.production.yml @@ -333,6 +333,7 @@ services: - ./backup.sh:/backup.sh:ro - ./backup-entrypoint.sh:/backup-entrypoint.sh:ro - backup_data:/backups + - odoo_data:/odoo_data:ro networks: - openspp-prod restart: always From d7a29a14c8df2275cd86b40d8802c36bb3ef9559 Mon Sep 17 00:00:00 2001 From: Tarek Chehahde <138624319+Tarekchehahde@users.noreply.github.com> Date: Sat, 19 Sep 2026 10:38:23 +0200 Subject: [PATCH 2/3] fix(docker): address filestore backup review feedback Make filestore backup opt-in, harden tar under set -e, add retention knobs/flock/compress toggle, and document restore + sizing. Co-authored-by: Cursor --- docker/.env.production.example | 9 ++++ docker/README.md | 27 +++++++++--- docker/backup.sh | 64 ++++++++++++++++++++++------ docker/docker-compose.nginx.yml | 7 +++ docker/docker-compose.production.yml | 7 +++ 5 files changed, 97 insertions(+), 17 deletions(-) diff --git a/docker/.env.production.example b/docker/.env.production.example index fad0c3821..0e1c21d8e 100644 --- a/docker/.env.production.example +++ b/docker/.env.production.example @@ -146,6 +146,15 @@ FLUENTD_LOG_PATH=./fluentd/log # Default: daily at 2am BACKUP_SCHEDULE=0 2 * * * +# Filestore backup (opt-in). Full copies grow as KEEP_* × filestore size — +# suitable for small/mid on-prem; large deployments prefer object storage + snapshots. +BACKUP_FILESTORE=false +BACKUP_FILESTORE_COMPRESS=false +# FILESTORE_SRC=/odoo_data/filestore/openspp +# BACKUP_FILESTORE_KEEP_DAYS=7 +# BACKUP_FILESTORE_KEEP_WEEKS=4 +# BACKUP_FILESTORE_KEEP_MONTHS=6 + # ============================================================================= # ANTIVIRUS (enable with COMPOSE_PROFILES=https,clamav) # ============================================================================= diff --git a/docker/README.md b/docker/README.md index 552515283..63094068f 100644 --- a/docker/README.md +++ b/docker/README.md @@ -215,20 +215,37 @@ DATABASE_URL=postgres://user:password@hostname:5432/openspp?sslmode=require The production stack includes automated PostgreSQL backups: - **Schedule:** Daily at 2am (configurable via `BACKUP_SCHEDULE`) -- **Retention:** 7 daily, 4 weekly, 6 monthly +- **Retention:** 7 daily, 4 weekly, 6 monthly (DB dumps via `BACKUP_KEEP_*`) - **Location:** `backup_data` Docker volume -- **Filestore:** When the `odoo_data` volume is mounted on the backup service, attachments under `/odoo_data/filestore/` are archived daily as `*_filestore_*.tar.gz` alongside the database dump +- **Filestore:** Opt-in via `BACKUP_FILESTORE=true`. When enabled, attachments under `FILESTORE_SRC` (default `/odoo_data/filestore/`) are archived alongside the dump as `*_filestore_*.tar` (or `.tar.gz` if `BACKUP_FILESTORE_COMPRESS=true`). Filestore retention uses `BACKUP_FILESTORE_KEEP_*` (defaults match `BACKUP_KEEP_*`). Plan disk as roughly `KEEP_* × filestore size` — nightly full copies do not dedupe. +- **Consistency:** Dump then filestore is crash-consistent, not a true point-in-time pair. A genuinely consistent restore needs a volume snapshot or a brief Odoo stop. +- **Data protection:** `backup_data` holds beneficiary documents in the clear (same class of exposure as the DB dump). Encrypt the volume at rest in production. -To restore a backup: +To restore a backup (pair dump and filestore files that share the same `TIMESTAMP`): ```bash # List backups -docker compose -f docker/docker-compose.production.yml exec backup ls -la /backups +docker compose -f docker/docker-compose.production.yml exec backup ls -la /backups/daily # Restore (stop services first) docker compose -f docker/docker-compose.production.yml stop odoo queue-worker + +# 1) Database (pg_dump -Fc custom format; filenames use underscores) docker compose -f docker/docker-compose.production.yml exec db \ - pg_restore -U odoo -d openspp /backups/daily/openspp-YYYYMMDD-HHMMSS.sql.gz + pg_restore -U odoo -d openspp --clean --if-exists \ + /backups/daily/openspp_YYYYMMDD_HHMMSS.dump + +# 2) Filestore (same timestamp as the dump; adjust extension if compressed) +docker compose -f docker/docker-compose.production.yml run --rm --no-deps \ + -v odoo_data:/odoo_data \ + -v backup_data:/backups:ro \ + backup sh -c ' + rm -rf /odoo_data/filestore/openspp + mkdir -p /odoo_data/filestore + tar -xf /backups/daily/openspp_filestore_YYYYMMDD_HHMMSS.tar -C /odoo_data/filestore + chown -R 101:101 /odoo_data/filestore/openspp + ' + docker compose -f docker/docker-compose.production.yml start odoo queue-worker ``` diff --git a/docker/backup.sh b/docker/backup.sh index 14e0de8fa..29d144dd8 100755 --- a/docker/backup.sh +++ b/docker/backup.sh @@ -11,10 +11,13 @@ # Environment variables: # PGHOST, PGPORT, PGUSER, PGPASSWORD, PGDATABASE (standard PostgreSQL vars) # BACKUP_DIR (default: /backups) +# BACKUP_FILESTORE (default: false) — set true to archive Odoo filestore # FILESTORE_SRC (default: /odoo_data/filestore/$PGDATABASE) +# BACKUP_FILESTORE_COMPRESS (default: false) — gzip filestore archives # BACKUP_KEEP_DAYS (default: 7) # BACKUP_KEEP_WEEKS (default: 4) # BACKUP_KEEP_MONTHS (default: 6) +# BACKUP_FILESTORE_KEEP_DAYS / _WEEKS / _MONTHS (default: same as BACKUP_KEEP_*) set -e @@ -23,6 +26,11 @@ BACKUP_DIR="${BACKUP_DIR:-/backups}" BACKUP_KEEP_DAYS="${BACKUP_KEEP_DAYS:-7}" BACKUP_KEEP_WEEKS="${BACKUP_KEEP_WEEKS:-4}" BACKUP_KEEP_MONTHS="${BACKUP_KEEP_MONTHS:-6}" +BACKUP_FILESTORE="${BACKUP_FILESTORE:-false}" +BACKUP_FILESTORE_COMPRESS="${BACKUP_FILESTORE_COMPRESS:-false}" +BACKUP_FILESTORE_KEEP_DAYS="${BACKUP_FILESTORE_KEEP_DAYS:-${BACKUP_KEEP_DAYS}}" +BACKUP_FILESTORE_KEEP_WEEKS="${BACKUP_FILESTORE_KEEP_WEEKS:-${BACKUP_KEEP_WEEKS}}" +BACKUP_FILESTORE_KEEP_MONTHS="${BACKUP_FILESTORE_KEEP_MONTHS:-${BACKUP_KEEP_MONTHS}}" # Directories DAILY_DIR="${BACKUP_DIR}/daily" @@ -32,6 +40,10 @@ MONTHLY_DIR="${BACKUP_DIR}/monthly" # Create directories mkdir -p "${DAILY_DIR}" "${WEEKLY_DIR}" "${MONTHLY_DIR}" +# Skip overlapping runs (filestore archives can outlast the cron interval) +exec 9>"${BACKUP_DIR}/.backup.lock" +flock -n 9 || { echo "[$(date -Iseconds)] Backup already running; skipping"; exit 0; } + # Timestamp TIMESTAMP=$(date +%Y%m%d_%H%M%S) DATE=$(date +%Y%m%d) @@ -53,22 +65,47 @@ ln -sf "${BACKUP_FILE}" "${DAILY_DIR}/${PGDATABASE:-openspp}_latest.dump" echo "[$(date -Iseconds)] Daily backup complete: ${BACKUP_FILE}" -# Filestore backup (attachments, documents) when odoo data volume is mounted +# Filestore backup (attachments, documents) — opt-in. +# Dump runs before the archive on purpose: an attachment written between dump and +# tar leaves an orphan file that Odoo's filestore GC reaps; the reverse order +# produces a DB row whose file was never captured (FileNotFoundError on restore). +# The pair is crash-consistent, not a true point-in-time snapshot. FILESTORE_SRC="${FILESTORE_SRC:-/odoo_data/filestore/${PGDATABASE:-openspp}}" -FILESTORE_BACKUP_FILE="${PGDATABASE:-openspp}_filestore_${TIMESTAMP}.tar.gz" -if [ -d "${FILESTORE_SRC}" ]; then - echo "[$(date -Iseconds)] Starting filestore backup from ${FILESTORE_SRC}..." - tar -czf "${DAILY_DIR}/${FILESTORE_BACKUP_FILE}" -C "$(dirname "${FILESTORE_SRC}")" "$(basename "${FILESTORE_SRC}")" - ln -sf "${FILESTORE_BACKUP_FILE}" "${DAILY_DIR}/${PGDATABASE:-openspp}_filestore_latest.tar.gz" - echo "[$(date -Iseconds)] Filestore backup complete: ${FILESTORE_BACKUP_FILE}" +FILESTORE_SRC="${FILESTORE_SRC%/}" +if [ "${BACKUP_FILESTORE_COMPRESS}" = "true" ]; then + FILESTORE_EXT="tar.gz" + FILESTORE_TAR_FLAGS="-czf" else + FILESTORE_EXT="tar" + FILESTORE_TAR_FLAGS="-cf" +fi +FILESTORE_BACKUP_FILE="${PGDATABASE:-openspp}_filestore_${TIMESTAMP}.${FILESTORE_EXT}" +FILESTORE_BACKUP_CREATED=0 + +if [ "${BACKUP_FILESTORE}" != "true" ]; then + echo "[$(date -Iseconds)] Filestore backup disabled (BACKUP_FILESTORE=${BACKUP_FILESTORE})" +elif [ ! -d "${FILESTORE_SRC}" ]; then echo "[$(date -Iseconds)] Filestore not found at ${FILESTORE_SRC}; skipping filestore backup" +else + echo "[$(date -Iseconds)] Starting filestore backup from ${FILESTORE_SRC}..." + # tar exits 1 when Odoo's filestore GC unlinks a file mid-archive. Running it + # as an `if` condition keeps `set -e` from skipping the retention pass below. + if tar ${FILESTORE_TAR_FLAGS} "${DAILY_DIR}/${FILESTORE_BACKUP_FILE}.part" \ + -C "$(dirname "${FILESTORE_SRC}")" "$(basename "${FILESTORE_SRC}")"; then + mv "${DAILY_DIR}/${FILESTORE_BACKUP_FILE}.part" "${DAILY_DIR}/${FILESTORE_BACKUP_FILE}" + ln -sf "${FILESTORE_BACKUP_FILE}" "${DAILY_DIR}/${PGDATABASE:-openspp}_filestore_latest.${FILESTORE_EXT}" + FILESTORE_BACKUP_CREATED=1 + echo "[$(date -Iseconds)] Filestore backup complete: ${FILESTORE_BACKUP_FILE}" + else + rm -f "${DAILY_DIR}/${FILESTORE_BACKUP_FILE}.part" + echo "[$(date -Iseconds)] WARNING: filestore backup failed; database dump kept" + fi fi # Weekly backup (Sunday) if [ "${DAY_OF_WEEK}" = "7" ]; then cp "${DAILY_DIR}/${BACKUP_FILE}" "${WEEKLY_DIR}/" - if [ -f "${DAILY_DIR}/${FILESTORE_BACKUP_FILE}" ]; then + if [ "${FILESTORE_BACKUP_CREATED}" = "1" ] && [ -f "${DAILY_DIR}/${FILESTORE_BACKUP_FILE}" ]; then cp "${DAILY_DIR}/${FILESTORE_BACKUP_FILE}" "${WEEKLY_DIR}/" fi echo "[$(date -Iseconds)] Weekly backup saved" @@ -77,7 +114,7 @@ fi # Monthly backup (1st of month) if [ "${DAY_OF_MONTH}" = "01" ]; then cp "${DAILY_DIR}/${BACKUP_FILE}" "${MONTHLY_DIR}/" - if [ -f "${DAILY_DIR}/${FILESTORE_BACKUP_FILE}" ]; then + if [ "${FILESTORE_BACKUP_CREATED}" = "1" ] && [ -f "${DAILY_DIR}/${FILESTORE_BACKUP_FILE}" ]; then cp "${DAILY_DIR}/${FILESTORE_BACKUP_FILE}" "${MONTHLY_DIR}/" fi echo "[$(date -Iseconds)] Monthly backup saved" @@ -88,15 +125,18 @@ echo "[$(date -Iseconds)] Cleaning up old backups..." # Remove daily backups older than BACKUP_KEEP_DAYS find "${DAILY_DIR}" -name "*.dump" -type f -mtime +${BACKUP_KEEP_DAYS} -delete 2>/dev/null || true -find "${DAILY_DIR}" -name "*_filestore_*.tar.gz" -type f -mtime +${BACKUP_KEEP_DAYS} -delete 2>/dev/null || true # Remove weekly backups older than BACKUP_KEEP_WEEKS weeks find "${WEEKLY_DIR}" -name "*.dump" -type f -mtime +$((BACKUP_KEEP_WEEKS * 7)) -delete 2>/dev/null || true -find "${WEEKLY_DIR}" -name "*_filestore_*.tar.gz" -type f -mtime +$((BACKUP_KEEP_WEEKS * 7)) -delete 2>/dev/null || true # Remove monthly backups older than BACKUP_KEEP_MONTHS months (approximate: 30 days per month) find "${MONTHLY_DIR}" -name "*.dump" -type f -mtime +$((BACKUP_KEEP_MONTHS * 30)) -delete 2>/dev/null || true -find "${MONTHLY_DIR}" -name "*_filestore_*.tar.gz" -type f -mtime +$((BACKUP_KEEP_MONTHS * 30)) -delete 2>/dev/null || true + +# Filestore retention stays outside the BACKUP_FILESTORE guard so archives age out +# even after the feature is turned back off. +find "${DAILY_DIR}" -name "*_filestore_*.tar*" -type f -mtime +${BACKUP_FILESTORE_KEEP_DAYS} -delete 2>/dev/null || true +find "${WEEKLY_DIR}" -name "*_filestore_*.tar*" -type f -mtime +$((BACKUP_FILESTORE_KEEP_WEEKS * 7)) -delete 2>/dev/null || true +find "${MONTHLY_DIR}" -name "*_filestore_*.tar*" -type f -mtime +$((BACKUP_FILESTORE_KEEP_MONTHS * 30)) -delete 2>/dev/null || true # Report disk usage echo "[$(date -Iseconds)] Backup sizes:" diff --git a/docker/docker-compose.nginx.yml b/docker/docker-compose.nginx.yml index 6211370f1..30fdfa3eb 100644 --- a/docker/docker-compose.nginx.yml +++ b/docker/docker-compose.nginx.yml @@ -423,6 +423,13 @@ services: BACKUP_KEEP_DAYS: ${BACKUP_KEEP_DAYS:-7} BACKUP_KEEP_WEEKS: ${BACKUP_KEEP_WEEKS:-4} BACKUP_KEEP_MONTHS: ${BACKUP_KEEP_MONTHS:-6} + # Filestore backup (opt-in; see docker/README.md for sizing) + BACKUP_FILESTORE: ${BACKUP_FILESTORE:-false} + BACKUP_FILESTORE_COMPRESS: ${BACKUP_FILESTORE_COMPRESS:-false} + FILESTORE_SRC: ${FILESTORE_SRC:-/odoo_data/filestore/openspp} + BACKUP_FILESTORE_KEEP_DAYS: ${BACKUP_FILESTORE_KEEP_DAYS:-${BACKUP_KEEP_DAYS:-7}} + BACKUP_FILESTORE_KEEP_WEEKS: ${BACKUP_FILESTORE_KEEP_WEEKS:-${BACKUP_KEEP_WEEKS:-4}} + BACKUP_FILESTORE_KEEP_MONTHS: ${BACKUP_FILESTORE_KEEP_MONTHS:-${BACKUP_KEEP_MONTHS:-6}} volumes: - ./backup.sh:/backup.sh:ro,z - ./backup-entrypoint.sh:/backup-entrypoint.sh:ro,z diff --git a/docker/docker-compose.production.yml b/docker/docker-compose.production.yml index ab277f268..5c84b125c 100644 --- a/docker/docker-compose.production.yml +++ b/docker/docker-compose.production.yml @@ -329,6 +329,13 @@ services: BACKUP_KEEP_DAYS: ${BACKUP_KEEP_DAYS:-7} BACKUP_KEEP_WEEKS: ${BACKUP_KEEP_WEEKS:-4} BACKUP_KEEP_MONTHS: ${BACKUP_KEEP_MONTHS:-6} + # Filestore backup (opt-in; see docker/README.md for sizing) + BACKUP_FILESTORE: ${BACKUP_FILESTORE:-false} + BACKUP_FILESTORE_COMPRESS: ${BACKUP_FILESTORE_COMPRESS:-false} + FILESTORE_SRC: ${FILESTORE_SRC:-/odoo_data/filestore/openspp} + BACKUP_FILESTORE_KEEP_DAYS: ${BACKUP_FILESTORE_KEEP_DAYS:-${BACKUP_KEEP_DAYS:-7}} + BACKUP_FILESTORE_KEEP_WEEKS: ${BACKUP_FILESTORE_KEEP_WEEKS:-${BACKUP_KEEP_WEEKS:-4}} + BACKUP_FILESTORE_KEEP_MONTHS: ${BACKUP_FILESTORE_KEEP_MONTHS:-${BACKUP_KEEP_MONTHS:-6}} volumes: - ./backup.sh:/backup.sh:ro - ./backup-entrypoint.sh:/backup-entrypoint.sh:ro From cc4f5476e5016965b46aadeea973f926f9aa0506 Mon Sep 17 00:00:00 2001 From: Tarek Chehahde <138624319+Tarekchehahde@users.noreply.github.com> Date: Sat, 19 Sep 2026 15:35:41 +0200 Subject: [PATCH 3/3] style(docker): wrap README backup bullets for pre-commit Co-authored-by: Cursor --- docker/README.md | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/docker/README.md b/docker/README.md index 63094068f..7d869dc5f 100644 --- a/docker/README.md +++ b/docker/README.md @@ -217,9 +217,15 @@ The production stack includes automated PostgreSQL backups: - **Schedule:** Daily at 2am (configurable via `BACKUP_SCHEDULE`) - **Retention:** 7 daily, 4 weekly, 6 monthly (DB dumps via `BACKUP_KEEP_*`) - **Location:** `backup_data` Docker volume -- **Filestore:** Opt-in via `BACKUP_FILESTORE=true`. When enabled, attachments under `FILESTORE_SRC` (default `/odoo_data/filestore/`) are archived alongside the dump as `*_filestore_*.tar` (or `.tar.gz` if `BACKUP_FILESTORE_COMPRESS=true`). Filestore retention uses `BACKUP_FILESTORE_KEEP_*` (defaults match `BACKUP_KEEP_*`). Plan disk as roughly `KEEP_* × filestore size` — nightly full copies do not dedupe. -- **Consistency:** Dump then filestore is crash-consistent, not a true point-in-time pair. A genuinely consistent restore needs a volume snapshot or a brief Odoo stop. -- **Data protection:** `backup_data` holds beneficiary documents in the clear (same class of exposure as the DB dump). Encrypt the volume at rest in production. +- **Filestore:** Opt-in via `BACKUP_FILESTORE=true`. When enabled, attachments under + `FILESTORE_SRC` (default `/odoo_data/filestore/`) are archived alongside the + dump as `*_filestore_*.tar` (or `.tar.gz` if `BACKUP_FILESTORE_COMPRESS=true`). + Filestore retention uses `BACKUP_FILESTORE_KEEP_*` (defaults match `BACKUP_KEEP_*`). + Plan disk as roughly `KEEP_* × filestore size` — nightly full copies do not dedupe. +- **Consistency:** Dump then filestore is crash-consistent, not a true point-in-time + pair. A genuinely consistent restore needs a volume snapshot or a brief Odoo stop. +- **Data protection:** `backup_data` holds beneficiary documents in the clear (same + class of exposure as the DB dump). Encrypt the volume at rest in production. To restore a backup (pair dump and filestore files that share the same `TIMESTAMP`):