feat(deploy): Unterstützung für Staging-Backups und -Wiederherstellungen

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.
This commit is contained in:
2026-06-05 18:45:52 +02:00
parent 4c36c9160a
commit 697c5781b7
3 changed files with 154 additions and 20 deletions
+16 -7
View File
@@ -2,7 +2,7 @@
Automatische und manuelle Sicherung von PostgreSQL, `.env`, `docker-compose.yml` und App-Code (Git-Archiv) auf der Prod-VM. 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? ## Was wird gesichert?
@@ -36,6 +36,15 @@ cd /opt/kapteins-daagbok
./scripts/backup.sh --reason manual --dry-run # Vorschau ohne Schreiben ./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) ## Crontab (unbeaufsichtigt)
Beispiel: [`scripts/crontab.prod.example`](../../scripts/crontab.prod.example) 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 ## Umgebungsvariablen
| Variable | Default (Prod) | | Variable | Prod (default) | Staging (`-dest stage`) |
|----------|----------------| |----------|----------------|-------------------------|
| `BACKUP_DIR` | `/var/backups/kapteins-daagbok` | | `COMPOSE_FILE` | `docker-compose.yml` | `docker-compose.staging.yml` |
| `COMPOSE_FILE` | `docker-compose.yml` | | `DB_CONTAINER` | `daagbox-prod-db` | `daagbox-staging-db` |
| `DB_CONTAINER` | `daagbox-prod-db` | | `BACKUP_DIR` | `/var/backups/kapteins-daagbok` | gleich |
| `RETENTION` | `5` | | `RETENTION` | `5` | `5` |
+52 -4
View File
@@ -5,6 +5,7 @@
# #
# Usage: # Usage:
# ./scripts/backup.sh # ./scripts/backup.sh
# ./scripts/backup.sh -dest stage # Staging-Container (daagbox-staging-db)
# ./scripts/backup.sh --reason cron # ./scripts/backup.sh --reason cron
# ./scripts/backup.sh --reason pre-deploy --tag v0.1.1.20 # ./scripts/backup.sh --reason pre-deploy --tag v0.1.1.20
# ./scripts/backup.sh --dry-run # ./scripts/backup.sh --dry-run
@@ -14,18 +15,40 @@
set -euo pipefail set -euo pipefail
BACKUP_DIR="${BACKUP_DIR:-/var/backups/kapteins-daagbok}" 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}" ENV_FILE="${ENV_FILE:-.env}"
RETENTION="${RETENTION:-5}" RETENTION="${RETENTION:-5}"
DEST="prod"
REASON="manual" REASON="manual"
EXPLICIT_TAG="" EXPLICIT_TAG=""
DRY_RUN=0 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() { usage() {
sed -n '2,14p' "$0" sed -n '2,14p' "$0"
echo "" echo ""
echo "Options:" echo "Options:"
echo " -dest prod|stage Target environment (default: prod)"
echo " --reason cron|pre-deploy|manual Backup trigger (default: manual)" 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 " --tag TAG Git tag label (e.g. v0.1.1.20 for pre-deploy)"
echo " --dry-run Show actions without writing backup" echo " --dry-run Show actions without writing backup"
@@ -34,6 +57,14 @@ usage() {
while [ $# -gt 0 ]; do while [ $# -gt 0 ]; do
case "$1" in case "$1" in
-dest)
DEST="${2:?-dest requires an argument}"
shift 2
;;
-dest=*)
DEST="${1#*=}"
shift
;;
--reason) --reason)
REASON="${2:?--reason requires an argument}" REASON="${2:?--reason requires an argument}"
shift 2 shift 2
@@ -58,6 +89,16 @@ while [ $# -gt 0 ]; do
esac esac
done 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 case "$REASON" in
cron|pre-deploy|manual) ;; cron|pre-deploy|manual) ;;
*) *)
@@ -124,8 +165,14 @@ if [ -z "${POSTGRES_PASSWORD:-}" ]; then
fi fi
if ! docker inspect "$DB_CONTAINER" >/dev/null 2>&1; then if ! docker inspect "$DB_CONTAINER" >/dev/null 2>&1; then
echo "Error: DB container '$DB_CONTAINER' not found" >&2 if [[ "$DEST" == "prod" ]] && docker inspect daagbox-staging-db >/dev/null 2>&1; then
exit 1 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 fi
if [ "$(docker inspect -f '{{.State.Running}}' "$DB_CONTAINER")" != "true" ]; then if [ "$(docker inspect -f '{{.State.Running}}' "$DB_CONTAINER")" != "true" ]; then
@@ -173,6 +220,7 @@ from datetime import datetime, timezone
manifest = { manifest = {
"timestamp": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"), "timestamp": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
"local_timestamp": "${TIMESTAMP}", "local_timestamp": "${TIMESTAMP}",
"destination": "${DEST}",
"reason": "${REASON}", "reason": "${REASON}",
"git_tag": "${GIT_TAG}", "git_tag": "${GIT_TAG}",
"git_sha": "${GIT_SHA}", "git_sha": "${GIT_SHA}",
+86 -9
View File
@@ -3,21 +3,46 @@
# #
# Usage: # Usage:
# ./scripts/restore-backup.sh --list # ./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 /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: # Environment overrides:
# BACKUP_DIR, COMPOSE_FILE, DB_CONTAINER, BACKEND_CONTAINER, ENV_FILE # BACKUP_DIR, COMPOSE_FILE, DB_CONTAINER, BACKEND_CONTAINER, ENV_FILE
set -euo pipefail set -euo pipefail
BACKUP_DIR="${BACKUP_DIR:-/var/backups/kapteins-daagbok}" 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}" ENV_FILE="${ENV_FILE:-.env}"
MAX_WAIT=90 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" MODE="full"
RESTORE_PATH="" RESTORE_PATH=""
@@ -28,6 +53,7 @@ usage() {
sed -n '2,10p' "$0" sed -n '2,10p' "$0"
echo "" echo ""
echo "Options:" echo "Options:"
echo " -dest prod|stage Target environment (default: prod)"
echo " --list List available backups" echo " --list List available backups"
echo " --restore PATH Backup archive to restore" echo " --restore PATH Backup archive to restore"
echo " --full Restore DB + .env (default)" echo " --full Restore DB + .env (default)"
@@ -48,6 +74,14 @@ confirm() {
while [ $# -gt 0 ]; do while [ $# -gt 0 ]; do
case "$1" in case "$1" in
-dest)
DEST="${2:?-dest requires an argument}"
shift 2
;;
-dest=*)
DEST="${1#*=}"
shift
;;
--list) --list)
LIST=1 LIST=1
shift shift
@@ -84,6 +118,16 @@ while [ $# -gt 0 ]; do
esac esac
done 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() { list_backups() {
if [ ! -d "$BACKUP_DIR" ]; then if [ ! -d "$BACKUP_DIR" ]; then
echo "No backup directory: $BACKUP_DIR" echo "No backup directory: $BACKUP_DIR"
@@ -138,9 +182,42 @@ echo "Backup manifest:"
python3 -m json.tool "$MANIFEST" python3 -m json.tool "$MANIFEST"
echo "" echo ""
GIT_TAG="$(python3 -c "import json; print(json.load(open('$MANIFEST'))['git_tag'])")" read_manifest_field() {
GIT_SHA="$(python3 -c "import json; print(json.load(open('$MANIFEST'))['git_sha'])")" python3 -c "import json; print(json.load(open('$MANIFEST')).get('$1', '') or '')"
BACKUP_TS="$(python3 -c "import json; print(json.load(open('$MANIFEST'))['local_timestamp'])")" }
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 if ! confirm "Restore backup from $BACKUP_TS (tag: $GIT_TAG)? Mode: $MODE"; then
echo "Aborted." echo "Aborted."