From a58726ba055c550b1ab56ff6ef2b00915ffc00a7 Mon Sep 17 00:00:00 2001 From: Nicolas MASSE Date: Mon, 1 Dec 2025 10:02:05 +0100 Subject: [PATCH] implement backup/restore --- postgresql/config/backup.sh | 22 ++++++++++++--- postgresql/config/config.env | 1 + postgresql/config/init.sh | 37 +++++++++++++++++++++++++ postgresql/postgresql-backup.container | 4 --- postgresql/postgresql-init.container | 6 ++-- postgresql/postgresql-server.container | 3 -- postgresql/postgresql-upgrade.container | 3 -- postgresql/postgresql.pod | 17 ------------ postgresql/postgresql.target | 4 +-- 9 files changed, 60 insertions(+), 37 deletions(-) create mode 100755 postgresql/config/init.sh delete mode 100644 postgresql/postgresql.pod diff --git a/postgresql/config/backup.sh b/postgresql/config/backup.sh index a4a36a2..2e86e3b 100755 --- a/postgresql/config/backup.sh +++ b/postgresql/config/backup.sh @@ -3,9 +3,12 @@ set -Eeuo pipefail export PGHOST=/var/run/postgresql -BACKUP_DIR=/backup/$(date +%Y%m%d) +BACKUP_DIR=/var/lib/postgresql/backup/$(date +%Y-%m-%d_%H-%M-%S)/ mkdir -p "$BACKUP_DIR" +echo "Starting complete backup of the whole PostgreSQL server..." +pg_basebackup --pgdata=$BACKUP_DIR --format=tar --manifest-checksums=SHA256 --verbose +echo "Starting backup of individual databases..." psql -c "SELECT datname FROM pg_database WHERE datname NOT IN ('template0', 'template1', 'postgres');" -t | while read db; do if [ -z "$db" ]; then continue @@ -14,6 +17,17 @@ psql -c "SELECT datname FROM pg_database WHERE datname NOT IN ('template0', 'tem echo "Backup of database $db..." pg_dump -c --if-exists "$db" | gzip -c > "$BACKUP_DIR/dump-$db.sql.gz" done -echo "Complete backup of the whole PostgreSQL server..." -pg_basebackup -D "$BACKUP_DIR/pg_basebackup" -echo "Backups stored in $BACKUP_DIR" +echo "Backup stored in $BACKUP_DIR." + +# backup rotation / retention policy +POSTGRES_BACKUP_RETENTION=${POSTGRES_BACKUP_RETENTION:-7} +if [[ "$POSTGRES_BACKUP_RETENTION" -gt 0 ]] && ls -1ct /var/lib/postgresql/backup/*/backup_manifest &>/dev/null; then + echo "Applying backup retention policy: keeping the last $POSTGRES_BACKUP_RETENTION backups." + ls -1ct /var/lib/postgresql/backup/*/backup_manifest | tail -n "+$((POSTGRES_BACKUP_RETENTION + 1))" | while read old_backup; do + old_backup=$(dirname "$old_backup") + echo "Removing old backup: $old_backup" + rm -rf "$old_backup" + done +else + echo "No backup retention policy applied." +fi diff --git a/postgresql/config/config.env b/postgresql/config/config.env index 772ef58..24278f6 100644 --- a/postgresql/config/config.env +++ b/postgresql/config/config.env @@ -5,3 +5,4 @@ POSTGRES_HOST_AUTH_METHOD=scram-sha-256 POSTGRES_INITDB_ARGS=--auth-host=scram-sha-256 PGPORT=5432 PG_MAJOR=17 +POSTGRES_BACKUP_RETENTION=7 diff --git a/postgresql/config/init.sh b/postgresql/config/init.sh new file mode 100755 index 0000000..204db76 --- /dev/null +++ b/postgresql/config/init.sh @@ -0,0 +1,37 @@ +#!/bin/bash + +set -Eeuo pipefail + +last_backup="" +for f in /var/lib/postgresql/backup/*/backup_manifest; do + # If there are no backups, the glob pattern above won't match any files + if [ ! -f "$f" ]; then + continue + fi + + # Check if this is the most recent backup + if [ -z "$last_backup" ] || [ "$f" -nt "$last_backup" ]; then + last_backup="$f" + fi +done + +if [ -n "$last_backup" ]; then + last_backup=$(dirname "$last_backup") + echo "Restoring from last backup: $last_backup..." + mkdir -p "$PGDATA" + tar -xvf "$last_backup/base.tar" -C "$PGDATA" + if [ -f "$last_backup/pg_wal.tar" ]; then + mkdir -p "$PGDATA/pg_wal" + tar -xvf "$last_backup/pg_wal.tar" -C "$PGDATA/pg_wal" + fi + echo "Verifying backup integrity..." + pg_verifybackup -m "$last_backup/backup_manifest" "$PGDATA" + echo "Setting ownership and permissions..." + chown -R postgres:postgres "$PGDATA" + chmod 700 "$PGDATA" + echo "Restoration complete." + exit 0 +fi + +echo "No previous backup found, initializing an empty database!" +exec /usr/local/bin/docker-ensure-initdb.sh diff --git a/postgresql/postgresql-backup.container b/postgresql/postgresql-backup.container index 043e0b1..a834879 100644 --- a/postgresql/postgresql-backup.container +++ b/postgresql/postgresql-backup.container @@ -25,12 +25,8 @@ User=postgres Volume=/var/lib/quadlets/postgresql:/var/lib/postgresql:z Volume=/etc/quadlets/postgresql/backup.sh:/usr/local/bin/backup.sh:z,ro -# This container is part of a Pod -Pod=postgresql.pod - # Share /var/run/postgresql/ between containers in the pod for the Unix socket Volume=/var/run/quadlets/postgresql:/var/run/postgresql:z -Volume=/var/lib/quadlets/postgresql/backup:/backup:z [Service] Restart=no diff --git a/postgresql/postgresql-init.container b/postgresql/postgresql-init.container index bc3c495..4852b57 100644 --- a/postgresql/postgresql-init.container +++ b/postgresql/postgresql-init.container @@ -27,13 +27,11 @@ Environment=PGDATA=/var/lib/postgresql/${PG_MAJOR}/docker EnvironmentFile=/etc/quadlets/postgresql/config.env # Use the official entrypoint script to initialize the database -Entrypoint=/usr/local/bin/docker-ensure-initdb.sh +Entrypoint=/usr/local/bin/init.sh # Volume mounts Volume=/var/lib/quadlets/postgresql:/var/lib/postgresql:z - -# This container is part of a Pod -Pod=postgresql.pod +Volume=/etc/quadlets/postgresql/init.sh:/usr/local/bin/init.sh:z,ro [Service] Restart=no diff --git a/postgresql/postgresql-server.container b/postgresql/postgresql-server.container index 2ae9384..efc3d35 100644 --- a/postgresql/postgresql-server.container +++ b/postgresql/postgresql-server.container @@ -41,9 +41,6 @@ HealthTimeout=10s HealthStartPeriod=60s HealthRetries=3 -# This container is part of a Pod -Pod=postgresql.pod - # Share /var/run/postgresql/ between containers in the pod for the Unix socket Volume=/var/run/quadlets/postgresql:/var/run/postgresql:z diff --git a/postgresql/postgresql-upgrade.container b/postgresql/postgresql-upgrade.container index fa63450..d1f2036 100644 --- a/postgresql/postgresql-upgrade.container +++ b/postgresql/postgresql-upgrade.container @@ -32,9 +32,6 @@ EnvironmentFile=/etc/quadlets/postgresql/config.env # Volume mounts Volume=/var/lib/quadlets/postgresql:/var/lib/postgresql:z -# This container is part of a Pod -Pod=postgresql.pod - [Service] Restart=no TimeoutStartSec=600 diff --git a/postgresql/postgresql.pod b/postgresql/postgresql.pod deleted file mode 100644 index dda25c5..0000000 --- a/postgresql/postgresql.pod +++ /dev/null @@ -1,17 +0,0 @@ -[Unit] -Description=PostgreSQL Database Server - Pod -Documentation=https://hub.docker.com/_/postgres/ -After=network.target -Before=postgresql.target - -# Only start if PostgreSQL has been configured -ConditionPathExists=/etc/quadlets/postgresql/config.env - -# Start/stop this unit when the target is started/stopped -PartOf=postgresql.target - -[Pod] -PodName=postgresql - -[Install] -WantedBy=postgresql.target diff --git a/postgresql/postgresql.target b/postgresql/postgresql.target index 50c587d..55f9f2b 100644 --- a/postgresql/postgresql.target +++ b/postgresql/postgresql.target @@ -1,8 +1,8 @@ [Unit] Description=PostgreSQL Service Target Documentation=man:systemd.target(5) -Requires=postgresql-pod.service postgresql-server.service postgresql-upgrade.service postgresql-init.service postgresql-set-major.service -After=postgresql-pod.service postgresql-server.service postgresql-upgrade.service postgresql-init.service postgresql-set-major.service +Requires=postgresql-server.service postgresql-upgrade.service postgresql-init.service postgresql-set-major.service +After=postgresql-server.service postgresql-upgrade.service postgresql-init.service postgresql-set-major.service # Allow isolation - can stop/start this target independently AllowIsolate=yes