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

72
docker-compose.prod.yml Normal file
View File

@@ -0,0 +1,72 @@
# Stack de PRODUCCIÓN de recordaLexia (instancia pública en la VM).
# docker compose -f docker-compose.prod.yml --env-file .env.prod up -d --build
#
# Principios:
# - Superficie mínima: Postgres y backend NO publican puertos; el frontend solo
# escucha en 127.0.0.1:8089 y el nginx NATIVO de la VM hace el proxy TLS.
# - Límites de recursos: la VM aloja más servicios; nadie se la come.
# - Sin familia demo: el seeder es fail-safe y aquí NO se activa.
# - Ningún secreto en este fichero: todo llega de .env.prod (fuera de git).
services:
postgres:
image: postgres:16-alpine
container_name: recordalexia-prod-postgres
environment:
POSTGRES_DB: ${DB_NAME}
POSTGRES_USER: ${DB_USER}
POSTGRES_PASSWORD: ${DB_PASSWORD}
TZ: Europe/Madrid
volumes:
- pgdata-prod:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${DB_USER} -d ${DB_NAME}"]
interval: 10s
timeout: 5s
retries: 5
mem_limit: 512m
restart: unless-stopped
backend:
build:
context: ./backend
container_name: recordalexia-prod-backend
depends_on:
postgres:
condition: service_healthy
environment:
SPRING_PROFILES_ACTIVE: prod
DB_HOST: postgres
DB_PORT: "5432"
DB_NAME: ${DB_NAME}
SPRING_DATASOURCE_USERNAME: ${DB_USER}
SPRING_DATASOURCE_PASSWORD: ${DB_PASSWORD}
# Dominio público (enlaces del email de recuperación) y CORS estricto.
PUBLIC_BASE_URL: ${PUBLIC_BASE_URL}
CORS_ALLOWED_ORIGINS: ${PUBLIC_BASE_URL}
MAIL_FROM: ${MAIL_FROM}
# SMTP del proveedor (binding relajado de Spring). Sin SMTP_HOST definido,
# los emails de recuperación se quedan en el log del contenedor.
SPRING_MAIL_HOST: ${SMTP_HOST:-}
SPRING_MAIL_PORT: ${SMTP_PORT:-587}
SPRING_MAIL_USERNAME: ${SMTP_USERNAME:-}
SPRING_MAIL_PASSWORD: ${SMTP_PASSWORD:-}
SPRING_MAIL_PROPERTIES_MAIL_SMTP_AUTH: "true"
SPRING_MAIL_PROPERTIES_MAIL_SMTP_STARTTLS_ENABLE: "true"
TZ: Europe/Madrid
mem_limit: 768m
restart: unless-stopped
frontend:
build:
context: ./frontend
container_name: recordalexia-prod-frontend
depends_on:
- backend
ports:
# SOLO loopback: el único acceso desde fuera es el nginx nativo de la VM.
- "127.0.0.1:8089:80"
mem_limit: 64m
restart: unless-stopped
volumes:
pgdata-prod: