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

  1. Qué es una PaaS y qué te ahorra
  2. Heroku y sus alternativas actuales
  3. Desplegar Escena Viva paso a paso
  4. Complementos gestionados y el problema de las conexiones
  5. Migraciones en el despliegue y cambios de esquema sin parada
  6. El sistema de ficheros efímero
  7. HTTPS, proxy inverso y trust proxy
  8. Escalado horizontal y vertical
  9. Dominios, preproducción y estrategias de despliegue
  10. Cómo se revierte un despliegue
  11. Costes y dimensionado con datos

  1. 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.

  1. 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.

  1. 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):

web: node src/servidor.js
worker: node src/procesos/consumidor-entradas.js
release: npm run migrar

Tres líneas y tres significados muy distintos:

  • web es 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.
  • worker es 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.
  • release es 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 hay

Aquí 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.

  1. 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:

  1. Dimensionar el pool según las instancias, que es exactamente lo que hace src/db/sequelize.js desde 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));
  1. Usar un agrupador de conexiones externo (PgBouncer, o el pgbouncer que 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.

  2. 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.

  1. 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.

  1. 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.

  1. HTTPS, proxy inverso y trust proxy

Aquí 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:

  1. 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.
  2. Las cookies secure no 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.
  3. 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.

  1. 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).

heroku ps:scale web=4 worker=3 --app escena-viva

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.

  1. 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.

  1. 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-viva

Cada 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:

  1. 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.
  2. 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.
  3. 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.

  1. 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.PORT y escucha en 0.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 proxy a true. 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 README y 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

Módulo 2: Conceptos Básicos

Módulo 3: Sistema de Archivos y E/S

Módulo 4: HTTP y Servidores Web

Módulo 5: NPM y Gestión de Paquetes

Módulo 6: Framework Express.js

Módulo 7: Bases de Datos y ORMs

Módulo 8: Autenticación y Autorización

Módulo 9: Pruebas y Depuración

Módulo 10: Temas Avanzados

Módulo 11: Despliegue y DevOps

Módulo 12: Proyectos del Mundo Real

© Copyright 2026. Todos los derechos reservados