Si el servidor muere mañana: el runbook de recuperación que sí ejecutamos en 4 horas
Tener backups no es lo mismo que saber recuperar. Compartimos el runbook completo de Disaster Recovery que validamos sobre nuestro VPS: 9 fases ordenadas, scripts reales, lo que se recupera automático y lo que NO. RTO objetivo: 4 horas. Es DevOps avanzado pero accesible. Si te interesa este patrón en tu infraestructura, lo armamos contigo.
Hace meses escribimos un post más conceptual sobre planes de recuperación cuando el servidor cae. Este post es la versión profunda: el runbook real que ejecutaríamos si nuestro VPS desapareciera mañana. 9 fases, scripts, lo que sale del backup automático y lo que NO.
Es para alguien técnico que opera infra propia. Si llegas como dueño de negocio: la idea para ti está en el TL;DR de arriba — sin runbook documentado y practicado, los backups solos no te salvan.
RTO y RPO: los dos números que importan
Antes de los pasos, dos términos que casi siempre se confunden:
- RTO (Recovery Time Objective): cuánto tiempo te puede tomar volver a estar arriba después de un desastre. "Si el servidor muere ahora, ¿en cuánto rato vuelvo a vender?"
- RPO (Recovery Point Objective): cuántos datos puedes permitirte perder. "Si recupero, ¿pierdo la última hora? ¿el último día? ¿la última semana?"
Para nuestra infra:
| Métrica | Valor |
|---|---|
| RTO | 2-4 horas (depende del ancho de banda para descargar backups) |
| RPO datos transaccionales (Postgres) | Hasta 24h (backup diario 2 AM) |
RPO configuraciones (/opt) | Hasta 24h (backup diario 3 AM) |
| RPO volúmenes Docker (Qdrant, MinIO, RabbitMQ, Grafana) | Hasta 7 días (backup semanal domingo 4 AM) |
¿Es perfecto? No. Para datos transaccionales con cero pérdida tolerable, harían falta réplicas en tiempo real (no backups diarios). Para PyME, este RPO es razonable. Para una fintech, no.
Lo importante: tú sabes tus números. Si te preguntan por RTO/RPO de tu infra y no lo sabes, ese es el primer trabajo.
Estrategia de backups
Lo que se respalda y dónde:
| Tipo | Script | Cron | Retención local | Retención S3 |
|---|---|---|---|---|
| PostgreSQL (6 DBs) | backup-db.sh | Diario 2 AM | 7 días | 30 días |
/opt (configs, compose, .env) | backup-opt.sh | Diario 3 AM | 3 días | 30 días |
| Volúmenes Docker críticos | backup-volumes.sh | Domingo 4 AM | 14 días | 60 días |
Por qué backups en dos lugares: el local es para recuperar rápido si necesitas algo de hace 2-3 días. El S3 es para "el servidor entero murió y necesito empezar de cero en otro VPS". Sin S3, un solo desastre coordinado se lleva todo.
Los scripts: corren con cron, escriben logs en /opt/app/logs/, y el backup-db.sh hace dump por base de datos para no parar el servicio.
Verificación pre-emergencia (la que casi nadie hace):
# Ver últimos backups en S3
aws s3 ls s3://s3-backups-server/postgres/ --profile ecr | tail -10
aws s3 ls s3://s3-backups-server/opt/ --profile ecr | tail -5
# Ver logs recientes
tail -30 /opt/app/logs/backup-db.log
# Script de verificación end-to-end
/opt/app/scripts/check-backups.sh
Si nunca corriste estos comandos, ve a hacerlo después de leer este post. Backups que están corriendo "según yo" pero que nadie miró son una falsa sensación de seguridad común.
Las 9 fases del runbook
Fase 0 — VPS nuevo
Mínimo recomendado: 8 vCPU, 24 GB RAM, 200 GB SSD, Ubuntu 22.04 o 24.04. Para PyME chica, 4 vCPU + 16 GB es el piso. Para grande, 16 vCPU + 64 GB.
sudo apt update && sudo apt upgrade -y
sudo apt install -y curl wget git unzip awscli python3
Fase 1 — Docker
Sin Docker no hay nada (todo nuestro stack está containerizado).
sudo apt install -y ca-certificates curl gnupg lsb-release
sudo mkdir -p /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
sudo usermod -aG docker ubuntu
Fase 2 — Restaurar /opt desde S3
/opt es donde viven los docker-compose.yml, los .env, los scripts. Sin esto, no sabes ni cómo levantar las apps.
mkdir -p /tmp/restore
LATEST_OPT=$(aws s3 ls s3://s3-backups-server/opt/ --profile ecr | sort | tail -1 | awk '{print $4}')
aws s3 cp "s3://s3-backups-server/opt/$LATEST_OPT" /tmp/restore/opt-backup.tar.gz --profile ecr
sudo tar -xzf /tmp/restore/opt-backup.tar.gz -C /
Detalle importante: tus credenciales AWS para descargar tienen que estar disponibles en el VPS nuevo. Las nuestras viven en
~/.aws/credentialsy NO están en el backup (sería contraproducente). Hay que tener una copia segura aparte (gestor de contraseñas).
Fase 3 — Seguridad antes de exponer nada
Esto es crítico y se olvida fácil bajo presión: antes de abrir puertos, configurar firewall y SSH hardening.
sudo apt install -y ufw fail2ban
sudo ufw default deny incoming && sudo ufw default allow outgoing
sudo ufw allow 22/tcp
sudo ufw allow in on lo
sudo ufw --force enable
# SSH hardening en /etc/ssh/sshd_config
# PermitRootLogin no
# MaxAuthTries 3
# PasswordAuthentication no
sudo systemctl restart sshd
sudo systemctl enable fail2ban && sudo systemctl start fail2ban
Hay un patrón importante acá: el backup NO incluye las IPs baneadas en UFW. Eso significa que cuando levantas el servidor nuevo, los bots que ya conocen tu IP empiezan a tantear de nuevo. Fail2ban los detecta y banea, pero hay un período inicial expuesto. Si el ataque era dirigido y tienes IPs específicas conocidas, conviene re-banearlas a mano antes de exponer.
Fase 4 — Nginx Proxy Manager (NPM)
NPM es el reverse proxy con SSL automático. Sin él, ningún dominio público funciona.
cd /opt/app/infra/nginx-proxy-manager
# Logs path
sudo mkdir -p /var/log/applications/nginx
# Levantar primero la DB
docker compose up -d db
sleep 30
# Si hay backup del volumen npm-data, restaurarlo:
# aws s3 cp s3://s3-backups-server/volumes/npm-VERSION.tar.gz /tmp/restore/ --profile ecr
# tar -xzf /tmp/restore/npm-VERSION.tar.gz -C /opt/.../nginx-proxy-manager/
docker compose up -d
Lo que NO se recupera automático: si no tienes el backup del volumen
npm-datareciente, los proxy hosts (los 12 que configuran cada dominio público) hay que rehacerlos a mano. Es un par de horas de trabajo manual con la lista del documento de proxy hosts.
Fase 5 — Cloudflare Tunnel
El tunnel es lo que permite que los dominios públicos lleguen al VPS sin abrir puertos a internet (ver Cloudflare Tunnel: cero puertos abiertos).
# Instalar cloudflared
curl -fsSL https://pkg.cloudflare.com/cloudflare-main.gpg | sudo tee /usr/share/keyrings/cloudflare-main.gpg
echo "deb [signed-by=/usr/share/keyrings/cloudflare-main.gpg] https://pkg.cloudflare.com/cloudflared $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/cloudflared.list
sudo apt update && sudo apt install -y cloudflared
# Las credenciales tunnel-XXX.json viven en /etc/cloudflared/
# Verificar que están restauradas en /etc/cloudflared/
ls -la /etc/cloudflared/
sudo chmod 600 /etc/cloudflared/*.json /etc/cloudflared/config.yml
sudo cloudflared service install
sudo systemctl enable cloudflared
sudo systemctl start cloudflared
Si las credenciales del tunnel no estaban en
/opt/(a veces no terminan ahí por permisos), hay que descargarlas desde el dashboard de Cloudflare. Pero el dashboard no permite "descargar el .json existente" — hay que crear uno nuevo y reconectar el tunnel. Eso son 30 minutos extra que vale la pena evitar verificando que el JSON esté en el backup ANTES de tener un desastre.
Fase 6 — Restaurar PostgreSQL
Las 6 bases de datos que tenemos: auth_db, billing_db, notification_db, ordenaahora_db, flowordr_v3_prod, formsapi.
mkdir -p /tmp/restore/postgres
aws s3 sync s3://s3-backups-server/postgres/ /tmp/restore/postgres/ \
--profile ecr --exclude "*" \
--include "*$(date +%Y%m%d)*"
# Levantar el container postgres primero
cd /opt/app/services/forms-api
docker compose up -d forms-db-prod
sleep 15
# Restaurar cada DB
for DB in auth_db billing_db notification_db ordenaahora_db; do
FILE=$(ls /tmp/restore/postgres/${DB}_*.sql.gz | tail -1)
gunzip -c "$FILE" | docker exec -i forms-db-prod psql -U saas_admin -d "$DB"
done
for DB in flowordr_v3_prod formsapi; do
FILE=$(ls /tmp/restore/postgres/${DB}_*.sql.gz | tail -1)
gunzip -c "$FILE" | docker exec -i forms-db-prod psql -U formsuser -d "$DB"
done
Fase 7 — Restaurar volúmenes Docker
Qdrant (RAG), MinIO (objetos), RabbitMQ (messaging), Grafana (dashboards).
aws s3 sync s3://s3-backups-server/volumes/ /tmp/restore/volumes/ \
--profile ecr --exclude "*" \
--include "*$(date +%Y%m%d)*"
sudo /opt/app/scripts/restaurar-backup.sh /tmp/restore
Fase 8 — Levantar servicios en orden
El orden importa: dependencias primero. RabbitMQ y Postgres antes que cualquier microservicio que los consuma.
# 1. Infraestructura compartida (monitoreo)
cd /opt/app/infra/monitoring && docker compose up -d
# 2. Servicios base
cd /opt/app/services/auth/app && docker compose up -d
cd /opt/app/services/forms-api && docker compose up -d
# 3. Tenants / productos
cd /opt/app/tenants/serverstack-solutions && docker compose up -d
cd /opt/app/tenants/flowordr/app && docker compose up -d
cd /opt/app/tenants/ordenaahora && docker compose up -d
# 4. Conectar Prometheus a redes externas (el monitoreo necesita esto)
docker network connect flowordrv3_network prometheus
Fase 9 — Verificación
# Containers todos UP
docker ps --format "table {{.Names}}\t{{.Status}}" | grep -v Exited
# Targets de Prometheus
curl -s http://localhost:9090/api/v1/targets | python3 -m json.tool | grep -E "(job|health)"
# Dominios públicos
curl -sI https://serverstack.solutions | head -3
curl -sI https://flowordr.com | head -3
# Tunnel
cloudflared tunnel info <TUNNEL-ID>
# Re-instalar crontab si no se restauró
crontab -l || crontab - << 'EOF'
0 2 * * * /opt/app/scripts/backup-db.sh >> /opt/app/logs/backup-db.log 2>&1
0 3 * * * /opt/app/scripts/backup-opt.sh >> /opt/app/logs/backup-opt.log 2>&1
0 4 * * 0 /opt/app/scripts/backup-volumes.sh >> /opt/app/logs/backup-volumes.log 2>&1
EOF
Lo que NO se recupera automático
Esta tabla es la que más nos costó armar — son las cosas que aprendimos por las malas:
| Ítem | Por qué no se recupera | Cómo recuperar |
|---|---|---|
| Proxy hosts de NPM | Viven en MariaDB del container; si no está el backup del volumen | Reconfigurar a mano usando lista documentada de los 12 proxy hosts |
| Cloudflare Tunnel credentials | A veces no están en /opt/ por permisos | Verificar que el .json está en el backup antes del desastre, o crear nuevo tunnel |
| Certificados SSL | En letsencrypt/, en backup de volumen | Let's Encrypt los re-emite automático cuando NPM levanta |
| Dashboards Grafana editados en UI | En volumen grafana-data, backup semanal | Restaurar volumen o re-importar JSON desde grafana/dashboards/ |
| IPs baneadas en UFW | En reglas UFW vivas, no en backup | Re-banear las 4-5 IPs conocidas a mano |
| Token de ECR (para deploys) | Token expira cada 12h, no está en backup | Renovar manual con script ecr-login.sh |
Lo más caro que aprendimos
1. Practicar el runbook antes del desastre
La primera vez que ejecutamos el runbook completo (en un VPS de prueba, no el real), descubrimos 4 cosas que faltaban en la documentación. Si el primer día que lo haces es bajo presión real, se duplica el RTO.
Recomendación: una vez por trimestre, levantar un VPS de prueba (200 USD/mes prorateados a 1 día = 7 USD/día) y ejecutar el runbook completo. Documentar lo que faltó. Repetir.
2. Los backups silenciosamente fallidos son lo peor
Tener cron job de backup pero nadie revisa logs = backup que no existe. Llevamos meses con un caso real donde un script fallaba pero el cron lo "ejecutaba": ningún error externo visible. Lo descubrimos por casualidad al revisar logs.
Solución: monitorear el resultado del backup, no su ejecución. Una alerta tipo "el último backup tiene más de 48h" es lo que hace falta. En nuestro stack vive en Grafana (ver post sobre stack de monitoreo).
3. Documentar lo que NO está en backup
La tabla de arriba ("lo que NO se recupera automático") es probablemente lo más valioso de este post. Sin ella, vas a la mitad del runbook y descubres que falta algo crítico (NPM proxy hosts, tunnel credentials).
Nuestra regla: cualquier configuración que no está en /opt o en un volumen Docker, anotarla en el runbook. Si es repetitiva, automatizarla. Si es única, documentar la fuente de verdad.
Si tu negocio depende del servidor
Si la respuesta a "¿qué pasa si tu servidor cae mañana?" no es "ejecuto este runbook documentado y vuelvo en 4 horas", tienes un problema. No alarmista — operativo.
Lo que hacemos:
- Auditamos tu setup actual: backups existentes, qué se respalda, dónde, cuál es el RTO real medido.
- Armamos el runbook de tu caso, documentado en tu wiki o repo.
- Validamos en frío con un VPS de prueba.
- Te dejamos la documentación + scripts.
Es un proyecto de 2-4 días. Cuesta una fracción de lo que cuesta un día de caída no planificada.
Hablemos. En 30 minutos identificamos cuán expuesto estás hoy y qué te conviene.
Relacionado: Plan de recuperación si el servidor cae · Stack de monitoreo Prometheus + Grafana + GlitchTip · Cloudflare Tunnel: cero puertos abiertos · Una hora 40 caída por una IP hardcodeada: postmortem.
Preguntas frecuentes
¿Cuántas veces hay que practicar el runbook?
Mínimo una vez por trimestre. Idealmente una vez al mes durante los primeros 3-6 meses después de armarlo, hasta que el equipo lo ejecute con fluidez. Después se puede bajar a trimestral o semestral. La regla: si pasas más de 6 meses sin ejecutarlo en frío, casi seguro algo cambió en tu infra que no actualizaste en el runbook. La fricción que aparece cuando lo ejecutas es información valiosa.
¿Es realmente posible un RTO de 4 horas?
Sí, una vez que lo practicaste 2-3 veces. La primera ejecución casi siempre toma 8-12 horas porque vas descubriendo cosas que faltan. La segunda baja a 6-8h. Después de 3-4 ejecuciones, 3-4 horas es realista. El cuello de botella suele ser ancho de banda para descargar backups grandes desde S3 — si tus volúmenes son grandes (Qdrant con miles de vectores, Postgres de varios GB), pesar.
¿Conviene tener un servidor de standby caliente en vez de runbook?
Depende del costo de caída. Si una hora caída cuesta cientos de USD, un servidor standby (~50% del costo del primario) y replicación en tiempo real (Postgres streaming, etc.) se justifica. Para PyME donde una caída de 4 horas duele pero no es catastrófica, el runbook con backups es la opción más sana costo/beneficio. La fórmula: si "costo de 4h caída" >> "costo mensual del servidor standby", invertí en standby.
¿Qué pasa si los backups en S3 también se corrompen?
Es por eso que recomendamos versionado de S3 + retención de 30-60 días + ocasionalmente verificar backups antiguos restaurándolos. La probabilidad de que TODOS los backups en S3 se corrompan al mismo tiempo es muy baja (S3 tiene 11 9s de durabilidad). Pero un backup tomado de una base ya corrupta sí puede pasar — por eso la verificación periódica con una restauración real es importante. Lo paranoico sería un segundo destino de backup en otro proveedor (Backblaze B2, Wasabi); lo recomendamos para infra crítica.
¿Cuánto cuesta tener todo este setup?
El stack de backups: ~5-15 USD/mes en S3 (depende del volumen — el nuestro mueve ~50 GB mensuales). El tiempo de armar el runbook por primera vez: 2-4 días si se hace bien. El mantenimiento anual: ~1-2 días por trimestre (practicar + actualizar). Si lo armamos nosotros para tu negocio: típicamente 1500-3500 USD según complejidad de infra. Es probablemente la mejor relación costo/beneficio que existe en seguridad operativa para PyME con servidor propio.