From 697c5781b79b3fcee92f74ef16c8c128b86976cb Mon Sep 17 00:00:00 2001 From: elpatron Date: Fri, 5 Jun 2026 18:45:52 +0200 Subject: [PATCH] =?UTF-8?q?feat(deploy):=20Unterst=C3=BCtzung=20f=C3=BCr?= =?UTF-8?q?=20Staging-Backups=20und=20-Wiederherstellungen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Erweitert die Backup- und Restore-Skripte um die Möglichkeit, Staging-Umgebungen zu unterstützen. Fügt die Option `-dest stage` hinzu, um spezifische Konfigurationen für Staging zu verwenden, einschließlich separater Docker-Compose-Dateien und Datenbankcontainer. Dokumentation aktualisiert, um manuelle Tests und Umgebungsvariablen für Staging zu reflektieren. --- docs/deployment/backup.md | 23 +++++++--- scripts/backup.sh | 56 +++++++++++++++++++++-- scripts/restore-backup.sh | 95 +++++++++++++++++++++++++++++++++++---- 3 files changed, 154 insertions(+), 20 deletions(-) diff --git a/docs/deployment/backup.md b/docs/deployment/backup.md index 6e4ed72..815898b 100644 --- a/docs/deployment/backup.md +++ b/docs/deployment/backup.md @@ -2,7 +2,7 @@ Automatische und manuelle Sicherung von PostgreSQL, `.env`, `docker-compose.yml` und App-Code (Git-Archiv) auf der Prod-VM. -**Staging:** Kein automatisches Backup — Daten sind bewusst wegwerfbar. Deploy via `update-remotes.sh -dest stage` legt kein Backup an. +**Staging:** Kein automatisches Backup — Daten sind bewusst wegwerfbar. Deploy via `update-remotes.sh -dest stage` legt kein Backup an. Zum manuellen Testen auf Staging: `-dest stage` (oder Auto-Fallback, wenn nur `daagbox-staging-db` läuft). ## Was wird gesichert? @@ -36,6 +36,15 @@ cd /opt/kapteins-daagbok ./scripts/backup.sh --reason manual --dry-run # Vorschau ohne Schreiben ``` +### Staging (manueller Test) + +```bash +cd /opt/kapteins-daagbok-staging +./scripts/backup.sh -dest stage --reason manual +# oder: Auto-Fallback, wenn nur daagbox-staging-db läuft +./scripts/backup.sh --reason manual +``` + ## Crontab (unbeaufsichtigt) Beispiel: [`scripts/crontab.prod.example`](../../scripts/crontab.prod.example) @@ -94,9 +103,9 @@ Vor [`rotate-postgres-password.sh`](../../scripts/rotate-postgres-password.sh) e ## Umgebungsvariablen -| Variable | Default (Prod) | -|----------|----------------| -| `BACKUP_DIR` | `/var/backups/kapteins-daagbok` | -| `COMPOSE_FILE` | `docker-compose.yml` | -| `DB_CONTAINER` | `daagbox-prod-db` | -| `RETENTION` | `5` | +| Variable | Prod (default) | Staging (`-dest stage`) | +|----------|----------------|-------------------------| +| `COMPOSE_FILE` | `docker-compose.yml` | `docker-compose.staging.yml` | +| `DB_CONTAINER` | `daagbox-prod-db` | `daagbox-staging-db` | +| `BACKUP_DIR` | `/var/backups/kapteins-daagbok` | gleich | +| `RETENTION` | `5` | `5` | diff --git a/scripts/backup.sh b/scripts/backup.sh index 5f5ffc2..048b9cb 100755 --- a/scripts/backup.sh +++ b/scripts/backup.sh @@ -5,6 +5,7 @@ # # Usage: # ./scripts/backup.sh +# ./scripts/backup.sh -dest stage # Staging-Container (daagbox-staging-db) # ./scripts/backup.sh --reason cron # ./scripts/backup.sh --reason pre-deploy --tag v0.1.1.20 # ./scripts/backup.sh --dry-run @@ -14,18 +15,40 @@ set -euo pipefail BACKUP_DIR="${BACKUP_DIR:-/var/backups/kapteins-daagbok}" -COMPOSE_FILE="${COMPOSE_FILE:-docker-compose.yml}" -DB_CONTAINER="${DB_CONTAINER:-daagbox-prod-db}" ENV_FILE="${ENV_FILE:-.env}" RETENTION="${RETENTION:-5}" +DEST="prod" REASON="manual" EXPLICIT_TAG="" DRY_RUN=0 +COMPOSE_FILE="" +DB_CONTAINER="" + +apply_dest_config() { + local dest="$1" + local force="${2:-0}" + if [[ "$dest" == "stage" ]]; then + if [[ "$force" == "1" || -z "${COMPOSE_FILE}" ]]; then + COMPOSE_FILE="docker-compose.staging.yml" + fi + if [[ "$force" == "1" || -z "${DB_CONTAINER}" ]]; then + DB_CONTAINER="daagbox-staging-db" + fi + else + if [[ "$force" == "1" || -z "${COMPOSE_FILE}" ]]; then + COMPOSE_FILE="docker-compose.yml" + fi + if [[ "$force" == "1" || -z "${DB_CONTAINER}" ]]; then + DB_CONTAINER="daagbox-prod-db" + fi + fi +} usage() { sed -n '2,14p' "$0" echo "" echo "Options:" + echo " -dest prod|stage Target environment (default: prod)" echo " --reason cron|pre-deploy|manual Backup trigger (default: manual)" echo " --tag TAG Git tag label (e.g. v0.1.1.20 for pre-deploy)" echo " --dry-run Show actions without writing backup" @@ -34,6 +57,14 @@ usage() { while [ $# -gt 0 ]; do case "$1" in + -dest) + DEST="${2:?-dest requires an argument}" + shift 2 + ;; + -dest=*) + DEST="${1#*=}" + shift + ;; --reason) REASON="${2:?--reason requires an argument}" shift 2 @@ -58,6 +89,16 @@ while [ $# -gt 0 ]; do esac done +case "$DEST" in + prod|stage) ;; + *) + echo "Error: invalid -dest '$DEST' (use prod or stage)" >&2 + exit 1 + ;; +esac + +apply_dest_config "$DEST" + case "$REASON" in cron|pre-deploy|manual) ;; *) @@ -124,8 +165,14 @@ if [ -z "${POSTGRES_PASSWORD:-}" ]; then fi if ! docker inspect "$DB_CONTAINER" >/dev/null 2>&1; then - echo "Error: DB container '$DB_CONTAINER' not found" >&2 - exit 1 + if [[ "$DEST" == "prod" ]] && docker inspect daagbox-staging-db >/dev/null 2>&1; then + echo "Note: $DB_CONTAINER not found — falling back to staging (daagbox-staging-db). Use -dest stage explicitly." + apply_dest_config stage 1 + DEST="stage" + else + echo "Error: DB container '$DB_CONTAINER' not found" >&2 + exit 1 + fi fi if [ "$(docker inspect -f '{{.State.Running}}' "$DB_CONTAINER")" != "true" ]; then @@ -173,6 +220,7 @@ from datetime import datetime, timezone manifest = { "timestamp": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"), "local_timestamp": "${TIMESTAMP}", + "destination": "${DEST}", "reason": "${REASON}", "git_tag": "${GIT_TAG}", "git_sha": "${GIT_SHA}", diff --git a/scripts/restore-backup.sh b/scripts/restore-backup.sh index a792704..1418d87 100755 --- a/scripts/restore-backup.sh +++ b/scripts/restore-backup.sh @@ -3,21 +3,46 @@ # # Usage: # ./scripts/restore-backup.sh --list +# ./scripts/restore-backup.sh -dest stage --restore PATH # ./scripts/restore-backup.sh --restore /var/backups/kapteins-daagbok/kapteins-daagbok_....tar.gz -# ./scripts/restore-backup.sh --restore PATH --db-only -# ./scripts/restore-backup.sh --restore PATH --env-only -# ./scripts/restore-backup.sh --restore PATH --full --yes # # Environment overrides: # BACKUP_DIR, COMPOSE_FILE, DB_CONTAINER, BACKEND_CONTAINER, ENV_FILE set -euo pipefail BACKUP_DIR="${BACKUP_DIR:-/var/backups/kapteins-daagbok}" -COMPOSE_FILE="${COMPOSE_FILE:-docker-compose.yml}" -DB_CONTAINER="${DB_CONTAINER:-daagbox-prod-db}" -BACKEND_CONTAINER="${BACKEND_CONTAINER:-daagbox-prod-backend}" ENV_FILE="${ENV_FILE:-.env}" MAX_WAIT=90 +DEST="prod" +COMPOSE_FILE="" +DB_CONTAINER="" +BACKEND_CONTAINER="" + +apply_dest_config() { + local dest="$1" + local force="${2:-0}" + if [[ "$dest" == "stage" ]]; then + if [[ "$force" == "1" || -z "${COMPOSE_FILE}" ]]; then + COMPOSE_FILE="docker-compose.staging.yml" + fi + if [[ "$force" == "1" || -z "${DB_CONTAINER}" ]]; then + DB_CONTAINER="daagbox-staging-db" + fi + if [[ "$force" == "1" || -z "${BACKEND_CONTAINER}" ]]; then + BACKEND_CONTAINER="daagbox-staging-backend" + fi + else + if [[ "$force" == "1" || -z "${COMPOSE_FILE}" ]]; then + COMPOSE_FILE="docker-compose.yml" + fi + if [[ "$force" == "1" || -z "${DB_CONTAINER}" ]]; then + DB_CONTAINER="daagbox-prod-db" + fi + if [[ "$force" == "1" || -z "${BACKEND_CONTAINER}" ]]; then + BACKEND_CONTAINER="daagbox-prod-backend" + fi + fi +} MODE="full" RESTORE_PATH="" @@ -28,6 +53,7 @@ usage() { sed -n '2,10p' "$0" echo "" echo "Options:" + echo " -dest prod|stage Target environment (default: prod)" echo " --list List available backups" echo " --restore PATH Backup archive to restore" echo " --full Restore DB + .env (default)" @@ -48,6 +74,14 @@ confirm() { while [ $# -gt 0 ]; do case "$1" in + -dest) + DEST="${2:?-dest requires an argument}" + shift 2 + ;; + -dest=*) + DEST="${1#*=}" + shift + ;; --list) LIST=1 shift @@ -84,6 +118,16 @@ while [ $# -gt 0 ]; do esac done +case "$DEST" in + prod|stage) ;; + *) + echo "Error: invalid -dest '$DEST' (use prod or stage)" >&2 + exit 1 + ;; +esac + +apply_dest_config "$DEST" + list_backups() { if [ ! -d "$BACKUP_DIR" ]; then echo "No backup directory: $BACKUP_DIR" @@ -138,9 +182,42 @@ echo "Backup manifest:" python3 -m json.tool "$MANIFEST" echo "" -GIT_TAG="$(python3 -c "import json; print(json.load(open('$MANIFEST'))['git_tag'])")" -GIT_SHA="$(python3 -c "import json; print(json.load(open('$MANIFEST'))['git_sha'])")" -BACKUP_TS="$(python3 -c "import json; print(json.load(open('$MANIFEST'))['local_timestamp'])")" +read_manifest_field() { + python3 -c "import json; print(json.load(open('$MANIFEST')).get('$1', '') or '')" +} + +MANIFEST_COMPOSE="$(read_manifest_field compose_file)" +MANIFEST_DB="$(read_manifest_field db_container)" +MANIFEST_DEST="$(read_manifest_field destination)" + +if [ -n "$MANIFEST_COMPOSE" ]; then + COMPOSE_FILE="$MANIFEST_COMPOSE" +fi +if [ -n "$MANIFEST_DB" ]; then + DB_CONTAINER="$MANIFEST_DB" +fi +if [ -n "$MANIFEST_DEST" ]; then + if [[ "$MANIFEST_DEST" == "stage" ]]; then + BACKEND_CONTAINER="${BACKEND_CONTAINER:-daagbox-staging-backend}" + else + BACKEND_CONTAINER="${BACKEND_CONTAINER:-daagbox-prod-backend}" + fi +fi + +if ! docker inspect "$DB_CONTAINER" >/dev/null 2>&1; then + if [[ "$DEST" == "prod" ]] && docker inspect daagbox-staging-db >/dev/null 2>&1; then + echo "Note: $DB_CONTAINER not found — falling back to staging (daagbox-staging-db). Use -dest stage explicitly." + apply_dest_config stage 1 + DEST="stage" + else + echo "Error: DB container '$DB_CONTAINER' not found" >&2 + exit 1 + fi +fi + +GIT_TAG="$(read_manifest_field git_tag)" +GIT_SHA="$(read_manifest_field git_sha)" +BACKUP_TS="$(read_manifest_field local_timestamp)" if ! confirm "Restore backup from $BACKUP_TS (tag: $GIT_TAG)? Mode: $MODE"; then echo "Aborted."