Update backup script to include everything needed to do a bare metal restore of the new V6 server
Quality Gate / gate (push) Failing after 49s

This commit is contained in:
Jim Lancaster
2026-08-26 12:21:40 -05:00
parent 2c6ef46f5f
commit 89ed83239e
5 changed files with 88 additions and 14 deletions
+1 -1
View File
@@ -182,7 +182,7 @@ See `docs/data_migration.md` for commands and cutover steps.
## Backup and restore workflow ## Backup and restore workflow
Production PostgreSQL backup/restore steps are documented in `docs/backup_restore.md`. Production backup/restore (PostgreSQL + uploads + deployment config) steps are documented in `docs/backup_restore.md`.
## Destructive test procedure (with data backup) ## Destructive test procedure (with data backup)
+38 -5
View File
@@ -8,19 +8,52 @@ RETENTION_DAYS="${BACKUP_RETENTION_DAYS:-14}"
SYNOLOGY_BACKUP_DIR="${SYNOLOGY_BACKUP_DIR:-}" SYNOLOGY_BACKUP_DIR="${SYNOLOGY_BACKUP_DIR:-}"
timestamp="$(date -u +%Y%m%d-%H%M%S)" timestamp="$(date -u +%Y%m%d-%H%M%S)"
backup_file="postgres-${timestamp}.dump" postgres_file="postgres-${timestamp}.dump"
uploads_file="uploads-${timestamp}.tar.gz"
config_file="config-${timestamp}.tar.gz"
manifest_file="backup-${timestamp}.manifest"
mkdir -p "${BACKUP_DIR}" mkdir -p "${BACKUP_DIR}"
docker compose --env-file "${ENV_FILE}" -f "${COMPOSE_FILE}" exec -T postgres sh -lc \ docker compose --env-file "${ENV_FILE}" -f "${COMPOSE_FILE}" exec -T postgres sh -lc \
"PGPASSWORD=\"\$POSTGRES_PASSWORD\" pg_dump -U \"\$POSTGRES_USER\" -d \"\$POSTGRES_DB\" -Fc" \ "PGPASSWORD=\"\$POSTGRES_PASSWORD\" pg_dump -U \"\$POSTGRES_USER\" -d \"\$POSTGRES_DB\" -Fc" \
> "${BACKUP_DIR}/${backup_file}" > "${BACKUP_DIR}/${postgres_file}"
find "${BACKUP_DIR}" -type f -name 'postgres-*.dump' -mtime +"${RETENTION_DAYS}" -delete docker compose --env-file "${ENV_FILE}" -f "${COMPOSE_FILE}" run --rm --no-deps --entrypoint sh app -lc \
"tar -C /app/uploads -czf - ." \
> "${BACKUP_DIR}/${uploads_file}"
tar -C . -czf "${BACKUP_DIR}/${config_file}" \
"${ENV_FILE}" \
"${COMPOSE_FILE}" \
deploy/cloudflared/config.yml
cat > "${BACKUP_DIR}/${manifest_file}" <<EOF
created_at_utc=${timestamp}
postgres_dump=${postgres_file}
uploads_archive=${uploads_file}
config_archive=${config_file}
compose_file=${COMPOSE_FILE}
env_file=${ENV_FILE}
EOF
find "${BACKUP_DIR}" -type f \( \
-name 'postgres-*.dump' -o \
-name 'uploads-*.tar.gz' -o \
-name 'config-*.tar.gz' -o \
-name 'backup-*.manifest' \
\) -mtime +"${RETENTION_DAYS}" -delete
if [ -n "${SYNOLOGY_BACKUP_DIR}" ]; then if [ -n "${SYNOLOGY_BACKUP_DIR}" ]; then
mkdir -p "${SYNOLOGY_BACKUP_DIR}" mkdir -p "${SYNOLOGY_BACKUP_DIR}"
cp "${BACKUP_DIR}/${backup_file}" "${SYNOLOGY_BACKUP_DIR}/${backup_file}" cp "${BACKUP_DIR}/${postgres_file}" "${SYNOLOGY_BACKUP_DIR}/${postgres_file}"
cp "${BACKUP_DIR}/${uploads_file}" "${SYNOLOGY_BACKUP_DIR}/${uploads_file}"
cp "${BACKUP_DIR}/${config_file}" "${SYNOLOGY_BACKUP_DIR}/${config_file}"
cp "${BACKUP_DIR}/${manifest_file}" "${SYNOLOGY_BACKUP_DIR}/${manifest_file}"
fi fi
echo "Created backup: ${BACKUP_DIR}/${backup_file}" echo "Created backup set:"
echo " ${BACKUP_DIR}/${postgres_file}"
echo " ${BACKUP_DIR}/${uploads_file}"
echo " ${BACKUP_DIR}/${config_file}"
echo " ${BACKUP_DIR}/${manifest_file}"
+31
View File
@@ -15,6 +15,22 @@ if [ ! -f "${dump_file}" ]; then
exit 1 exit 1
fi fi
backup_dir="$(dirname "${dump_file}")"
backup_name="$(basename "${dump_file}")"
timestamp="$(printf '%s' "${backup_name}" | sed -n 's/^postgres-\([0-9]\{8\}-[0-9]\{6\}\)\.dump$/\1/p')"
uploads_file=""
config_file=""
if [ -n "${timestamp}" ]; then
candidate_uploads="${backup_dir}/uploads-${timestamp}.tar.gz"
candidate_config="${backup_dir}/config-${timestamp}.tar.gz"
if [ -f "${candidate_uploads}" ]; then
uploads_file="${candidate_uploads}"
fi
if [ -f "${candidate_config}" ]; then
config_file="${candidate_config}"
fi
fi
docker compose --env-file "${ENV_FILE}" -f "${COMPOSE_FILE}" exec -T postgres sh -lc \ docker compose --env-file "${ENV_FILE}" -f "${COMPOSE_FILE}" exec -T postgres sh -lc \
"PGPASSWORD=\"\$POSTGRES_PASSWORD\" psql -U \"\$POSTGRES_USER\" -d postgres -c \"DROP DATABASE IF EXISTS \\\"\$POSTGRES_DB\\\";\"" "PGPASSWORD=\"\$POSTGRES_PASSWORD\" psql -U \"\$POSTGRES_USER\" -d postgres -c \"DROP DATABASE IF EXISTS \\\"\$POSTGRES_DB\\\";\""
@@ -24,4 +40,19 @@ docker compose --env-file "${ENV_FILE}" -f "${COMPOSE_FILE}" exec -T postgres sh
cat "${dump_file}" | docker compose --env-file "${ENV_FILE}" -f "${COMPOSE_FILE}" exec -T postgres sh -lc \ cat "${dump_file}" | docker compose --env-file "${ENV_FILE}" -f "${COMPOSE_FILE}" exec -T postgres sh -lc \
"PGPASSWORD=\"\$POSTGRES_PASSWORD\" pg_restore -U \"\$POSTGRES_USER\" -d \"\$POSTGRES_DB\" --clean --if-exists --no-owner --no-privileges" "PGPASSWORD=\"\$POSTGRES_PASSWORD\" pg_restore -U \"\$POSTGRES_USER\" -d \"\$POSTGRES_DB\" --clean --if-exists --no-owner --no-privileges"
if [ -n "${uploads_file}" ]; then
cat "${uploads_file}" | docker compose --env-file "${ENV_FILE}" -f "${COMPOSE_FILE}" run --rm --no-deps --entrypoint sh app -lc \
"mkdir -p /app/uploads && find /app/uploads -mindepth 1 -delete && tar -xzf - -C /app/uploads"
fi
if [ -n "${config_file}" ]; then
tar -xzf "${config_file}" -C .
fi
echo "Restore complete from: ${dump_file}" echo "Restore complete from: ${dump_file}"
if [ -n "${uploads_file}" ]; then
echo "Restored uploads archive: ${uploads_file}"
fi
if [ -n "${config_file}" ]; then
echo "Restored config archive: ${config_file}"
fi
+16 -7
View File
@@ -1,22 +1,25 @@
# Backup and Restore (V6.0 Phase 4) # Backup and Restore (V6.0 Phase 4)
This guide defines operational backup/restore for the Docker PostgreSQL runtime and Synology replication. This guide defines operational backup/restore for clean-slate recovery of the Docker runtime and Synology replication.
## 1. Backup artifacts ## 1. Backup artifacts (full recovery set)
- Primary local backup location: `./data/backups` - Primary local backup location: `./data/backups`
- Backup format: PostgreSQL custom dump (`pg_dump -Fc`) - Files created per timestamp:
- Naming: `postgres-YYYYMMDD-HHMMSS.dump` (UTC timestamp) - `postgres-YYYYMMDD-HHMMSS.dump` (PostgreSQL custom dump via `pg_dump -Fc`)
- `uploads-YYYYMMDD-HHMMSS.tar.gz` (full `/app/uploads` volume; includes `homepage.md`, documents, and photos)
- `config-YYYYMMDD-HHMMSS.tar.gz` (deployment config snapshot: `.env.production`, `docker-compose.production.yml`, `deploy/cloudflared/config.yml`)
- `backup-YYYYMMDD-HHMMSS.manifest` (artifact index)
## 2. Creating backups ## 2. Creating backups
Use the scripted command: Use the scripted command from the repository root:
```bash ```bash
sh deploy/backup/create_postgres_backup.sh sh deploy/backup/create_postgres_backup.sh
``` ```
Optional environment overrides: Environment overrides:
- `BACKUP_DIR` (default `./data/backups`) - `BACKUP_DIR` (default `./data/backups`)
- `BACKUP_RETENTION_DAYS` (default `14`) - `BACKUP_RETENTION_DAYS` (default `14`)
@@ -38,12 +41,18 @@ Restore requires downtime for app + worker writes.
- `docker compose --env-file .env.production -f docker-compose.production.yml stop app worker` - `docker compose --env-file .env.production -f docker-compose.production.yml stop app worker`
2. Restore: 2. Restore:
- `sh deploy/backup/restore_postgres_backup.sh ./data/backups/postgres-YYYYMMDD-HHMMSS.dump` - `sh deploy/backup/restore_postgres_backup.sh ./data/backups/postgres-YYYYMMDD-HHMMSS.dump`
- if same-timestamp `uploads-*.tar.gz` and `config-*.tar.gz` exist in the same directory, they are restored automatically.
3. Start app and worker: 3. Start app and worker:
- `docker compose --env-file .env.production -f docker-compose.production.yml start app worker` - `docker compose --env-file .env.production -f docker-compose.production.yml start app worker`
4. Validate `/healthz` and run one smoke workflow. 4. Validate `/healthz` and run one smoke workflow.
Notes:
- Config restore extracts the archived files back into the current repository path.
- If only the database dump is present, restore runs in database-only mode (legacy behavior).
## 4. Retention and recovery targets ## 4. Retention and recovery targets
- Retention baseline: keep at least 14 days of backups locally. - Retention baseline: keep at least 14 days of backups locally.
- Synology copy: replicate each new backup to DS420j mounted path. - Synology copy: replicate each backup artifact set to DS420j mounted path.
- Periodic restore drill: run at least once per release cycle to verify recovery. - Periodic restore drill: run at least once per release cycle to verify recovery.
+2 -1
View File
@@ -36,7 +36,7 @@ This runbook is the operational checklist for releasing and monitoring the trans
- stdout aggregation receives events - stdout aggregation receives events
- file logs are written under `./data/logs` - file logs are written under `./data/logs`
5. Create a fresh PostgreSQL backup after successful deployment: 5. Create a fresh PostgreSQL backup after successful deployment:
- `sh deploy/backup/create_postgres_backup.sh` - `sh deploy/backup/create_postgres_backup.sh` (creates DB dump + uploads + config recovery set)
## 3. Rollback triggers and actions ## 3. Rollback triggers and actions
@@ -56,6 +56,7 @@ This runbook is the operational checklist for releasing and monitoring the trans
- relevant DB rows (`job`, `job_source`, `execution_attempt`) - relevant DB rows (`job`, `job_source`, `execution_attempt`)
5. If persistence regression is confirmed, restore the latest valid DB dump: 5. If persistence regression is confirmed, restore the latest valid DB dump:
- `sh deploy/backup/restore_postgres_backup.sh <dump-file>` - `sh deploy/backup/restore_postgres_backup.sh <dump-file>`
- paired uploads/config artifacts (same timestamp) are restored automatically when present.
## 4. Post-release monitoring checklist ## 4. Post-release monitoring checklist