From 2c8359fdb1d02e8596855657d7f0ae19cc19e76e Mon Sep 17 00:00:00 2001 From: BaaRai <130491490+BaaRai@users.noreply.github.com> Date: Sun, 11 Oct 2026 22:46:47 +0200 Subject: [PATCH] feat: built-in backup script (pg_dump with rotation, optional data archive) scripts/backup.sh dumps the Postgres database through the postgres compose service (no host psql needed) into timestamped gzip files under backups/, keeping the newest N (default 14). --with-data additionally tars the bind-mounted data directory and fails loudly when files written by the container are unreadable instead of shipping a partial archive. README gains a Backups section with the restore one-liner and a cron example. Closes #40 --- .gitignore | 2 +- README.md | 23 +++++++++++++ scripts/backup.sh | 85 +++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 109 insertions(+), 1 deletion(-) create mode 100755 scripts/backup.sh diff --git a/.gitignore b/.gitignore index 0561467..3689e17 100644 --- a/.gitignore +++ b/.gitignore @@ -57,4 +57,4 @@ Thumbs.db # bench benchmark related (local artifacts, not committed) /bench/ /Dockerfile.bench -/Dockerfile.bench.v2 \ No newline at end of file +/Dockerfile.bench.v2backups/ diff --git a/README.md b/README.md index d1461ab..a71137b 100644 --- a/README.md +++ b/README.md @@ -148,6 +148,29 @@ ARTEX_TARGETS=linux/amd64,windows/amd64 ./build.sh --release --- +## Backups + +`scripts/backup.sh` dumps the Postgres database (and optionally the data +directory) with timestamped output and rotation: + +```bash +scripts/backup.sh --with-data # db dump + data/ archive, keep 14 +scripts/backup.sh --keep 7 # db only, keep 7 +``` + +Restore a dump into a running stack: + +```bash +gunzip -c backups/artex-db-.sql.gz | docker compose exec -T postgres \ + psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" +``` + +Daily cron example (03:20, operator crontab): + +``` +20 3 * * * cd /path/to/ARTEX && scripts/backup.sh --with-data >> backups/backup.log 2>&1 +``` + ## Updating and upgrading > Upgrading only swaps the program and never touches data: the Postgres volume `pgdata`, `./data` (jwt.key / SQLite, etc.), and `./skills` are all preserved. **No manual database migration is needed**: `artex` idempotently re-runs `schema.sql` on every start (with `ADD COLUMN` / `CREATE INDEX IF NOT EXISTS`), i.e. "restart equals migrate". It is still recommended to back up `./data` and the database before upgrading. diff --git a/scripts/backup.sh b/scripts/backup.sh new file mode 100755 index 0000000..5867a2d --- /dev/null +++ b/scripts/backup.sh @@ -0,0 +1,85 @@ +#!/usr/bin/env bash +# Backup the ARTEX database (and optionally the data directory) with rotation. +# +# Usage: +# scripts/backup.sh [--with-data] [--keep N] [--output-dir DIR] +# +# --with-data also archive the bind-mounted data/ directory (worker +# artifacts, evidence, skills state) as a tar.gz +# --keep N number of db dumps to keep (default 14; data archives rotate +# to the same count) +# --output-dir where to write (default backups/, relative to the repo root) +# +# The database dump runs inside the postgres compose service, so no psql client +# is needed on the host. Restore: +# gunzip -c backups/artex-db-.sql.gz | docker compose exec -T postgres \ +# psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" +set -euo pipefail +cd "$(dirname "$0")/.." + +usage() { sed -n '2,15p' "$0" | sed 's/^# \?//'; } + +KEEP=14 +OUTPUT_DIR="backups" +WITH_DATA=0 +while [ $# -gt 0 ]; do + case "$1" in + --keep) KEEP="${2:?}"; shift 2 ;; + --output-dir) OUTPUT_DIR="${2:?}"; shift 2 ;; + --with-data) WITH_DATA=1; shift ;; + -h|--help) usage; exit 0 ;; + *) echo "unknown option: $1" >&2; usage >&2; exit 2 ;; + esac +done +case "$KEEP" in (*[!0-9]*|'') echo "--keep must be a positive integer" >&2; exit 2 ;; esac + +# Compose reads .env for credentials; source it for the dump command too. +if [ -f .env ]; then + set -a + # shellcheck disable=SC1091 + . ./.env + set +a +fi +PGUSER_NAME="${POSTGRES_USER:-artex}" +PGDATABASE_NAME="${POSTGRES_DB:-artex}" + +mkdir -p "$OUTPUT_DIR" +TS=$(date -u +%Y%m%dT%H%M%SZ) +DB_OUT="$OUTPUT_DIR/artex-db-$TS.sql.gz" +if [ -e "$DB_OUT" ]; then + echo "refusing to overwrite $DB_OUT" >&2 + exit 1 +fi + +if ! docker compose exec -T postgres pg_dump -U "$PGUSER_NAME" "$PGDATABASE_NAME" | gzip > "$DB_OUT"; then + rm -f "$DB_OUT" + echo "pg_dump failed (is the stack running?)" >&2 + exit 1 +fi +gzip -t "$DB_OUT" +echo "wrote $DB_OUT ($(du -h "$DB_OUT" | cut -f1))" + +if [ "$WITH_DATA" -eq 1 ]; then + DATA_OUT="$OUTPUT_DIR/artex-data-$TS.tar.gz" + # data/ contains files written by the container (root); unreadable ones make + # tar exit 2. Fail loudly rather than ship a partial archive. + if ! tar -czf "$DATA_OUT" data 2>"$OUTPUT_DIR/.tar.err"; then + rm -f "$DATA_OUT" + echo "data archive failed (run as root so container-written files are readable):" >&2 + head -5 "$OUTPUT_DIR/.tar.err" >&2 + rm -f "$OUTPUT_DIR/.tar.err" + exit 1 + fi + rm -f "$OUTPUT_DIR/.tar.err" + echo "wrote $DATA_OUT ($(du -h "$DATA_OUT" | cut -f1))" +fi + +prune() { + ls -1t "$OUTPUT_DIR"/$1 2>/dev/null | tail -n +$((KEEP + 1)) | while IFS= read -r old; do + echo "pruning $old" + rm -f "$old" + done +} +prune 'artex-db-*.sql.gz' +[ "$WITH_DATA" -eq 1 ] && prune 'artex-data-*.tar.gz' +echo "backup complete ($KEEP newest kept)"