From a5bb4dc33d79d461628206585c3444a1c7faaa90 Mon Sep 17 00:00:00 2001 From: dlawler489 <104159223@student.swin.edu.au> Date: Sun, 20 Sep 2026 16:19:21 +1000 Subject: [PATCH] Add a nightly backup script A dated pg_dump plus an rsync mirror of the model files, with the stack running. The dump is read back with pg_restore -l inside the container before it replaces the day's file, so a bad run leaves older backups alone. Older dumps are pruned after KEEP_DAYS. Co-Authored-By: Claude Opus 5 --- .env.example | 5 ++++ README.md | 28 ++++++++++++++++++++ tools/backup.sh | 70 +++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 103 insertions(+) create mode 100755 tools/backup.sh diff --git a/.env.example b/.env.example index 7d09efe..ed00f5b 100644 --- a/.env.example +++ b/.env.example @@ -10,5 +10,10 @@ DATA_DIR=/Volumes/Data/printhive # or a network share. PGDATA_DIR=/Volumes/Data/printhive/pgdata +# Where tools/backup.sh writes (default ./backups) and how many days of +# database dumps it keeps (default 14). Best on a different disk from DATA_DIR. +BACKUP_DIR=/Volumes/Data/printhive-backups +KEEP_DAYS=14 + # Thingiverse import — get a free app token at https://www.thingiverse.com/developers THINGIVERSE_TOKEN= diff --git a/README.md b/README.md index 36fcc64..e13b67b 100644 --- a/README.md +++ b/README.md @@ -77,6 +77,34 @@ files live on one of those. A backup needs the database (or a `pg_dump`) and `uploads`; the other three rebuild themselves. Everything else in the container is replaced on each update, so nothing outside these folders is kept. +### Backups + +`tools/backup.sh` takes a dated dump of the database and mirrors the model files, leaving Printhive running: + +```bash +./tools/backup.sh +``` + +It writes to `./backups` unless `BACKUP_DIR` says otherwise: `backups/db/printhive-YYYY-MM-DD.dump` (kept for +`KEEP_DAYS`, 14 by default) and `backups/uploads`, an rsync mirror of the files — matching what's there now, +deletions included, rather than a history. Thumbnails, previews and extracted archives are skipped, as they rebuild. +Each dump is read back with `pg_restore -l` before it replaces the day's file, so a failed run leaves the older +backups alone and exits non-zero. + +Nightly, at 3:30am — `crontab -e`, with the path to your own copy: + +``` +30 3 * * * cd /path/to/printhive && ./tools/backup.sh >> backups/backup.log 2>&1 +``` + +Docker has to be running for it to work, and the backups are best kept on a different disk from `DATA_DIR`. + +To restore the database into a running stack: + +```bash +docker compose exec -T db pg_restore -U printhive -d printhive --clean --if-exists < backups/db/printhive-2026-01-01.dump +``` + Upgrading from a version that used Docker named volumes: stop the stack, run `tools/volumes-to-folders.sh` to copy the data across, then start it again. The script only copies — the old volumes stay until you remove them yourself. diff --git a/tools/backup.sh b/tools/backup.sh new file mode 100755 index 0000000..504a4c1 --- /dev/null +++ b/tools/backup.sh @@ -0,0 +1,70 @@ +#!/usr/bin/env bash +# Nightly backup: a dated dump of the database, and a mirror of the model files. +# +# Run it from anywhere — it finds the folder holding docker-compose.yml itself. +# Printhive keeps running throughout; pg_dump takes a consistent snapshot. +# +# ./tools/backup.sh +# +# Settings (in .env beside docker-compose.yml, or the environment): +# BACKUP_DIR where backups go (default: ./backups) +# DATA_DIR where the data lives (default: ./data) +# KEEP_DAYS dumps to keep, in days (default: 14) +set -euo pipefail + +cd "$(dirname "$0")/.." + +setting() { # name, fallback + local from_env="${!1:-}" + if [ -n "$from_env" ]; then echo "$from_env"; return; fi + if [ -f .env ]; then + local line + line="$(grep -E "^$1=" .env | tail -1 | cut -d= -f2-)" + if [ -n "$line" ]; then echo "$line"; return; fi + fi + echo "$2" +} + +BACKUP_DIR="$(setting BACKUP_DIR ./backups)" +DATA_DIR="$(setting DATA_DIR ./data)" +KEEP_DAYS="$(setting KEEP_DAYS 14)" +stamp="$(date +%Y-%m-%d)" + +mkdir -p "$BACKUP_DIR/db" + +# --- the database ----------------------------------------------------------- +# Written to a .part file first, so an interrupted run never leaves behind +# something that looks like a good backup. +dump="$BACKUP_DIR/db/printhive-$stamp.dump" + +# Dumped inside the container and read back there — a dump that can't be listed +# is no backup at all, and pg_restore can only list a real file, not a pipe. +if ! docker compose exec -T db sh -c \ + 'pg_dump -U printhive -Fc printhive > /tmp/printhive.dump && pg_restore -l /tmp/printhive.dump > /dev/null'; then + echo "$(date '+%F %T') FAILED: couldn’t dump the database — keeping the old backups" >&2 + docker compose exec -T db rm -f /tmp/printhive.dump || true + exit 1 +fi +docker compose exec -T db cat /tmp/printhive.dump > "$dump.part" +docker compose exec -T db rm -f /tmp/printhive.dump || true + +if [ ! -s "$dump.part" ]; then + echo "$(date '+%F %T') FAILED: the dump came out empty — keeping the old backups" >&2 + rm -f "$dump.part" + exit 1 +fi +mv "$dump.part" "$dump" +echo "$(date '+%F %T') database → $dump ($(du -h "$dump" | cut -f1))" + +# Older dumps go, so the folder doesn't grow for ever. +find "$BACKUP_DIR/db" -name 'printhive-*.dump' -type f -mtime "+$KEEP_DAYS" -delete + +# --- the model files -------------------------------------------------------- +# A mirror, not a history: it matches what's there now, deletions included. +# Thumbnails, previews and extracted archives are left out — they rebuild. +if [ ! -d "$DATA_DIR/uploads" ]; then + echo "$(date '+%F %T') FAILED: no $DATA_DIR/uploads — is the disk mounted?" >&2 + exit 1 +fi +rsync -a --delete "$DATA_DIR/uploads/" "$BACKUP_DIR/uploads/" +echo "$(date '+%F %T') files → $BACKUP_DIR/uploads ($(du -sh "$BACKUP_DIR/uploads" | cut -f1))"