Skip to content

Commit ec87b3f

Browse files
committed
Adding ability to perform incremental base backups, making the WAL backup actually useful.
1 parent e90d0a0 commit ec87b3f

5 files changed

Lines changed: 105 additions & 38 deletions

File tree

README.md

Lines changed: 20 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ A simple, containerized solution for automated full and incremental backups of a
55
## Features
66

77
- **Full Backups:** Scheduled `pg_dump` backups of your PostgreSQL database, compressed and stored in `/backups/full` or `/backups/$BACKUP_SUBDIR/full`.
8-
- **Incremental Backups:** Optionally archive PostgreSQL WAL files for point-in-time recovery, stored in `/backups/incremental` or `/backups/$BACKUP_SUBDIR/incremental`.
8+
- **Incremental Backups:** Optionally perform a physical base backup (`pg_basebackup`) and archive PostgreSQL WAL files for point-in-time recovery. Base backups are stored in `/backups/base` or `/backups/$BACKUP_SUBDIR/base`, and WAL incrementals in `/backups/incremental` or `/backups/$BACKUP_SUBDIR/incremental`. Incrementals are retained only as long as their corresponding base backup exists.
99
- **Retention Policies:** Automatically remove old backups based on configurable retention periods.
1010
- **Configurable Scheduling:** Use environment variables to control backup intervals via cron.
1111
- **Easy Integration:** Designed to run as a Docker container, with minimal configuration.
@@ -15,20 +15,21 @@ A simple, containerized solution for automated full and incremental backups of a
1515

1616
### Environment Variables
1717

18-
| Variable | Description | Default |
19-
|-------------------------------|----------------------------------------------------------|------------------------|
20-
| `POSTGRES_HOST` | PostgreSQL host | (required) |
21-
| `POSTGRES_PORT` | PostgreSQL port | (required) |
22-
| `POSTGRES_USER` | PostgreSQL user | (required) |
23-
| `POSTGRES_DB` | PostgreSQL database name | (required) |
24-
| `PGPASSWORD_FILE` | Path to file containing the PostgreSQL password | (required) |
25-
| `ENABLE_INCREMENTAL` | Enable incremental (WAL) backups (`true`/`false`) | `true` |
26-
| `BACKUP_NAME` | Name for the backup file | `backup` |
27-
| `RETENTION_FULL_DAYS` | Days to keep full backups | `7` |
28-
| `RETENTION_INC_DAYS` | Days to keep incremental backups | `3` |
29-
| `BACKUP_FULL_INTERVAL` | Cron schedule for full backups | `0 2 * * 0` |
30-
| `BACKUP_INCREMENTAL_INTERVAL` | Cron schedule for incremental backups | `0 */6 * * *` |
31-
| `BACKUP_SUBDIR` | Subdirectory for backups to be stored | (undefined) |
18+
| Variable | Description | Default |
19+
|-----------------------------------|----------------------------------------------------------|------------------------|
20+
| `POSTGRES_HOST` | PostgreSQL host | (required) |
21+
| `POSTGRES_PORT` | PostgreSQL port | (required) |
22+
| `POSTGRES_USER` | PostgreSQL user | (required) |
23+
| `POSTGRES_DB` | PostgreSQL database name | (required) |
24+
| `PGPASSWORD_FILE` | Path to file containing the PostgreSQL password | (required) |
25+
| `ENABLE_INCREMENTAL` | Enable incremental (WAL) backups (`true`/`false`) | `true` |
26+
| `BACKUP_NAME` | Name for the backup file | `backup` |
27+
| `RETENTION_FULL_DAYS` | Days to keep full backups | `7` |
28+
| `RETENTION_INC_DAYS` | Days to keep incremental backups | `3` |
29+
| `BACKUP_FULL_INTERVAL` | Cron schedule for full backups | `0 2 1 * *` |
30+
| `BACKUP_INCREMENTAL_BASE_INTERVAL`| Cron schedule for incremental base backups | `0 3 * * 0` |
31+
| `BACKUP_INCREMENTAL_INTERVAL` | Cron schedule for incremental backups | `0 */6 * * *` |
32+
| `BACKUP_SUBDIR` | Subdirectory for backups to be stored | (undefined) |
3233

3334
### Volumes
3435

@@ -44,8 +45,8 @@ docker run -d \
4445
-e POSTGRES_USER=postgres \
4546
-e POSTGRES_DB=mydb \
4647
-e PGPASSWORD_FILE=/run/secrets/pgpassword \
47-
-e RETENTION_FULL_DAYS=7 \
48-
-e RETENTION_INC_DAYS=3 \
48+
-e RETENTION_FULL_DAYS=30 \
49+
-e RETENTION_INC_DAYS=10 \
4950
-e ENABLE_INCREMENTAL=true \
5051
-v /host/backups:/backups \
5152
-v /host/wal_archive:/wal_archive \
@@ -68,8 +69,8 @@ services:
6869
- POSTGRES_USER=postgres
6970
- POSTGRES_DB=mydb
7071
- PGPASSWORD_FILE=/run/secrets/pgpassword
71-
- RETENTION_FULL_DAYS=7
72-
- RETENTION_INC_DAYS=3
72+
- RETENTION_FULL_DAYS=30
73+
- RETENTION_INC_DAYS=10
7374
- ENABLE_INCREMENTAL=true
7475
volumes:
7576
- /host/backups:/backups

backup_full.sh

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,25 @@ fi
1010

1111
DATE=$(date +%F_%H-%M-%S)
1212
DEST_DIR="$BASE_BACKUP_DIR/full/$DATE"
13-
mkdir -p "$DEST_DIR"
13+
if ! mkdir -p "$DEST_DIR"; then
14+
echo "[ERROR] Failed to create directory $DEST_DIR"
15+
exit 1
16+
fi
1417

1518
export PGPASSWORD=$(cat $PGPASSWORD_FILE)
1619

17-
echo "[$(date)] Performing full backup..."
18-
pg_dump -h "${POSTGRES_HOST}" \
20+
echo "[INFO] Performing full backup..."
21+
if ! pg_dump -h "${POSTGRES_HOST}" \
1922
-p "${POSTGRES_PORT}" \
2023
-U "${POSTGRES_USER}" \
21-
"${POSTGRES_DB}" | gzip > "$DEST_DIR/$BACKUP_NAME.gz"
24+
"${POSTGRES_DB}" | gzip > "$DEST_DIR/$BACKUP_NAME.gz"; then
25+
echo "[ERROR] $1"
26+
exit 1
27+
fi
2228

2329
# Apply retention
24-
find $BASE_BACKUP_DIR/full -type d -mtime +${RETENTION_FULL_DAYS} -exec rm -rf {} +
30+
if ! find "$BASE_BACKUP_DIR/full" -type d -mtime +"${RETENTION_FULL_DAYS}" -exec rm -rf {} +; then
31+
echo "[WARNING] Retention cleanup failed"
32+
fi
2533

26-
echo "[$(date)] Full backup completed."
34+
echo "[INFO] Full backup completed."

backup_incremental.sh

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,22 +10,27 @@ fi
1010

1111
DATE=$(date +%F_%H-%M-%S)
1212
DEST_DIR="$BASE_BACKUP_DIR/incremental/$DATE"
13-
mkdir -p "$DEST_DIR"
13+
if ! mkdir -p "$DEST_DIR"; then
14+
echo "[ERROR] Failed to create directory $DEST_DIR"
15+
exit 1
16+
fi
1417

15-
echo "[$(date)] Performing incremental backup..."
18+
echo "[INFO] Performing incremental backup..."
1619

1720
# Backup all WALs since last backup
18-
cp /wal_archive/* "$DEST_DIR/" || true
19-
20-
# Apply retention on the incremental backups themselves
21-
find $BASE_BACKUP_DIR/incremental -type d -mtime +${RETENTION_INC_DAYS} -exec rm -rf {} +
21+
if ! cp /wal_archive/* "$DEST_DIR/" 2>/dev/null; then
22+
echo "[WARNING] No WAL files found to copy from /wal_archive"
23+
fi
2224

2325
# Clean up WAL files using pg_archivecleanup
24-
# Determine the last WAL file to keep
25-
LAST_WAL=$(ls -1 /wal_archive/* | sort | tail -n 1 || true)
26+
LAST_WAL=$(ls -1 /wal_archive/* 2>/dev/null | sort | tail -n 1 || true)
2627
if [ -n "$LAST_WAL" ]; then
27-
echo "[$(date)] Cleaning up WAL archive up to $LAST_WAL"
28-
pg_archivecleanup /wal_archive "$LAST_WAL"
28+
echo "[INFO] Cleaning up WAL archive up to $LAST_WAL"
29+
if ! pg_archivecleanup /wal_archive "$LAST_WAL"; then
30+
echo "[WARNING] pg_archivecleanup failed"
31+
fi
32+
else
33+
echo "[INFO] No WAL files found for cleanup."
2934
fi
3035

31-
echo "[$(date)] Incremental backup completed."
36+
echo "[INFO] Incremental backup completed."

backup_incremental_base.sh

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
#!/bin/bash
2+
set -euo pipefail
3+
4+
BACKUP_SUBDIR="${BACKUP_SUBDIR:-}"
5+
if [ -n "$BACKUP_SUBDIR" ]; then
6+
BASE_BACKUP_DIR="/backups/$BACKUP_SUBDIR"
7+
else
8+
BASE_BACKUP_DIR="/backups"
9+
fi
10+
11+
DATE=$(date +%F_%H-%M-%S)
12+
DEST_DIR="$BASE_BACKUP_DIR/incremental_base/$DATE"
13+
14+
echo "[INFO] Performing incremental base backup into $DEST_DIR"
15+
mkdir -p "$DEST_DIR"
16+
17+
export PGPASSWORD=$(cat "$PGPASSWORD_FILE")
18+
19+
if ! pg_basebackup \
20+
-h "${POSTGRES_HOST}" \
21+
-p "${POSTGRES_PORT}" \
22+
-U "${POSTGRES_USER}" \
23+
-D "$DEST_DIR" \
24+
-F tar \
25+
-z \
26+
-X none; then
27+
echo "[ERROR] Base backup failed"
28+
exit 1
29+
fi
30+
31+
echo "[INFO] Base backup completed."
32+
33+
# Cleanup old incremental base backups
34+
echo "[INFO] Applying incremental base backup retention policy..."
35+
if ! find "$BASE_BACKUP_DIR/incremental_base" -mindepth 1 -maxdepth 1 -type d -mtime +"${RETENTION_INC_DAYS}" -print -exec rm -rf {} + 2>&1; then
36+
echo "[WARNING] Retention cleanup for incremental base backups may have failed" >&2
37+
fi
38+
39+
# Cleanup old incrementals relative to the oldest base
40+
OLDEST_BASE=$(ls -1 "$BASE_BACKUP_DIR/incremental_base" | sort | head -n 1 || true)
41+
42+
if [ -n "$OLDEST_BASE" ]; then
43+
echo "[INFO] Retaining incrementals since base $OLDEST_BASE"
44+
if ! find "$BASE_BACKUP_DIR/incremental" -mindepth 1 -maxdepth 1 -type d \
45+
! -newer "$BASE_BACKUP_DIR/incremental_base/$OLDEST_BASE" \
46+
-print -exec rm -rf {} + 2>&1; then
47+
echo "[WARNING] Retention cleanup for incrementals may have failed" >&2
48+
fi
49+
else
50+
echo "[INFO] No incremental base backups found, skipping incremental cleanup"
51+
fi

entrypoint.sh

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,16 @@ CRON_DIR=/tmp/cron
55
mkdir -p "$CRON_DIR"
66

77
ENABLE_INCREMENTAL="${ENABLE_INCREMENTAL:-true}"
8-
FULL_INTERVAL="${BACKUP_FULL_INTERVAL:-0 2 * * 0}"
8+
FULL_INTERVAL="${BACKUP_FULL_INTERVAL:-0 2 1 * *}" # Default to 2:00 AM on the first day of each month
99

1010
cat > "$CRON_DIR/backup" <<EOF
1111
$FULL_INTERVAL /scripts/backup_full.sh
1212
EOF
1313

1414
if [[ "$ENABLE_INCREMENTAL" == "true" ]]; then
15-
INC_INTERVAL="${BACKUP_INCREMENTAL_INTERVAL:-0 */6 * * *}"
15+
INC_BASE_INTERVAL="${BACKUP_INCREMENTAL_BASE_INTERVAL:-0 3 * * 0}" # Default to 3:00 AM every Sunday
16+
echo "$INC_BASE_INTERVAL /scripts/backup_incremental_base.sh" >> "$CRON_DIR/backup"
17+
INC_INTERVAL="${BACKUP_INCREMENTAL_INTERVAL:-0 */6 * * *}" # Default to every 6 hours
1618
echo "$INC_INTERVAL /scripts/backup_incremental.sh" >> "$CRON_DIR/backup"
1719
fi
1820

0 commit comments

Comments
 (0)