feat(saas): infraestructura de producción para la VM

Bloque C de la fase SaaS (ficheros; el despliegue se hace con el runbook):
- docker-compose.prod.yml: superficie mínima (solo el frontend en
  127.0.0.1:8089 para el nginx nativo), límites de memoria, sin seeder.
- deploy/nginx-recordalexia.conf: server block con cabeceras de seguridad,
  X-Forwarded-For para el rate-limiter y preparado para certbot.
- deploy/backup.sh: pg_dump diario comprimido, retención 14, copia externa
  vía rclone y detección de backups vacíos.
- .env.prod.example (y .env.prod en .gitignore).
- docs/operacion-saas.md: runbook con primer despliegue, checklist,
  restore ENSAYADO paso a paso, actualización y problemas conocidos.
This commit is contained in:
Jaume Garriga Maestre
2026-07-13 22:33:12 +02:00
parent ddd1a96af9
commit 6b655634ca
6 changed files with 312 additions and 0 deletions

52
deploy/backup.sh Executable file
View File

@@ -0,0 +1,52 @@
#!/usr/bin/env bash
# Backup diario de la base de datos de recordaLexia (producción).
#
# Qué hace:
# 1. pg_dump comprimido del Postgres del compose de producción.
# 2. Retención local: conserva los últimos BACKUP_KEEP (por defecto 14).
# 3. Copia FUERA de la VM con rclone si RCLONE_REMOTE está definido
# (ej. "b2:recordalexia-backups" o "gdrive:backups/recordalexia").
#
# Instalación (cron del usuario en la VM, 03:30 hora de Madrid):
# crontab -e
# 30 3 * * * /ruta/al/repo/deploy/backup.sh >> $HOME/recordalexia-backup.log 2>&1
#
# RESTAURAR: ver docs/operacion-saas.md (procedimiento ensayado paso a paso).
set -euo pipefail
# --- Configuración (sobreescribible por variables de entorno) ---
REPO_DIR="${REPO_DIR:-$(cd "$(dirname "$0")/.." && pwd)}"
BACKUP_DIR="${BACKUP_DIR:-$HOME/backups/recordalexia}"
BACKUP_KEEP="${BACKUP_KEEP:-14}"
RCLONE_REMOTE="${RCLONE_REMOTE:-}"
COMPOSE=(docker compose -f "$REPO_DIR/docker-compose.prod.yml" --env-file "$REPO_DIR/.env.prod")
# Credenciales de la BD: las mismas del compose (nunca en este script).
source "$REPO_DIR/.env.prod"
mkdir -p "$BACKUP_DIR"
STAMP="$(date +%Y%m%d-%H%M%S)"
FILE="$BACKUP_DIR/recordalexia-$STAMP.sql.gz"
echo "[$(date -Is)] Iniciando backup -> $FILE"
"${COMPOSE[@]}" exec -T postgres pg_dump -U "$DB_USER" "$DB_NAME" | gzip > "$FILE"
# El backup vacío o diminuto es un fallo, no un backup.
SIZE=$(stat -c%s "$FILE" 2>/dev/null || stat -f%z "$FILE")
if [ "$SIZE" -lt 1024 ]; then
echo "[$(date -Is)] ERROR: backup sospechosamente pequeño ($SIZE bytes)" >&2
exit 1
fi
echo "[$(date -Is)] Backup OK ($SIZE bytes)"
# Retención local: borrar los que sobren, del más antiguo al más nuevo.
ls -1t "$BACKUP_DIR"/recordalexia-*.sql.gz | tail -n +$((BACKUP_KEEP + 1)) | xargs -r rm --
echo "[$(date -Is)] Retención aplicada (máx. $BACKUP_KEEP locales)"
# Copia fuera de la VM (si hay remoto configurado en rclone).
if [ -n "$RCLONE_REMOTE" ]; then
rclone copy "$FILE" "$RCLONE_REMOTE/" --no-traverse
echo "[$(date -Is)] Copia externa OK -> $RCLONE_REMOTE"
else
echo "[$(date -Is)] AVISO: sin RCLONE_REMOTE; el backup solo existe en esta VM"
fi