Backup-Sidecar: täglicher pg_dump scharf geschaltet
Das dokumentierte Backup lief real nie (stock postgres-Image ohne Cron, backup.sh nirgends eingebunden) -> bei Volume-/Disk-Verlust drohte Totalverlust. - Neuer service "backup" (docker-compose.yml): postgres:16-alpine als Sidecar, taeglicher pg_dump um BACKUP_HOUR (Default 03:00 Europe/Berlin) - backup-loop.sh: abhaengigkeitsfreier Tages-Scheduler (umgeht busybox-cron- Environment-Eigenheiten) - backup.sh korrigiert: laeuft remote gegen PGHOST mit PGPASSWORD; -Fc statt "-Fc | gzip" (war doppelt komprimiert und als .sql.gz nicht restore-bar) -> .dump - BACKUP_HOUR/BACKUP_RETENTION_DAYS konfigurierbar (.env.example) - operations.md: Backup-/Restore-Anleitung an Ist-Zustand angepasst Verifiziert: pg_dump erstellt + pg_restore in frische DB, Daten intakt. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
39e259f3ac
commit
7285ed9cae
5 files changed
+95
-13
No files matched your search
@@ -17,6 +17,11 @@ SESSION_COOKIE_SECRET=please-change-me-in-prod-at-least-32-chars
|
||||
# --- Cron Bearer (Story 2.6 + 5.1 + 6.3) ---
|
||||
CRON_BEARER_TOKEN=please-change-me-in-prod
|
||||
|
||||
# --- Backup-Sidecar (Story 6.3, nur Prod) ---
|
||||
# Täglicher pg_dump im service "backup". Beide optional (Defaults: 03 / 14).
|
||||
# BACKUP_HOUR=03
|
||||
# BACKUP_RETENTION_DAYS=14
|
||||
|
||||
# --- App ---
|
||||
NODE_ENV=development
|
||||
LOG_LEVEL=info
|
||||
|
||||
@@ -21,6 +21,33 @@ services:
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
# Backup-Sidecar: täglicher pg_dump des postgres-Service nach cockpit_backups.
|
||||
# Nutzt scripts/backup.sh (Custom-Format-Dump + Retention + API-Heartbeat),
|
||||
# getaktet von scripts/backup-loop.sh. Stock postgres-Image → pg_dump passt
|
||||
# versionsgenau (16) zur DB. Restore: siehe docs/operations.md.
|
||||
backup:
|
||||
image: postgres:16-alpine
|
||||
container_name: cockpit-backup
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
POSTGRES_USER: ${POSTGRES_USER:-cockpit}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||
POSTGRES_DB: ${POSTGRES_DB:-cockpit}
|
||||
PGHOST: postgres
|
||||
API_URL: http://api:3000
|
||||
CRON_BEARER_TOKEN: ${CRON_BEARER_TOKEN}
|
||||
BACKUP_HOUR: ${BACKUP_HOUR:-03}
|
||||
BACKUP_RETENTION_DAYS: ${BACKUP_RETENTION_DAYS:-14}
|
||||
TZ: Europe/Berlin
|
||||
volumes:
|
||||
- cockpit_backups:/backups
|
||||
- ./scripts/backup.sh:/usr/local/bin/backup.sh:ro
|
||||
- ./scripts/backup-loop.sh:/usr/local/bin/backup-loop.sh:ro
|
||||
entrypoint: ['/bin/sh', '/usr/local/bin/backup-loop.sh']
|
||||
|
||||
api:
|
||||
build:
|
||||
context: .
|
||||
|
||||
+25
-4
@@ -54,14 +54,35 @@ docker compose exec api node -e "import('@cockpit/db/client').then(async ({ crea
|
||||
|
||||
## Backups (Story 6.3)
|
||||
|
||||
Tägliches `pg_dump` via Cron-Endpoint `POST /internal/cron/pg-dump-backup` mit Bearer-Token.
|
||||
14 Tage Retention. Restore-Drill quartalsweise.
|
||||
Tägliches `pg_dump` läuft im **Backup-Sidecar** (`docker-compose.yml` service `backup`):
|
||||
`scripts/backup-loop.sh` triggert `scripts/backup.sh` einmal pro Tag um
|
||||
`BACKUP_HOUR` (Default 03:00 Europe/Berlin). Dump im **Custom-Format** (`-Fc`,
|
||||
bereits komprimiert) nach `cockpit_backups:/backups` als `cockpit-<ts>.dump`.
|
||||
Retention `BACKUP_RETENTION_DAYS` (Default 14). Nach jedem Lauf ein Heartbeat an
|
||||
`POST /internal/cron/pg-dump-backup` (nur Logging — der Endpoint macht **kein**
|
||||
Backup). Restore-Drill quartalsweise.
|
||||
|
||||
Konfiguration (optional, in `.env.production`): `BACKUP_HOUR`, `BACKUP_RETENTION_DAYS`.
|
||||
|
||||
```bash
|
||||
# Status / letztes Backup ansehen
|
||||
docker compose logs --tail=20 backup
|
||||
docker compose exec backup ls -lh /backups
|
||||
|
||||
# Sofort ein Backup auslösen (außer der Reihe)
|
||||
docker compose exec backup /usr/local/bin/backup.sh
|
||||
```
|
||||
|
||||
### Restore-Anleitung
|
||||
|
||||
1. API stoppen: `docker compose stop api`
|
||||
2. Backup aus `/backups` auswählen.
|
||||
3. Restore in temporäre DB: `docker compose exec postgres pg_restore -U cockpit -d cockpit_restore /backups/<file>.sql.gz`
|
||||
2. Backup aus `/backups` auswählen (`docker compose exec postgres ls -lh /backups`).
|
||||
3. Restore in temporäre DB:
|
||||
```bash
|
||||
docker compose exec postgres createdb -U cockpit cockpit_restore
|
||||
docker compose exec postgres pg_restore -U cockpit -d cockpit_restore /backups/<file>.dump
|
||||
```
|
||||
(In die Produktiv-DB stattdessen mit `--clean --if-exists -d cockpit`.)
|
||||
4. Smoke-Test: `curl http://localhost:3000/api/health`
|
||||
5. API wieder starten: `docker compose start api`
|
||||
|
||||
|
||||
Executable
+19
@@ -0,0 +1,19 @@
|
||||
#!/bin/sh
|
||||
# Minimaler Tages-Scheduler für den Backup-Sidecar: führt backup.sh einmal
|
||||
# täglich zur Stunde BACKUP_HOUR (Container-TZ = Europe/Berlin) aus.
|
||||
#
|
||||
# Bewusst kein cron: ein simpler Poll-Loop erbt das Container-Environment
|
||||
# zuverlässig (POSTGRES_PASSWORD u. a.) und hat keine busybox-cron-Eigenheiten.
|
||||
|
||||
set -eu
|
||||
|
||||
HOUR="${BACKUP_HOUR:-03}"
|
||||
echo "Backup-Sidecar aktiv — täglich um ${HOUR}:00 (TZ=${TZ:-UTC}), Retention ${BACKUP_RETENTION_DAYS:-14} Tage."
|
||||
|
||||
while true; do
|
||||
if [ "$(date +%H)" = "$HOUR" ] && [ "$(date +%M)" = "00" ]; then
|
||||
/usr/local/bin/backup.sh || echo "Backup fehlgeschlagen (Exit $?)" >&2
|
||||
sleep 61 # diese Minute überspringen, damit nicht doppelt ausgelöst wird
|
||||
fi
|
||||
sleep 30
|
||||
done
|
||||
+19
-9
@@ -1,21 +1,31 @@
|
||||
#!/bin/sh
|
||||
# Tägliches pg_dump in /backups, 14 Tage Retention.
|
||||
# Läuft im Postgres-Container via System-Cron.
|
||||
# pg_dump des Cockpit-Postgres nach /backups. Custom-Format (-Fc): bereits
|
||||
# komprimiert UND erlaubt selektives pg_restore — daher KEIN zusätzliches gzip.
|
||||
#
|
||||
# Läuft im Backup-Sidecar (docker-compose.yml service "backup") gegen den
|
||||
# postgres-Service (PGHOST). Einmal pro Tag getriggert von backup-loop.sh.
|
||||
# Manuell ausführbar: `docker compose exec backup /usr/local/bin/backup.sh`.
|
||||
|
||||
set -eu
|
||||
|
||||
PGHOST="${PGHOST:-postgres}"
|
||||
PGUSER="${POSTGRES_USER:-cockpit}"
|
||||
PGDATABASE="${POSTGRES_DB:-cockpit}"
|
||||
RETENTION_DAYS="${BACKUP_RETENTION_DAYS:-14}"
|
||||
DATE=$(date +%Y%m%d-%H%M%S)
|
||||
OUT="/backups/cockpit-${DATE}.sql.gz"
|
||||
OUT="/backups/cockpit-${DATE}.dump"
|
||||
mkdir -p /backups
|
||||
|
||||
pg_dump -U "${POSTGRES_USER:-cockpit}" -d "${POSTGRES_DB:-cockpit}" -Fc | gzip > "$OUT"
|
||||
echo "Backup written: $OUT"
|
||||
export PGPASSWORD="${POSTGRES_PASSWORD:-}"
|
||||
pg_dump -h "$PGHOST" -U "$PGUSER" -d "$PGDATABASE" -Fc -f "$OUT"
|
||||
echo "Backup geschrieben: $OUT ($(du -h "$OUT" | cut -f1))"
|
||||
|
||||
# Retention: lösche Dumps älter als 14 Tage.
|
||||
find /backups -type f -name "cockpit-*.sql.gz" -mtime +14 -delete
|
||||
# Retention: Dumps älter als N Tage löschen.
|
||||
find /backups -type f -name 'cockpit-*.dump' -mtime "+${RETENTION_DAYS}" -delete
|
||||
|
||||
# Heartbeat an API (logging).
|
||||
if [ -n "${CRON_BEARER_TOKEN:-}" ] && [ -n "${API_URL:-}" ]; then
|
||||
# Heartbeat an die API (nur Observability/Logging — macht selbst kein Backup).
|
||||
API_URL="${API_URL:-http://api:3000}"
|
||||
if [ -n "${CRON_BEARER_TOKEN:-}" ]; then
|
||||
wget -q --header="Authorization: Bearer ${CRON_BEARER_TOKEN}" \
|
||||
-O- "${API_URL}/internal/cron/pg-dump-backup" || true
|
||||
fi
|
||||
Reference in new issue
Block a user