Escena Viva está configurada, observada, supervisada y empaquetada. Lo que todavía no está es en internet. Lucia no puede comprar sus entradas para el Festival de Jazz de Primavera porque la aplicación sigue corriendo en localhost:3000. Esta lección la pone en producción de verdad. Y lo hace por el camino que tiene más sentido para un proyecto de este tamaño: una plataforma como servicio, donde el proveedor se ocupa de las máquinas, el sistema operativo, el balanceador, los certificados TLS y el escalado, y tú te ocupas de tu aplicación. Vamos a ver el proceso completo con Heroku como caso canónico, entendiendo que sus conceptos son idénticos en Railway, Render, Fly.io o Cloud Run.
Contenido
- Qué es una PaaS y qué te ahorra
- Heroku y sus alternativas actuales
- Desplegar Escena Viva paso a paso
- Complementos gestionados y el problema de las conexiones
- Migraciones en el despliegue y cambios de esquema sin parada
- El sistema de ficheros efímero
- HTTPS, proxy inverso y
trust proxy - Escalado horizontal y vertical
- Dominios, preproducción y estrategias de despliegue
- Cómo se revierte un despliegue
- Costes y dimensionado con datos
- Qué es una PaaS y qué te ahorra
Una plataforma como servicio recibe tu código o tu imagen, la ejecuta y se encarga de todo lo que hay debajo: aprovisionar máquinas, parchear el sistema operativo, configurar el balanceador, emitir y renovar certificados TLS, recoger registros, reiniciar procesos caídos y escalar cuando se lo pides. A cambio te cobra dinero —bastante más por unidad de cómputo que un VPS— y control: no eliges la versión del núcleo, no instalas paquetes del sistema arbitrarios, no controlas dónde se ejecuta cada instancia y estás sujeto a los límites del proveedor.
| Modelo | Qué gestionas tú | Qué gestiona el proveedor | Coste | Cuándo elegirlo |
|---|---|---|---|---|
| VPS / servidor propio | Todo: SO, Node, proxy, TLS, PM2, copias, seguridad | Solo el hardware | El más bajo por CPU | Presupuesto ajustado y alguien con tiempo para operar |
| PaaS | Código y configuración | SO, TLS, balanceador, escalado, registros | Medio-alto | Equipos pequeños que quieren enviar producto |
| Contenedores gestionados (Cloud Run, ECS, App Runner) | Imagen y configuración | Ejecución, escalado, red | Medio, a menudo por uso real | Ya tienes la imagen de 11-04 y quieres control con poco trabajo |
| Serverless (Lambda, Functions) | Solo funciones | Absolutamente todo | Muy bajo si hay poco tráfico | Cargas esporádicas o picos extremos |
| Orquestador (Kubernetes) | Muchísimo | Muy poco | Alto en personas | Muchos servicios, varios equipos |
Para Escena Viva —tres salas, un equipo pequeño, picos concretos en los estrenos— la elección honesta está entre PaaS y contenedores gestionados. Serverless queda descartado: tenemos un consumidor de cola de larga vida, conexiones persistentes a PostgreSQL y arranques en frío que arruinarían el p99 que tanto cuidamos en el módulo 10. Kubernetes es artillería para cazar moscas.
- Heroku y sus alternativas actuales
Heroku inventó este modelo en 2007 y definió el vocabulario que todo el mundo usa: Procfile, dynos, buildpacks, complementos, fase de liberación. Merece la pena estudiarlo por eso, aunque hoy convenga ser honesto con dos cosas: su capa gratuita desapareció en noviembre de 2022 —el «despliega gratis en cinco minutos» de los tutoriales antiguos ya no existe— y hoy hay alternativas equivalentes, algunas con capa gratuita o de coste muy bajo.
| Proveedor | Modelo | Nota |
|---|---|---|
| Heroku | Buildpacks o contenedor | El original; vocabulario de referencia |
| Railway | Detecta el proyecto o usa Dockerfile | Muy simple; buen escalón para empezar |
| Render | Buildpacks o Dockerfile | El más parecido a Heroku hoy |
| Fly.io | Contenedores en el borde | Despliegue cerca del usuario; buen precio |
| DigitalOcean App Platform | Buildpacks o Dockerfile | Integrado con el resto de su ecosistema |
| Google Cloud Run | Solo contenedores | Escala a cero; pago por petición |
| AWS App Runner | Contenedor o código | Bien si ya vives en AWS |
Todos comparten los mismos conceptos, y son esos conceptos —no la interfaz de ningún proveedor— lo que hay que aprender: declarar tipos de proceso (uno web, otros de fondo); escuchar en el puerto que la plataforma asigna; recibir la configuración por variables de entorno; consumir servicios gestionados a través de una URL inyectada; ejecutar migraciones en una fase de liberación; aceptar un sistema de ficheros efímero; y tener TLS terminado en el borde, hablando HTTP por dentro. Aprendido eso, cambiar de proveedor es cuestión de horas.
- Desplegar Escena Viva paso a paso
El Procfile y los dos tipos de proceso
El Procfile declara qué procesos componen tu aplicación. Escena Viva tiene los mismos dos que declaramos en PM2 (11-03) y en compose (11-04):
Tres líneas y tres significados muy distintos:
webes el único tipo con nombre reservado: es el que recibe el tráfico HTTP del enrutador de la plataforma, y el único al que se le asigna un puerto.workeres un nombre libre para procesos de fondo. Es nuestro consumidor de la cola BullMQ del módulo 10: no escucha en ningún puerto, se conecta a Redis y procesa tareas. Se escala independientemente de la web, que es justo lo que queremos durante el estreno del Festival de Jazz.releasees especial: se ejecuta una sola vez por despliegue, antes de que la versión nueva reciba tráfico. Si falla, el despliegue se aborta. El apartado 5 vive de aquí.
Fíjate en que no hay npm start: por la misma razón que en Docker, un proceso intermedio de npm complica el reenvío de señales. Se invoca node directamente.
El puerto: process.env.PORT
Este es el punto donde más despliegues fallan la primera vez, y enlaza directamente con el módulo 4. La plataforma ejecuta varios contenedores en cada máquina y le asigna a cada uno un puerto arbitrario, que comunica por la variable PORT. Tu proceso debe escuchar en ese puerto. Si fijas el 3000, el enrutador de la plataforma llamará al puerto que asignó, no habrá nadie escuchando y el despliegue fallará con un tiempo de espera agotado. Nuestro esquema de 11-01 usa PUERTO, así que hay que aceptar ambos nombres:
// src/config/index.js — la plataforma inyecta PORT; nosotros usamos PUERTO.
// Damos prioridad a PORT porque, si existe, es imperativo.
const entornoNormalizado = { ...process.env, PUERTO: process.env.PORT ?? process.env.PUERTO };
const resultado = esquemaConfiguracion.safeParse(entornoNormalizado);
// src/servidor.js — escuchar en 0.0.0.0, NO en 127.0.0.1: dentro de un
// contenedor, 127.0.0.1 solo es alcanzable desde el propio contenedor.
servidor.listen(configuracion.puerto, '0.0.0.0');La regla general: en producción nunca se fija un puerto. En desarrollo eliges el 3000 por comodidad; en producción lo dicta el entorno. Es el mismo principio de los doce factores que gobierna toda la configuración.
engines y la construcción
package.json ya declara la versión de Node, y las PaaS lo leen para elegir el intérprete:
{
"engines": { "node": ">=24.5.0 <25", "npm": ">=10" },
"scripts": { "start": "node src/servidor.js", "migrar": "sequelize-cli db:migrate" }
}Sin engines, la plataforma elige la versión que le parezca —normalmente la LTS del momento— y puedes acabar en producción con una versión distinta de la que probaste. Un rango como este permite parches de seguridad sin saltar de mayor. En la construcción, la plataforma detecta Node y ejecuta npm ci --omit=dev si existe package-lock.json (por eso está versionado desde el módulo 5) y luego npm run build si existe. Si prefieres control total, casi todas aceptan tu Dockerfile de 11-04, y entonces la construcción es exactamente la que tú escribiste. Para Escena Viva es lo recomendable: la imagen probada en CI es la que se ejecuta en producción, sin traducciones intermedias.
Configuración: el panel, no el .env
En la PaaS no existe el .env (y no debe existir: el .dockerignore de 11-04 se encarga). Las variables se definen en el panel o por CLI:
heroku config:set NODE_ENV=production NIVEL_REGISTRO=info \
CONFIAR_EN_PROXY=true ORIGEN_CORS=https://escenaviva.test --app escena-viva
heroku config:set JWT_SECRETO=$(openssl rand -hex 32) \
SESION_SECRETO=$(openssl rand -hex 32) \
CSRF_SECRETO=$(openssl rand -hex 32) --app escena-viva
heroku config --app escena-viva # Revisar lo que hayAquí se cobra otro dividendo de 11-01: si olvidas una variable obligatoria, la validación zod mata el proceso en el arranque con un mensaje claro, la plataforma detecta que la versión nueva no arrancó y mantiene la anterior en servicio. Fallar rápido convierte un error de configuración en un despliegue abortado en lugar de en una caída. Dos avisos: cambiar una variable reinicia la aplicación en la mayoría de plataformas (coherente con la decisión de 11-01 de reiniciar en lugar de recargar en caliente), y cualquiera con acceso al panel ve todos los secretos en claro, así que el control de accesos del proyecto es parte de tu seguridad.
- Complementos gestionados y el problema de las conexiones
Los complementos son servicios que la plataforma aprovisiona y conecta a tu aplicación inyectando su URL como variable de entorno:
heroku addons:create heroku-postgresql:standard-0 --app escena-viva
heroku addons:create heroku-redis:premium-0 --app escena-viva
# MongoDB suele venir de un tercero: MongoDB Atlas.Se crean DATABASE_URL y REDIS_URL. Como nuestro esquema espera URL_POSTGRES y URL_REDIS, se normaliza en el mismo sitio que el puerto:
const entornoNormalizado = {
...process.env,
PUERTO: process.env.PORT ?? process.env.PUERTO,
URL_POSTGRES: process.env.DATABASE_URL ?? process.env.URL_POSTGRES,
URL_REDIS: process.env.REDIS_URL ?? process.env.URL_REDIS,
};Un solo fichero absorbe todas las particularidades del proveedor. Ese es exactamente el beneficio del punto único de lectura de process.env. Detalle práctico: la URL de la base de datos puede cambiar sin avisar (mantenimiento, conmutación por error). Nunca la copies a otro sitio ni la caches: léela siempre del entorno.
El problema de las conexiones, que es serio
Aquí está el error que tumba aplicaciones el día del estreno. Los planes gestionados limitan el número de conexiones simultáneas. Un plan pequeño de PostgreSQL puede permitir 20. Y la aritmética es implacable: La aritmética es instancias web × pool + instancias worker × (pool + conexiones de BullMQ). Escenario realista para el estreno del Festival de Jazz:
| Concepto | Cantidad | Pool | Conexiones |
|---|---|---|---|
| Instancias web | 4 | 10 | 40 |
| Instancias worker | 3 | 5 | 15 |
| Fase de liberación (migraciones) | 1 | 2 | 2 |
| Total | 57 |
Con un límite de 20, la mitad de los procesos no consiguen conectar. Y lo peor: la aplicación arranca igual (el pool crea conexiones bajo demanda) y falla bajo carga, que es cuando más caro sale. Tres remedios, en orden:
- Dimensionar el pool según las instancias, que es exactamente lo que hace
src/db/sequelize.jsdesde el módulo 7 con el número de trabajadores. Aquí la variable es el número de instancias de la plataforma:
// src/db/sequelize.js (fragmento)
const LIMITE_CONEXIONES = Number(process.env.LIMITE_CONEXIONES_BD ?? 20);
const INSTANCIAS = Number(process.env.INSTANCIAS_ESPERADAS ?? 4);
// Reservamos 4 conexiones para migraciones y para conectarse a depurar.
const poolMaximo = Math.max(2, Math.floor((LIMITE_CONEXIONES - 4) / INSTANCIAS));-
Usar un agrupador de conexiones externo (PgBouncer, o el
pgbouncerque ofrecen algunos planes). Multiplexa muchas conexiones de aplicación sobre pocas de servidor. Ojo: en modo transacción no se pueden usar sentencias preparadas de sesión, y hay que decírselo a Sequelize. -
Subir de plan. A veces es simplemente la respuesta correcta, y cuesta menos que un rediseño.
Con Redis pasa lo mismo, agravado porque BullMQ abre varias conexiones por trabajador (una para escuchar eventos bloqueantes, otra para comandos). Con CONCURRENCIA_COLA=5 y 3 instancias, cuenta 30-40 conexiones a Redis, no 3.
- Migraciones en el despliegue y cambios de esquema sin parada
La fase de liberación
La línea release: npm run migrar del Procfile se ejecuta una vez por despliegue, después de construir y antes de que la versión nueva reciba tráfico. Si falla, el despliegue se cancela y la versión antigua sigue sirviendo. Es la solución al problema que planteamos en 11-04: las migraciones no van en el arranque de la aplicación. Si fuesen en el CMD, con cuatro instancias arrancando a la vez tendrías cuatro procesos migrando el mismo esquema en paralelo. En la fase de liberación se ejecutan una sola vez, en un contenedor efímero, con la salida visible en los registros del despliegue.
Cambios de esquema sin parada: expandir → migrar → contraer
Y aquí viene el concepto más valioso de la lección. Durante un despliegue conviven, aunque sea unos segundos, la versión antigua y la nueva del código sobre la misma base de datos. Con despliegue azul/verde o canario, esa convivencia dura minutos u horas. Por tanto: toda migración debe ser compatible con el código anterior y con el nuevo. Un ALTER TABLE ... RENAME COLUMN rompe instantáneamente todas las instancias antiguas, que siguen consultando el nombre viejo. La técnica es dividir el cambio en tres despliegues. Ejemplo real: en Escena Viva queremos renombrar precio (un número decimal, herencia de un modelo antiguo) por precioCentimos (entero), respetando nuestra convención de dinero en céntimos. Despliegue 1 — Expandir. La migración añade sin quitar nada:
// migrations/20260815100000-anadir-precio-centimos.js
'use strict';
module.exports = {
async up(consultas, tiposDatos) {
// Columna nueva y ANULABLE: el codigo antiguo la ignora sin problema.
await consultas.addColumn('sesiones', 'precioCentimos', {
type: tiposDatos.INTEGER, allowNull: true,
});
// Y se rellena con los datos existentes.
await consultas.sequelize.query(
'UPDATE sesiones SET "precioCentimos" = ROUND(precio * 100) WHERE "precioCentimos" IS NULL'
);
},
async down(consultas) {
await consultas.removeColumn('sesiones', 'precioCentimos');
},
};El código de este despliegue escribe en las dos columnas y lee de la antigua. Ambas versiones funcionan. Despliegue 2 — Migrar. El código pasa a leer de precioCentimos y sigue escribiendo en las dos. Si hay que revertir a la versión 1, todo sigue funcionando porque ambas columnas están al día. Despliegue 3 — Contraer. El código deja de tocar precio, y solo entonces la migración lo elimina con await consultas.removeColumn('sesiones', 'precio').
| Despliegue | Migración | Código escribe | Código lee | ¿Se puede revertir? |
|---|---|---|---|---|
| 1. Expandir | Añade precioCentimos y rellena |
Ambas | precio |
Sí |
| 2. Migrar | Ninguna | Ambas | precioCentimos |
Sí |
| 3. Contraer | Elimina precio |
precioCentimos |
precioCentimos |
Solo al despliegue 2 |
Es más lento y son tres pasos en lugar de uno. A cambio, ninguno de los tres exige parar la aplicación ni impide revertir. En una plataforma de venta de entradas, donde una parada de dos minutos durante el estreno son cientos de ventas perdidas, esa lentitud es exactamente lo que quieres. Regla práctica para migraciones grandes: cuidado con ALTER TABLE que bloquea la tabla entera. Añadir una columna anulable es instantáneo en PostgreSQL moderno; rellenar dos millones de filas no lo es, y conviene hacerlo por lotes.
- El sistema de ficheros efímero
El disco de una instancia es efímero. Cuando la instancia se reinicia —cada despliegue, cada cambio de configuración, cada reciclaje de la plataforma— todo lo escrito desaparece. Y además cada instancia tiene su propio disco: lo que escribe una no lo ve otra. Esto invalida por completo lo que hicimos en el módulo 3 con el directorio informes/, donde guardábamos los PDF de las entradas generados por el pool de worker threads. En producción sobre PaaS: el PDF EV-2026-004182.pdf que genera el worker no existe para la instancia web que atenderá la descarga de Lucia, y aunque existiera desaparecería en el siguiente despliegue. La solución es almacenamiento de objetos —S3, Cloud Storage, R2, Spaces—, con este flujo:
// src/servicios/almacen-informes.js
'use strict';
const { S3Client, PutObjectCommand, GetObjectCommand } = require('@aws-sdk/client-s3');
const { getSignedUrl } = require('@aws-sdk/s3-request-presigner');
const { configuracion } = require('../config/index.js');
const cliente = new S3Client({ region: configuracion.almacen.region });
const Bucket = configuracion.almacen.cubo;
async function guardarEntradaPdf({ codigoEntrada, contenido }) {
const clave = `entradas/${codigoEntrada}.pdf`;
await cliente.send(
new PutObjectCommand({
Bucket, Key: clave, Body: contenido, ContentType: 'application/pdf',
ACL: 'private', // Nunca publico: las entradas son nominales.
})
);
return clave;
}
// El PDF no pasa por nuestro servidor: se firma una URL temporal.
async function urlDescargaTemporal(clave, segundos = 300) {
return getSignedUrl(cliente, new GetObjectCommand({ Bucket, Key: clave }), {
expiresIn: segundos,
});
}
module.exports = { guardarEntradaPdf, urlDescargaTemporal };Fíjate en el patrón de la URL firmada: el worker sube el PDF al almacén y guarda la clave; cuando Lucia pide su entrada, la API comprueba que es suya (exigir-propiedad.js del módulo 8) y devuelve una URL firmada que caduca en cinco minutos. El fichero nunca atraviesa nuestro servidor, así que no consume ni memoria ni ancho de banda de la instancia. La misma regla vale para cualquier estado local: subidas de usuarios, cachés en disco, ficheros de sesión, ficheros de registro. Nada persistente en el disco de la instancia. Las sesiones y el límite de peticiones ya están en Redis desde el módulo 10, precisamente por esto.
- HTTPS, proxy inverso y
trust proxy
trust proxyAquí cumplimos la promesa del módulo 8: HTTPS y certificados. La buena noticia es que en una PaaS no gestionas certificados. La plataforma pone un proxy inverso en el borde que termina TLS: recibe HTTPS del navegador, valida el certificado (emitido y renovado automáticamente, normalmente vía Let's Encrypt) y habla HTTP plano con tu instancia por la red interna.
flowchart LR
N[Navegador de Lucia] -->|HTTPS 443| B[Proxy del borde: TLS termina aqui]
B -->|HTTP + cabeceras X-Forwarded-*| W1[Instancia web 1]
B -->|HTTP| W2[Instancia web 2]
W1 --> R[(Redis)]
W1 --> P[(PostgreSQL)]
Tu aplicación no necesita https.createServer ni certificados. Node sirve HTTP y eso es correcto. Pero tiene una consecuencia importante, y es la promesa que dejamos pendiente en el módulo 6. Desde el punto de vista de Express, todas las peticiones vienen del proxy: req.ip es la IP interna del balanceador, req.protocol es http y req.secure es false. La información real viaja en cabeceras:
| Cabecera | Contenido |
|---|---|
X-Forwarded-For |
Cadena de IP: la del cliente primero |
X-Forwarded-Proto |
Protocolo original (https) |
X-Forwarded-Host |
Host solicitado por el cliente |
Express solo las usa si se lo dices, en src/app.js:
// Numero de proxies de confianza delante. La PaaS pone uno.
// NUNCA 'true' en produccion: confiar en cualquier cabecera permite
// que un cliente falsifique su IP poniendo X-Forwarded-For a mano.
if (configuracion.confiarEnProxy) app.set('trust proxy', 1);Sin trust proxy, tres cosas se rompen a la vez:
- El límite de peticiones por IP (
express-rate-limit, módulo 6, con almacén Redis del módulo 10) ve la misma IP —la del proxy— para todos los usuarios. Resultado: o el límite global se agota en segundos y bloquea a todo el mundo, o lo pones tan alto que no protege de nada. Es el fallo más grave de los tres. - Las cookies
secureno se envían, porque Express cree que la conexión no es segura. Las sesiones dejan de funcionar sin ningún mensaje de error útil. - Los registros de 11-02 guardan la IP del balanceador en lugar de la del usuario, y toda investigación de abuso se vuelve imposible.
El número importa: app.set('trust proxy', 1) significa «confía en el último salto». Si pones true, Express confía en toda la cadena de X-Forwarded-For, y como cualquiera puede enviar esa cabecera, cualquiera puede fingir su IP y saltarse el límite de peticiones. Cuenta los proxies reales y pon ese número. Dos añadidos que sí dependen de ti: redirigir HTTP a HTTPS (muchas plataformas lo hacen, pero conviene comprobarlo) y activar HSTS, que ya viene con helmet desde el módulo 6 y le dice al navegador que no vuelva a intentar HTTP nunca.
- Escalado horizontal y vertical
| Vertical | Horizontal | |
|---|---|---|
| Qué haces | Instancias más grandes | Más instancias |
| Límite | El tamaño máximo que ofrezcan | Prácticamente ninguno |
| Tolerancia a fallos | Si cae, cae todo | Si cae una, quedan las demás |
| Requisito | Ninguno | La aplicación no puede tener estado |
| En Node | Poco útil: un proceso usa un núcleo | El camino natural |
El escalado vertical es especialmente flojo en Node: un proceso usa un núcleo, así que pasar a una máquina de 8 CPU no acelera nada por sí solo. Por eso existía el cluster del módulo 10. En una PaaS, la unidad de escalado ya es la instancia, así que se escala horizontalmente y se ejecuta un proceso por instancia (misma doctrina que en contenedores, 11-04).
Y ahora lo importante: Escena Viva ya está preparada para esto, y no por casualidad. Cada decisión del curso apuntaba aquí:
| Requisito de escalado horizontal | Dónde lo resolvimos |
|---|---|
| Sin estado en memoria del proceso | Diseño desde el módulo 6 |
| Sesiones compartidas | connect-redis (módulo 10) |
| Límite de peticiones compartido | rate-limit-redis (módulo 10) |
| Caché compartida | src/cache/catalogo.js en Redis (módulo 10) |
| Trabajo de fondo fuera del proceso web | BullMQ y el consumidor (módulo 10) |
| Ficheros fuera del disco local | Almacén de objetos (apartado 6) |
| Apagado ordenado al reducir instancias | SIGTERM (módulo 6) |
| Sonda para retirar tráfico | /salud/listo (11-02) |
Si algo de esto faltara, escalar a 4 instancias produciría errores intermitentes imposibles de reproducir: sesiones que se pierden al saltar de instancia, límites que no limitan, PDF que no aparecen. Que la lista esté completa es la razón por la que ps:scale web=4 simplemente funciona.
- Dominios, preproducción y estrategias de despliegue
Dominios. Se añade el dominio, se apunta un CNAME al que indique el proveedor y este emite el certificado automáticamente. Recuerda actualizar ORIGEN_CORS con el dominio real: es el error de configuración más frecuente al estrenar dominio. Preproducción. Una segunda aplicación con la misma base de código y su propia configuración:
| Producción | Preproducción | |
|---|---|---|
| Aplicación | escena-viva |
escena-viva-pre |
NODE_ENV |
production |
staging |
| Base de datos | Plan grande, con copias | Plan pequeño, datos anonimizados |
| Instancias | web=4, worker=3 | web=1, worker=1 |
| Pasarela de pago | Real | Modo prueba |
| Correo | Real | A un buzón de captura .test |
Dos avisos que valen dinero: los datos de preproducción deben estar anonimizados (una copia cruda de producción es una filtración de datos personales esperando a ocurrir), y la pasarela en modo prueba evita cobrar de verdad a nadie durante una demo. Estrategias de despliegue:
| Estrategia | Cómo funciona | A favor | En contra |
|---|---|---|---|
| Recreate | Para todo y arranca lo nuevo | Simple | Corte de servicio |
| Progresivo (por defecto en casi todas) | Reemplaza instancias de una en una | Sin corte | Conviven dos versiones |
| Azul/verde | Se levanta el entorno completo nuevo y se conmuta el tráfico de golpe | Reversión instantánea | Doble coste durante la ventana |
| Canario | Se envía el 5 % del tráfico a la versión nueva, se observa y se sube | Limita el daño de un fallo | Requiere métricas y enrutado por peso |
El progresivo es el habitual y es el que impone la disciplina de expandir → migrar → contraer del apartado 5: durante el reemplazo conviven las dos versiones sobre el mismo esquema. El canario brilla justo en un caso como el nuestro: si el 5 % del tráfico va a la versión nueva y la tasa de error de /api/compras sube (métrica de 11-02), se revierte antes de que lo note el 95 % restante. Para hacerlo bien necesitas las métricas por versión, lo que exige etiquetar los registros y métricas con la versión desplegada — otra razón para registrar la versión al arrancar, como recomendamos en 11-02.
- Cómo se revierte un despliegue
Esto es lo primero que hay que saber hacer. Antes de desplegar por primera vez, asegúrate de saber revertir. Un equipo que sabe revertir en treinta segundos despliega con tranquilidad; uno que no, despliega los martes por la mañana con miedo.
$ heroku releases --app escena-viva
v52 Deploy 8f3a1c2 [email protected] 2026/08/15 18:02
v51 Deploy 4b9e0d7 [email protected] 2026/08/15 11:40
v50 Set JWT_SECRETO [email protected] 2026/08/14 09:15
$ heroku releases:rollback v51 --app escena-vivaCada versión es inmutable y contiene código y configuración, así que revertir devuelve el conjunto exacto que funcionaba. En segundos. Tres cosas que hay que tener claras sobre la reversión:
- Revertir el código no revierte la base de datos. Si la versión v52 ejecutó una migración destructiva, volver a v51 deja código antiguo sobre un esquema nuevo. Por eso las migraciones se diseñan compatibles hacia atrás. La estrategia de tres pasos no es purismo: es lo que hace que revertir sea seguro.
- Revertir primero, investigar después. Igual que con un secreto filtrado en 11-01: primero se restaura el servicio, luego se busca la causa. El impulso de «déjame que mire cinco minutos» ha alargado muchas incidencias.
- Ensaya la reversión. Hazla una vez en preproducción, con cronómetro. Descubrirás detalles —permisos que falta dar, quién tiene acceso, cuánto tarda— que no quieres descubrir con la venta caída.
Un patrón complementario muy útil: desacoplar despliegue de activación con banderas de funcionalidad. Despliegas el código de la nueva venta anticipada apagado, lo activas cuando toca y, si va mal, lo apagas sin desplegar nada. Recuerda de 11-01 que esas banderas van en base de datos, no en variables de entorno.
- Costes y dimensionado con datos
Una PaaS cobra por instancia y hora, más los complementos. Un ejemplo realista para Escena Viva en operación normal:
| Concepto | Cantidad | Coste mensual aproximado |
|---|---|---|
| Instancias web | 2 medianas | 100 € |
| Instancias worker | 1 mediana | 50 € |
| PostgreSQL gestionado | Plan estándar | 50 € |
| Redis gestionado | Plan pequeño | 15 € |
| Almacén de objetos | 50 GB + tráfico | 5 € |
| Registros y métricas | Retención de 14 días | 20 € |
| Total | ~240 €/mes |
Un VPS equivalente costaría 40-60 €, pero habría que sumar el tiempo de alguien manteniéndolo, actualizándolo, gestionando copias de seguridad y respondiendo de madrugada. Con una persona, la PaaS suele salir más barata en total. Y el consejo que cierra el módulo 10 y esta lección: dimensiona con datos, no con intuición. Ya sabes medir. Antes de decidir cuántas instancias necesitas para el estreno del Festival de Jazz, lanza autocannon contra preproducción con un perfil de carga realista, mira el p99, el retraso del bucle de eventos y el uso de CPU, y calcula. «Vamos a poner ocho instancias por si acaso» cuesta dinero todos los meses y a menudo no ayuda: si el cuello de botella es la base de datos, ocho instancias solo consumen más conexiones y empeoran las cosas. Consejos de coste que valen la pena: escala antes de un pico previsto (el estreno tiene fecha) y reduce después; vigila el gasto en registros, que crece con el tráfico y sorprende a todo el mundo; y activa alertas de facturación, porque un bucle de reintentos mal puesto puede multiplicar la factura en un fin de semana.
Errores Comunes y Consejos
- Fijar el puerto en producción. El despliegue falla con tiempo de espera agotado. Usa
process.env.PORTy escucha en0.0.0.0. - Olvidar
trust proxy. El límite por IP se rompe, las cookies seguras no viajan y los registros guardan la IP del balanceador. trust proxyatrue. Permite falsificar la IP de origen. Pon el número de proxies reales.- Escribir ficheros en el disco de la instancia. Se pierden en cada despliegue y no los ven las demás instancias.
- Migraciones en el arranque en lugar de en la fase de liberación. Se ejecutan en paralelo por cada instancia.
- Ignorar el límite de conexiones del plan. Instancias × pool agota el plan y falla justo bajo carga.
- Desplegar sin saber revertir. El error más caro de todos.
- Consejo: documenta el procedimiento de reversión en el
READMEy ensáyalo. Debe poder ejecutarlo cualquiera del equipo a las tres de la madrugada. - Consejo: despliega a menudo y pequeño. Un despliegue con dos días de trabajo es un despliegue de bajo riesgo; uno con dos meses es una noche en vela.
Ejercicios
Ejercicio 1 — Auditoría de preparación
Repasa Escena Viva y enumera todo lo que rompería al pasar a 4 instancias sin haber hecho el trabajo de los módulos 10 y 11: estado en memoria, ficheros locales, temporizadores, caché. Para cada punto, indica el mecanismo que lo resuelve y en qué módulo se introdujo.
Ejercicio 2 — Migración sin parada
Escena Viva necesita añadir el campo nombreAsistente a la tabla entradas, obligatorio a partir de ahora. Diseña la secuencia completa de expandir → migrar → contraer indicando, para cada uno de los tres despliegues, qué hace la migración y qué hace el código.
Ejercicio 3 — Presupuesto de conexiones
Con un plan de PostgreSQL de 20 conexiones y uno de Redis de 30, calcula la configuración máxima de instancias web y worker para Escena Viva, sabiendo que cada instancia web usa un pool de PostgreSQL y que cada worker usa pool más 2 conexiones de BullMQ por unidad de concurrencia. Propón valores concretos.
Soluciones
Ejercicio 1.
| Qué rompería | Por qué | Solución | Módulo |
|---|---|---|---|
| Sesiones en memoria | Cada instancia tiene las suyas; al saltar, el usuario aparece desconectado | connect-redis |
M10 |
| Límite de peticiones en memoria | El límite se multiplica por el número de instancias | rate-limit-redis |
M10 |
| Caché del catálogo en memoria | Invalidaciones inconsistentes: una instancia sirve datos viejos | Redis con TTL | M10 |
PDF en informes/ |
El worker escribe donde la web no lee, y se pierden al reiniciar | Almacén de objetos | 11-05 |
Tareas con setInterval en el proceso web |
Se ejecutan N veces, una por instancia | Cola BullMQ con tarea programada | M10 |
| Idempotencia en memoria | Una compra reenviada a otra instancia se duplicaría | Claves en Redis | M10 |
| Contadores de métricas locales | Cada instancia expone los suyos | Agregación en Prometheus | 11-02 |
Ejercicio 2.
Despliegue 1 — Expandir. Migración: addColumn('entradas', 'nombreAsistente', { type: STRING, allowNull: true }) y relleno de las filas existentes con el nombre del comprador. Código: escribe el campo si lo recibe, no lo exige y no lo lee para nada. El zod del endpoint lo marca como opcional. Despliegue 2 — Migrar. Sin migración. Código: el esquema zod pasa a exigir nombreAsistente en las compras nuevas y la interfaz lo muestra. Se puede revertir al despliegue 1 sin problema, porque la columna sigue siendo anulable en la base de datos. Despliegue 3 — Contraer. Migración: changeColumn para poner allowNull: false, una vez verificado con una consulta que no queda ninguna fila nula. Código: sin cambios. Este orden es esencial: si pusieras NOT NULL en el despliegue 1, todas las instancias antiguas —que no envían el campo— empezarían a fallar al insertar.
Ejercicio 3.
PostgreSQL, 20 conexiones: reservamos 4 para migraciones y acceso manual, quedan 16. Con web=3 y worker=2, y pool de 3 para web y 2 para worker, salen 3×3 + 2×2 = 13 conexiones y encaja con margen; con web=4 y pool de 4 serían 16 solo para web, sin dejar nada al worker. Redis, 30 conexiones. BullMQ abre unas 2 por unidad de concurrencia, más unas 2 por instancia web para sesiones, límites y caché. Web: 3 × 2 = 6. Worker: 2 instancias × (5 de concurrencia × 2) = 20, total 26: justo, pero cabe. Con CONCURRENCIA_COLA=3 en lugar de 5 serían 2 × 6 = 12, total 18, mucho más holgado.
Configuración propuesta: web=3 con pool 3, worker=2 con pool 2 y CONCURRENCIA_COLA=3. Y la conclusión importante: antes de escalar hay que hacer esta cuenta, porque el síntoma de agotar conexiones (errores intermitentes bajo carga) es de los más difíciles de diagnosticar en caliente.
Conclusión
Escena Viva está en internet. Un Procfile declara sus tres piezas —el proceso web, el worker que consume la cola del módulo 10 y la fase release que ejecuta las migraciones una sola vez—, escucha en el puerto que asigna la plataforma, lee su configuración de las variables del proveedor en lugar de un .env, consume PostgreSQL y Redis gestionados con el pool dimensionado para no agotar el plan, guarda los PDF de las entradas en un almacén de objetos porque el disco de la instancia es efímero, y sirve HTTPS con el certificado que gestiona el borde mientras Express confía en un único proxy y recupera así la IP real de cada usuario: la promesa del módulo 6 cumplida. Escala horizontalmente porque cada pieza de estado se sacó del proceso a su debido tiempo, cambia de esquema sin parada con expandir → migrar → contraer, y se revierte en segundos, que es lo primero que aprendimos a hacer. Queda un último eslabón, y es el que convierte todo esto en rutina. Ahora mismo desplegar sigue siendo una secuencia de comandos que alguien ejecuta a mano, en el orden correcto, esperando no olvidar ninguno. En la última lección del módulo, Integración y Despliegue Continuos, automatizamos la cadena entera: un fichero de GitHub Actions que instala con npm ci, pasa el linter, ejecuta las pruebas unitarias y de integración contra PostgreSQL, MongoDB y Redis levantados como servicios, exige un umbral de cobertura, audita las dependencias, construye la imagen una sola vez y la promueve entre entornos, ejecuta pruebas de humo contra /salud/listo y revierte sola si fallan. Hasta convertir subir una versión en algo aburrido.
Curso de Node.js: De Principiante a Avanzado
Módulo 1: Introducción a Node.js
- ¿Qué es Node.js?
- Instalación y Configuración del Entorno
- Tu Primer Programa en Node.js
- El REPL de Node.js
- JavaScript Moderno para Node.js
- El Proyecto del Curso: la Plataforma Escena Viva
Módulo 2: Conceptos Básicos
- Arquitectura de Node.js
- El Bucle de Eventos (Event Loop)
- Callbacks y Programación Asíncrona
- Promesas y async/await
- Eventos y EventEmitter
- Módulos CommonJS y require()
- Módulos ES e Interoperabilidad
Módulo 3: Sistema de Archivos y E/S
- Lectura y Escritura de Archivos
- El Módulo fs a Fondo
- Rutas Multiplataforma con el Módulo path
- Trabajando con Streams
- Streams de Transformación y pipeline
- Buffers y Datos Binarios
Módulo 4: HTTP y Servidores Web
- Creando un Servidor HTTP Simple
- Manejo de Solicitudes y Respuestas
- Enrutamiento Manual
- Sirviendo Archivos Estáticos
- Recibiendo Datos: Cuerpos de Petición y JSON
- Consumiendo APIs Externas desde Node.js
Módulo 5: NPM y Gestión de Paquetes
- Introducción a NPM y package.json
- Instalación y Uso de Paquetes
- Versionado Semántico y package-lock
- Scripts de npm y Automatización del Proyecto
- Creación y Publicación de Paquetes
- Seguridad y Mantenimiento de Dependencias
Módulo 6: Framework Express.js
- Introducción a Express.js
- Configuración de una Aplicación Express
- Enrutamiento en Express
- Middleware
- Middleware de Terceros Esenciales
- Validación de Datos de Entrada
- Manejo de Errores
Módulo 7: Bases de Datos y ORMs
- Introducción a las Bases de Datos
- Usando MongoDB con Mongoose
- Operaciones CRUD
- Relaciones, Poblado y Consultas Avanzadas
- Usando Bases de Datos SQL con Sequelize
- Migraciones, Transacciones y Datos de Prueba
Módulo 8: Autenticación y Autorización
- Introducción a la Autenticación
- Registro de Usuarios y Hash de Contraseñas
- Sesiones y Cookies con Passport.js
- Autenticación con JWT
- Control de Acceso Basado en Roles
- Buenas Prácticas de Seguridad en APIs
Módulo 9: Pruebas y Depuración
- Introducción a las Pruebas
- Pruebas Unitarias con Mocha y Chai
- Dobles de Prueba con Sinon
- Pruebas de Integración
- Cobertura y Automatización de las Pruebas
- Depuración de Aplicaciones Node.js
Módulo 10: Temas Avanzados
- El Módulo Cluster
- Hilos de Trabajo (Worker Threads)
- Caché y Colas de Trabajo con Redis
- Optimización del Rendimiento
- Construcción de APIs RESTful
- GraphQL con Node.js
Módulo 11: Despliegue y DevOps
- Configuración y Variables de Entorno
- Registro y Monitorización en Producción
- Usando PM2 para la Gestión de Procesos
- Empaquetado con Docker
- Desplegando en Heroku y Otras PaaS
- Integración y Despliegue Continuos
