From d66f0d65c3fc3ad3fe15892566d33dba68c87ff2 Mon Sep 17 00:00:00 2001 From: Jens Reinemann Date: Sun, 17 May 2026 11:06:52 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20t=C3=A4gliches=20PostgreSQL-Backup=20mi?= =?UTF-8?q?t=201=20GB=20Rotation=20(#87)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - backup/Dockerfile: Alpine 3.21 + postgresql-client + dcron - backup/backup.sh: pg_dump -> MD5-Checksum-Vergleich (skip bei unveränderter DB) -> gzip-komprimiertes Backup mit Timestamp -> Rotation: älteste .sql.gz löschen bis Gesamtgröße < 1 GB - docker-compose.yml: neuer Service 'backup', Volume 'backup_data' - Cronjob: täglich 03:00 UTC --- backup/Dockerfile | 14 ++++++++++++ backup/backup.sh | 57 ++++++++++++++++++++++++++++++++++++++++++++++ docker-compose.yml | 15 ++++++++++++ 3 files changed, 86 insertions(+) create mode 100644 backup/Dockerfile create mode 100644 backup/backup.sh diff --git a/backup/Dockerfile b/backup/Dockerfile new file mode 100644 index 0000000..be01d76 --- /dev/null +++ b/backup/Dockerfile @@ -0,0 +1,14 @@ +FROM alpine:3.21 + +# Install PostgreSQL client and dcron (lightweight cron daemon) +RUN apk add --no-cache postgresql-client dcron + +COPY backup.sh /usr/local/bin/backup.sh +RUN chmod +x /usr/local/bin/backup.sh + +# Crontab: run backup daily at 03:00 UTC +RUN echo "0 3 * * * /usr/local/bin/backup.sh >> /var/log/backup.log 2>&1" \ + > /etc/crontabs/root + +# crond -f: run in foreground; -l 2: log level notice +CMD ["crond", "-f", "-l", "2"] diff --git a/backup/backup.sh b/backup/backup.sh new file mode 100644 index 0000000..9993cbf --- /dev/null +++ b/backup/backup.sh @@ -0,0 +1,57 @@ +#!/bin/sh +set -e + +BACKUP_DIR="${BACKUP_DIR:-/backups}" +DB_HOST="${POSTGRES_HOST:-db}" +DB_NAME="${POSTGRES_DB:-krisenvorrat}" +DB_USER="${POSTGRES_USER:-krisenvorrat}" +MAX_SIZE_BYTES=$((1 * 1024 * 1024 * 1024)) # 1 GB + +mkdir -p "$BACKUP_DIR" + +TIMESTAMP=$(date -u +"%Y-%m-%d_%H%M") +BACKUP_FILE="${BACKUP_DIR}/${DB_NAME}_${TIMESTAMP}.sql.gz" +CHECKSUM_FILE="${BACKUP_DIR}/.last_checksum" + +echo "[$(date -u)] Starting backup of database '${DB_NAME}' on host '${DB_HOST}'..." + +# Dump to temp file for checksum comparison +TEMP_DUMP=$(mktemp) +export PGPASSWORD="${POSTGRES_PASSWORD:-}" +pg_dump -h "$DB_HOST" -U "$DB_USER" "$DB_NAME" > "$TEMP_DUMP" + +# Compute MD5 of dump content to detect changes +CURRENT_CHECKSUM=$(md5sum "$TEMP_DUMP" | cut -d' ' -f1) +LAST_CHECKSUM="" +if [ -f "$CHECKSUM_FILE" ]; then + LAST_CHECKSUM=$(cat "$CHECKSUM_FILE") +fi + +if [ "$CURRENT_CHECKSUM" = "$LAST_CHECKSUM" ]; then + echo "[$(date -u)] No changes since last backup. Skipping." + rm -f "$TEMP_DUMP" + exit 0 +fi + +# Compress and save +gzip -c "$TEMP_DUMP" > "$BACKUP_FILE" +rm -f "$TEMP_DUMP" +printf '%s' "$CURRENT_CHECKSUM" > "$CHECKSUM_FILE" +echo "[$(date -u)] Backup saved: ${BACKUP_FILE}" + +# Rotate: remove oldest .sql.gz until total backup dir size <= 1 GB +while true; do + TOTAL=$(du -sb "$BACKUP_DIR" | cut -f1) + if [ "$TOTAL" -le "$MAX_SIZE_BYTES" ]; then + break + fi + OLDEST=$(ls -tr "${BACKUP_DIR}"/*.sql.gz 2>/dev/null | head -1) + if [ -z "$OLDEST" ]; then + echo "[$(date -u)] Warning: Cannot reduce backup dir below 1 GB, no more files to delete." + break + fi + echo "[$(date -u)] Rotation: removing ${OLDEST} (dir total: ${TOTAL} bytes)" + rm -f "$OLDEST" +done + +echo "[$(date -u)] Backup complete." diff --git a/docker-compose.yml b/docker-compose.yml index a203661..617d0a6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -26,5 +26,20 @@ services: depends_on: - db + backup: + build: ./backup + container_name: krisenvorrat-backup + restart: unless-stopped + environment: + - POSTGRES_HOST=db + - POSTGRES_DB=krisenvorrat + - POSTGRES_USER=krisenvorrat + - POSTGRES_PASSWORD=krisenvorrat + volumes: + - backup_data:/backups + depends_on: + - db + volumes: pgdata: + backup_data: