Escena Viva ya se configura como es debido y ya cuenta lo que hace: registros estructurados con trazabilidad por petición, métricas en /metricas, sondas /salud/vivo y /salud/listo. Pero todo eso sigue arrancándose a mano. Alguien entra por SSH al servidor, escribe npm start, ve pasar las primeras líneas de registro y cierra la terminal. Y en ese momento la aplicación se muere. Esta lección resuelve el problema del proceso: quién lo arranca, quién lo vigila, quién lo levanta cuando cae y quién lo recarga cuando despliegas una versión nueva sin cortar ni una compra a medias. La herramienta será PM2, pero lo importante son los conceptos: se aplican igual si mañana usas systemd o un orquestador.

Contenido

  1. El problema: npm start en una terminal
  2. Qué es un supervisor de procesos y cuál elegir
  3. PM2: instalación y comandos de referencia
  4. El fichero de ecosistema de Escena Viva
  5. Modo cluster de PM2 frente a nuestro src/cluster.js
  6. reload frente a restart: recarga sin cortes
  7. Reinicio automático y protección contra bucles
  8. Reinicio por memoria como red de seguridad
  9. Arranque al iniciar la máquina
  10. Registros de PM2 y su rotación
  11. Variables de entorno y secretos en PM2
  12. Despliegue y monitorización
  13. Lo que PM2 no resuelve

  1. El problema: npm start en una terminal

Cuando ejecutas npm start en una sesión SSH, el proceso de Node es hijo de tu shell, y tu shell es hija del demonio SSH. Al desconectar, el núcleo envía SIGHUP a la sesión y todo el árbol se derrumba. Tu API desaparece. Los apaños clásicos —nohup npm start &, screen, tmux— resuelven solo esa parte, y dejan cinco problemas sin tocar:

  • Si el proceso cae por un uncaughtException (que, recuerda, en 11-02 decidimos que termina el proceso deliberadamente), nadie lo levanta.
  • Si el servidor se reinicia, la aplicación no vuelve sola.
  • Los registros van a un fichero infinito que llenará el disco.
  • No hay forma de desplegar sin corte: parar y arrancar deja un hueco de segundos con errores 502.
  • No hay visibilidad: cuántos procesos hay, cuánta memoria usan, cuántas veces se han reiniciado.

Eso es exactamente lo que hace un supervisor.

  1. Qué es un supervisor de procesos y cuál elegir

Un supervisor es un programa cuya única función es mantener otros programas vivos: los arranca, los vigila, los reinicia si terminan, redirige su salida y sobrevive al cierre de tu sesión.

Supervisor Qué es A favor En contra Cuándo elegirlo
PM2 Supervisor en Node, específico para aplicaciones Node Cluster integrado, recarga sin cortes, CLI cómoda, panel en terminal Es un proceso más que puede fallar; específico de Node VPS o servidor propio con una o pocas aplicaciones Node
systemd El init del sistema en Linux Ya está instalado, integrado con el arranque, journald, cgroups Sin cluster propio (necesitas src/cluster.js o plantillas de unidad), sintaxis árida Servidor Linux donde quieras cero dependencias extra
Docker + restart: always El motor de contenedores vigila el contenedor El entorno viaja con la app; un único mecanismo para todo No reparte procesos dentro del contenedor: uno por contenedor Cuando ya empaquetas con Docker (lección 11-04)
Orquestador (Kubernetes, ECS, Nomad) Supervisa contenedores en una flota de máquinas Escalado, sondas, despliegue progresivo, autorreparación Complejidad y coste operativo altos Muchos servicios, varios equipos, escala real
PaaS (Heroku, Render, Fly) El proveedor supervisa por ti No gestionas nada Menos control, coste por uso Equipos pequeños que quieren ir rápido (lección 11-05)

Para Escena Viva sobre un VPS, PM2 es la elección razonable: da cluster y recarga sin cortes sin escribir una línea de systemd. En 11-04 y 11-05 veremos que, en cuanto contenerizas, el supervisor pasa a ser otro — y ese solapamiento es normal, no un fallo de diseño.

  1. PM2: instalación y comandos de referencia

# Instalacion global en el servidor (no es dependencia del proyecto).
npm install -g pm2

pm2 --version

PM2 se instala global, no como dependencia. Ponerlo en dependencies es un error frecuente: PM2 arranca tu aplicación, no forma parte de ella, y meterlo dentro complica la imagen Docker de la lección siguiente. Al primer comando, PM2 arranca un demonio de fondo (God daemon) que sobrevive a tu sesión y mantiene la lista de aplicaciones. Los comandos del CLI hablan con ese demonio.

Comando Qué hace
pm2 start ecosystem.config.js --env production Arranca lo definido en el fichero de ecosistema
pm2 list Tabla con estado, CPU, memoria y reinicios de cada aplicación
pm2 logs escena-viva-api --lines 100 Sigue los registros en tiempo real
pm2 monit Panel interactivo en terminal: CPU, memoria y registros por proceso
pm2 describe escena-viva-api Ficha completa: rutas, variables, tiempo activo, reinicios, PID
pm2 restart <app> Para y arranca (hay corte de servicio)
pm2 reload <app> Recarga secuencial sin cortes (modo cluster)
pm2 stop <app> Para sin eliminar de la lista
pm2 delete <app> Elimina de la lista de PM2
pm2 save Guarda la lista actual para restaurarla al arrancar la máquina
pm2 startup Genera el servicio de systemd que arranca PM2 al encender
pm2 flush Vacía los ficheros de registro
pm2 scale escena-viva-api 4 Cambia el número de instancias en caliente

Una nota sobre estilo: aunque pm2 start src/servidor.js -i 4 funciona, en un servidor real nunca se arranca así. La configuración quedaría solo en el historial de bash de quien la escribió. Todo va al fichero de ecosistema.

  1. El fichero de ecosistema de Escena Viva

Escena Viva tiene dos procesos que desplegar, y esto viene directamente del módulo 10: la API HTTP y el consumidor de la cola de BullMQ (src/procesos/consumidor-entradas.js), que emite los PDF de las entradas y los correos. Son dos aplicaciones distintas, con perfiles de recursos distintos, y deben escalarse por separado: durante el estreno del Festival de Jazz puedes necesitar más consumidores sin necesitar más API.

// ecosystem.config.js
'use strict';

module.exports = {
  apps: [
    {
      name: 'escena-viva-api',
      script: './src/servidor.js',
      // 0 = tantas instancias como nucleos disponibles.
      instances: 0,
      exec_mode: 'cluster',

      // --- Arranque y apagado ordenado ---
      wait_ready: true,        // Espera a process.send('ready'), no al 'listen'.
      listen_timeout: 10000,   // Si no llega 'ready' en 10 s, se considera fallido.
      kill_timeout: 15000,     // Margen tras SIGINT antes de SIGKILL: drenaje del M6.

      // --- Politica de reinicio ---
      autorestart: true,
      max_restarts: 10,
      min_uptime: '30s',
      exp_backoff_restart_delay: 200,
      max_memory_restart: '700M',

      // --- Registros: pino ya escribe JSON a stdout (11-02) ---
      out_file: '/var/log/escena-viva/api.log',
      error_file: '/var/log/escena-viva/api.error.log',
      merge_logs: true,
      time: false,             // NO anteponer marca de tiempo: rompe el JSON.

      // --- Configuracion por entorno (solo valores NO secretos) ---
      env: {
        NODE_ENV: 'development',
        PUERTO: 3000,
        NIVEL_REGISTRO: 'debug',
      },
      env_production: {
        NODE_ENV: 'production',
        PUERTO: 3000,
        NIVEL_REGISTRO: 'info',
        CONFIAR_EN_PROXY: 'true',
      },
    },

    {
      name: 'escena-viva-consumidor',
      script: './src/procesos/consumidor-entradas.js',
      // El consumidor NO escucha en un puerto: no hay puerto que compartir.
      instances: 2,
      exec_mode: 'fork',

      wait_ready: false,
      kill_timeout: 30000,     // Margen mayor: puede estar generando un PDF.

      autorestart: true,
      max_restarts: 10,
      min_uptime: '60s',
      exp_backoff_restart_delay: 500,
      max_memory_restart: '900M',   // Los worker threads de PDF gastan mas.

      out_file: '/var/log/escena-viva/consumidor.log',
      error_file: '/var/log/escena-viva/consumidor.error.log',
      merge_logs: true,
      time: false,

      env: {
        NODE_ENV: 'development',
        CONCURRENCIA_COLA: 2,
      },
      env_production: {
        NODE_ENV: 'production',
        CONCURRENCIA_COLA: 5,
      },
    },
  ],
};

Las decisiones que hay detrás:

  • exec_mode: 'cluster' solo para la API. El modo cluster existe para repartir un puerto entre varios procesos. El consumidor no escucha en ningún puerto: BullMQ reparte el trabajo a través de Redis. Usar cluster ahí no aportaría nada, así que va en fork.
  • instances: 0 en la API, 2 fijas en el consumidor. La API escala con los núcleos; el consumidor escala con la carga de la cola y con la memoria que consumen los worker threads de generación de PDF (módulo 10). Son ejes distintos.
  • kill_timeout distinto en cada una. La API drena peticiones HTTP: 15 segundos sobran. El consumidor puede estar a mitad de un PDF: 30 segundos evitan matarlo con trabajo a medias.
  • time: false. Es sutil y muy importante: si PM2 antepone su propia marca de tiempo a cada línea, el JSON de pino deja de ser JSON válido y el agregador no puede indexarlo. Con registro estructurado, PM2 debe limitarse a redirigir.
  • env_production no contiene ni un secreto. Volveremos a esto en el apartado 11.

Se arranca con:

pm2 start ecosystem.config.js --env production

Sin --env production, PM2 usa el bloque env, es decir, desarrollo. Es una de las formas más habituales de acabar con NODE_ENV=development en un servidor de producción, con todas las consecuencias que vimos en 11-01.

  1. Modo cluster de PM2 frente a nuestro src/cluster.js

En el módulo 10 escribimos src/cluster.js a mano: un proceso primario que bifurca N trabajadores con node:cluster, los supervisa, los reemplaza si mueren y hace recarga secuencial sin cortes. Funciona. PM2 en exec_mode: 'cluster' hace exactamente lo mismo: usa el módulo cluster de Node por debajo, con el mismo mecanismo de reparto del socket entre procesos hijo. No es magia ni una tecnología distinta.

src/cluster.js propio Modo cluster de PM2
Reparto del puerto node:cluster node:cluster (idéntico)
Reemplazo de trabajadores muertos Lo mantienes tú Incluido
Recarga secuencial La escribiste tú pm2 reload
Cambiar N instancias en caliente No, hay que reiniciar pm2 scale
Reinicio por memoria Habría que implementarlo max_memory_restart
Métricas por trabajador Habría que implementarlo pm2 monit, pm2 describe
Dependencia externa Ninguna PM2 instalado y funcionando
Dentro de un contenedor Vale, y es lo habitual Sobra: el orquestador ya escala

Qué se gana: dejas de mantener y probar código de infraestructura que no es tu negocio, y obtienes gratis escalado en caliente, reinicio por memoria y monitorización. Qué se pierde: una dependencia externa que hay que instalar y actualizar en el servidor, y menos control fino sobre el ciclo de vida de cada trabajador.

Y por qué haberlo escrito a mano no fue tiempo perdido: porque ahora entiendes que instances: 4 no crea cuatro hilos sino cuatro procesos con memoria separada —de ahí que las sesiones y el límite de peticiones tuvieran que ir a Redis en el módulo 10—, que el pool de Sequelize se multiplica por cuatro (y que por eso src/db/sequelize.js lo dimensiona según el número de trabajadores), y que el estado en memoria de un proceso no existe para los otros tres. Quien nunca ha escrito un cluster a mano configura instances: 16 y luego se pregunta por qué se agotan las conexiones de PostgreSQL. En producción con PM2, src/cluster.js deja de usarse: script apunta a src/servidor.js y PM2 pone el cluster. El fichero se queda en el repositorio como alternativa para entornos donde PM2 no está (dentro de un contenedor, por ejemplo, aunque allí tampoco suele hacer falta).

  1. reload frente a restart: recarga sin cortes

Esta es la razón principal para usar PM2 en un servidor propio. pm2 restart: mata todos los procesos y arranca otros nuevos. Entre una cosa y otra hay un hueco de uno a tres segundos en el que nadie escucha en el puerto. Todo lo que llegue en ese hueco es un 502.

pm2 reload (solo en modo cluster): reemplaza los trabajadores de uno en uno. Arranca el nuevo, espera a que esté listo, retira el viejo del reparto de conexiones, le da tiempo a terminar lo que tiene entre manos y lo mata. Siempre queda alguien escuchando. Cero errores para el usuario.

sequenceDiagram
    participant PM2
    participant T1 as Trabajador 1 (viejo)
    participant T1n as Trabajador 1 (nuevo)
    participant T2 as Trabajador 2 (viejo)
    PM2->>T1n: fork con el codigo nuevo
    T1n-->>PM2: process.send('ready')
    PM2->>T1: SIGINT
    T1->>T1: deja de aceptar, drena peticiones
    T1-->>PM2: exit(0)
    PM2->>PM2: repite con el trabajador 2
    Note over T2: sigue atendiendo todo el tiempo

Para que esto funcione de verdad hacen falta tres piezas coordinadas, y las dos primeras ya existen desde el módulo 6. 1. Apagado ordenado en src/servidor.js. PM2 envía SIGINT (no SIGTERM) a los procesos en cluster. Nuestro manejador ya escucha ambas señales: deja de aceptar conexiones, marca estado.aceptandoTrafico = false para que /salud/listo devuelva 503, cierra las conexiones ociosas con closeIdleConnections, espera a las activas y sale con un temporizador de gracia.

2. La señal ready. Con wait_ready: true, PM2 no considera vivo al trabajador hasta que este lo dice explícitamente:

// src/servidor.js (fragmento del arranque)
async function arrancarServidor() {
  await conectarBaseDatos();
  await obtenerClienteRedis().ping();

  const app = crearAplicacion(dependencias);
  const servidor = http.createServer(app);

  await new Promise((resolver) => servidor.listen(configuracion.puerto, resolver));

  logger.info(
    { puerto: configuracion.puerto, entorno: configuracion.entorno, pid: process.pid },
    'servidor escuchando'
  );

  // Avisamos a PM2 SOLO cuando todo esta realmente listo:
  // base de datos conectada, Redis respondiendo y puerto escuchando.
  if (process.send) {
    process.send('ready');
  }

  return servidor;
}

La diferencia es enorme. Sin wait_ready, PM2 da por bueno el trabajador en cuanto el proceso existe, y retira el viejo cuando el nuevo todavía está conectando a PostgreSQL: los primeros usuarios ven errores. Con wait_ready, la ventana de reemplazo solo se abre cuando el nuevo puede atender de verdad. Nota el if (process.send): solo existe cuando el proceso ha sido bifurcado por otro, así que arrancar directamente con node src/servidor.js sigue funcionando. 3. kill_timeout suficiente. Es el tiempo que PM2 espera desde SIGINT hasta SIGKILL. Debe ser mayor que el drenaje más largo que espera tu aplicación. Si tienes peticiones de hasta 10 segundos, un kill_timeout de 5000 mata respuestas a medias y el trabajo del módulo 6 no sirve de nada. Nuestros 15 segundos en la API cubren el peor caso con margen.

Y listen_timeout es el reverso: si ready no llega en 10 segundos, PM2 asume que el arranque ha fallado. Protege contra el escenario en que una versión nueva no consigue conectar a la base de datos y se queda colgada para siempre, dejando el despliegue a medias.

  1. Reinicio automático y protección contra bucles

Con autorestart: true, si un proceso termina, PM2 arranca otro. Bien. Pero imagina que la versión nueva tiene un error en el arranque: falta una variable de entorno y src/config/index.js hace process.exit(1) (11-01). PM2 lo reinicia. Falla otra vez. Lo reinicia. En bucle, cientos de veces por minuto, quemando CPU y llenando el disco de registros. Tres parámetros lo evitan:

Parámetro Qué hace Valor en Escena Viva
min_uptime Tiempo mínimo vivo para considerar el arranque «bueno» 30s (API), 60s (consumidor)
max_restarts Reinicios consecutivos por debajo de min_uptime antes de rendirse 10
exp_backoff_restart_delay Retardo creciente entre reintentos (200, 400, 800 ms...) 200 (API), 500 (consumidor)

Con esta combinación, un proceso que muere al segundo de arrancar se reintenta diez veces con espera creciente y luego pasa a estado errored, donde se queda. PM2 deja de insistir y espera intervención humana, que es lo correcto: la aplicación está rota y reintentar no la va a arreglar. Y un aviso que vale por toda la lección: reiniciar sin arreglar la causa es tapar un problema. Si pm2 list muestra 3.400 reinicios en la columna ↺, no tienes un sistema resiliente: tienes un bug que se manifiesta cada pocos minutos y un supervisor tapándolo. Revisa esa columna cada vez que la mires. Un valor que crece es una investigación pendiente, no una tranquilidad.

  1. Reinicio por memoria como red de seguridad

max_memory_restart: '700M' reinicia el proceso cuando supera ese consumo. Es útil, pero hay que entender bien qué es y qué no es.

Qué es: una red de seguridad. Ante una fuga de memoria lenta —de esas que el módulo 9 nos enseñó a diagnosticar con instantáneas del heap—, evita que el proceso crezca hasta que el núcleo lo mate por falta de memoria (OOM killer) o hasta que el recolector de basura se pase el día trabajando y el p99 se dispare. Qué no es: una solución. Si el proceso se reinicia por memoria cada dos horas, tienes una fuga y hay que encontrarla. El reinicio compra tiempo para investigar, no cierra el asunto.

Para elegir el valor: mira el consumo estable en producción con pm2 monit y pon un 60-80 % por encima. Demasiado bajo y reiniciarás sin motivo bajo carga alta; demasiado alto y la red de seguridad nunca actúa. Ten en cuenta además que el límite es por proceso: con 4 instancias a 700 MB necesitas 2,8 GB solo para la API, más el consumidor. Ese cálculo se vuelve crítico en el contenedor de la lección siguiente.

  1. Arranque al iniciar la máquina

Un supervisor que no sobrevive a un reinicio del servidor resuelve la mitad del problema. Dos comandos:

# 1. Genera e instala el servicio de systemd que arranca PM2 al encender.
#    Imprime un comando con sudo que debes ejecutar tal cual.
pm2 startup

# 2. Guarda la lista ACTUAL de aplicaciones para restaurarla en el arranque.
pm2 save

El orden importa y el olvido es clásico: pm2 startup hace que PM2 arranque, pero sin lista de aplicaciones. Es pm2 save el que guarda el volcado (~/.pm2/dump.pm2) que PM2 restaura. Si cambias el fichero de ecosistema y no vuelves a hacer pm2 save, tras el próximo reinicio del servidor volverá la configuración antigua. Regla: después de cualquier cambio en las aplicaciones, pm2 save. Nota curiosa y sana: por debajo, pm2 startup crea una unidad de systemd. Es decir, systemd supervisa a PM2 y PM2 supervisa tus aplicaciones. Si eso te parece una capa de más, es una intuición correcta, y es parte del argumento para pasar a contenedores en la lección siguiente.

  1. Registros de PM2 y su rotación

PM2 captura stdout y stderr de cada proceso y los escribe a los ficheros indicados en out_file y error_file. Como pino ya escribe JSON estructurado (11-02), PM2 solo debe redirigir: nada de añadir marcas de tiempo ni prefijos.

out_file: '/var/log/escena-viva/api.log',
error_file: '/var/log/escena-viva/api.error.log',
merge_logs: true,   // Todas las instancias al mismo fichero: ya llevan pid dentro.
time: false,        // Sin prefijo: preserva el JSON valido linea a linea.

merge_logs: true junta las cuatro instancias en un fichero. Podría parecer confuso, pero cada línea JSON de pino ya lleva pid y idPeticion, así que separar por fichero no aporta nada y complica la consulta.

Sin rotación, esos ficheros crecen hasta llenar el disco, y un disco lleno tumba la aplicación y la base de datos. El módulo de PM2 que lo resuelve:

pm2 install pm2-logrotate

pm2 set pm2-logrotate:max_size 50M
pm2 set pm2-logrotate:retain 14          # 14 ficheros rotados
pm2 set pm2-logrotate:compress true      # gzip de los antiguos
pm2 set pm2-logrotate:rotateInterval '0 0 * * *'   # a medianoche

Aun así, recuerda el factor XI de los doce factores (11-02): lo ideal es que los registros no acaben en ficheros del servidor sino en un agregador. Un patrón habitual con PM2 es dejar que escriba a fichero y poner un agente ligero (Vector, Fluent Bit, Promtail) que lea esos ficheros y los envíe. Los ficheros pasan a ser un búfer temporal, no el destino final.

  1. Variables de entorno y secretos en PM2

Los bloques env y env_production del fichero de ecosistema son cómodos... y son un riesgo, porque ese fichero está versionado. Poner ahí JWT_SECRETO es exactamente el fallo de la prueba del repositorio público de 11-01. En el fichero de ecosistema van solo valores no secretos: NODE_ENV, PUERTO, NIVEL_REGISTRO, CONFIAR_EN_PROXY. Los secretos llegan por otra vía. Tres opciones, de menos a más recomendable:

a) Fichero .env fuera del control de versiones, cargado por dotenv. Es lo que ya hace src/config/index.js. En el servidor existe /opt/escena-viva/.env con permisos 600 y propietario el usuario de la aplicación. Simple y suficiente para un VPS. b) EnvironmentFile de systemd en la unidad que genera pm2 startup. Las variables entran al demonio de PM2 y las heredan todas las aplicaciones. Menos flexible si tienes varias aplicaciones con secretos distintos.

c) Exportarlas antes de arrancar PM2, obteniéndolas de un gestor de secretos:

# En el script de despliegue, no en el repositorio.
export JWT_SECRETO=$(vault kv get -field=jwt secret/escena-viva)
export SESION_SECRETO=$(vault kv get -field=sesion secret/escena-viva)
pm2 reload ecosystem.config.js --env production --update-env

--update-env es imprescindible: PM2 recuerda el entorno con el que arrancó una aplicación y lo reutiliza en los reinicios. Sin esa bandera, cambias una variable, recargas, y sigue corriendo con el valor viejo. Es una de las confusiones más frecuentes con PM2, y ha costado más de una hora de depuración a mucha gente. Si has rotado un secreto siguiendo el procedimiento de 11-01 y parece que no ha tenido efecto, empieza por aquí.

  1. Despliegue y monitorización

PM2 incluye un sistema de despliegue por SSH, pm2 deploy, que se configura en el mismo fichero:

// ecosystem.config.js (bloque adicional)
deploy: {
  production: {
    user: 'escena',
    host: ['api1.escenaviva.test'],
    ref: 'origin/master',
    repo: '[email protected]:escena-viva/plataforma.git',
    path: '/opt/escena-viva',
    'post-deploy':
      'npm ci --omit=dev && npm run migrar && pm2 reload ecosystem.config.js --env production --update-env',
  },
},

Con pm2 deploy production se conecta por SSH, hace git fetch y checkout, ejecuta post-deploy y recarga sin cortes. Fíjate en el orden: npm ci --omit=dev (el pago del módulo 5), migraciones antes de recargar, y reload, nunca restart. Es una solución digna para un VPS y un equipo pequeño. Tiene límites claros: despliega desde el repositorio en cada máquina, así que cada servidor construye por su cuenta y puede acabar con dependencias ligeramente distintas. La alternativa —construir un artefacto y promoverlo— es lo que veremos con Docker en 11-04 y con la tubería de CI en 11-06. Mientras tanto, pm2 deploy funciona.

Para la monitorización diaria:

  • pm2 monit: panel interactivo con CPU y memoria por proceso y registros en vivo. Útil durante un despliegue o un pico de carga como el estreno del Festival de Jazz.
  • pm2 describe escena-viva-api: la ficha completa de una aplicación. Lo primero que hay que mirar al investigar: número de reinicios, tiempo activo, entorno efectivo y rutas de registro.
  • pm2 list: la vista rápida. Vigila la columna de reinicios (↺) y la memoria.

Estas herramientas son para inspección puntual. La monitorización continua es la de 11-02: métricas en Prometheus, paneles en Grafana y alertas sobre síntomas. pm2 monit no te despierta de noche.

  1. Lo que PM2 no resuelve

Y aquí llega el cierre honesto. PM2 resuelve el proceso, no el entorno. PM2 garantiza que tu aplicación esté viva, se reinicie si cae, se recargue sin cortes y arranque con la máquina. Todo eso suponiendo que la máquina sea correcta. Y esa suposición es enorme:

  • La versión de Node debe ser la que exige engines (>=24.5.0 <25). Si el servidor tiene la 20, require('node:...') puede funcionar y otras cosas no, y lo descubrirás en producción.
  • Las librerías del sistema deben estar presentes: bcrypt compila contra la libc del sistema, las fuentes para generar PDF deben existir, libpq para PostgreSQL.
  • Las variables de entorno deben estar puestas, con permisos correctos y actualizadas.
  • El usuario, los directorios, los permisos y las rutas de registro deben existir.

Todo eso se configura a mano en cada servidor. Y en cuanto hay dos servidores, empiezan a divergir: uno tiene una versión de OpenSSL distinta, en otro alguien instaló algo para depurar y no lo quitó. El resultado es el clásico «en el servidor A funciona y en el B no», que es la versión adulta del «en mi máquina funciona». La respuesta es empaquetar el entorno junto con la aplicación, de modo que lo que despliegas no sea código que se ejecuta sobre una máquina desconocida, sino un artefacto que lleva su propio sistema de ficheros dentro. Eso es un contenedor.

Errores Comunes y Consejos

  • Arrancar sin --env production. PM2 usa el bloque env (desarrollo). Comprueba siempre con pm2 describe qué NODE_ENV está efectivamente en uso.
  • Olvidar --update-env tras cambiar una variable. PM2 reutiliza el entorno guardado y tu cambio no surte efecto.
  • Usar restart donde tocaba reload. Un corte de dos segundos en cada despliegue, evitable con una letra distinta.
  • kill_timeout demasiado corto. PM2 mata a mitad de respuesta y anula el apagado ordenado del módulo 6.
  • Dejar time: true con registro JSON. El prefijo rompe el JSON y el agregador no puede indexar nada.
  • Secretos en env_production. Están versionados. Es el mismo error de 11-01 con otra ropa.
  • Olvidar pm2 save tras cambiar la configuración. Al reiniciar el servidor vuelve la configuración anterior, a veces meses después, y nadie entiende qué pasa.
  • Ignorar la columna de reinicios. Un contador que crece es un bug tapado por el supervisor.
  • Consejo: ejecuta PM2 con un usuario sin privilegios, nunca como root. Y si la aplicación necesitase el puerto 80 (módulo 4), pon delante un proxy inverso en lugar de dar privilegios a Node.
  • Consejo: fija la versión de PM2 en el servidor y actualízala a propósito. Es infraestructura, y una actualización sorpresa durante una venta es una mala noche.

Ejercicios

Ejercicio 1 — Verificar la recarga sin cortes

Con la API arrancada en modo cluster con cuatro instancias, lanza autocannon (módulo 10) contra /api/eventos durante 30 segundos y ejecuta pm2 reload escena-viva-api a mitad. Comprueba que no aparece ningún error ni ningún 502. Luego repite con pm2 restart y compara.

Ejercicio 2 — Simular un bucle de reinicios

Provoca deliberadamente un fallo de arranque (quita JWT_SECRETO del entorno) y observa el comportamiento de PM2 con pm2 logs y pm2 list. Comprueba que tras max_restarts intentos la aplicación queda en estado errored. Documenta cuánto tarda en rendirse con exp_backoff_restart_delay: 200.

Ejercicio 3 — Escalar el consumidor

La cola de entradas acumula 2.000 tareas pendientes durante la venta anticipada del Festival de Jazz. Escala el consumidor a seis instancias sin parar la API, verifica el efecto sobre la métrica escenaviva_cola_pendientes de 11-02 y explica qué otro límite podría alcanzarse antes de que ayude añadir más instancias.

Soluciones

Ejercicio 1.

pm2 start ecosystem.config.js --env production
npx autocannon -c 50 -d 30 http://localhost:3000/api/eventos &
sleep 10 && pm2 reload escena-viva-api

Con reload, el informe de autocannon muestra non-2xx: 0 y errors: 0; en la latencia se aprecia como mucho un ligero aumento del p99 mientras un trabajador menos atiende el tráfico. Con restart aparecen decenas de errores de conexión rechazada concentrados en el instante del corte. La diferencia se debe por completo a las tres piezas del apartado 6: apagado ordenado, wait_ready y kill_timeout suficiente. Si al probarlo con reload también ves errores, revisa que process.send('ready') se esté enviando de verdad: sin él, wait_ready: true haría que el arranque expirara.

Ejercicio 2. Con los reintentos exponenciales a partir de 200 ms (200, 400, 800, 1600...), diez intentos suman en torno a 100 segundos antes de que PM2 se rinda. En pm2 logs se ve repetido el mensaje de src/config/index.js:

Configuracion invalida. Escena Viva no puede arrancar:
  - JWT_SECRETO: Required

Y ahí se aprecia el valor de la validación de 11-01: el motivo del fallo aparece en la primera línea de cada intento, en lugar de un críptico error interno de jsonwebtoken tres horas después. En pm2 list el estado final es errored y el contador de reinicios se detiene en 10.

Ejercicio 3.

pm2 scale escena-viva-consumidor 6
pm2 describe escena-viva-consumidor

El escalado es inmediato y no afecta a la API, porque son aplicaciones independientes. escenaviva_cola_pendientes debería bajar con una pendiente aproximadamente tres veces mayor. El límite que se alcanza antes es el de conexiones. Cada consumidor abre conexiones a Redis (BullMQ usa varias por trabajador) y a PostgreSQL para registrar las entradas emitidas. Con 6 consumidores × concurrencia 5, más las 4 instancias de la API con su pool, es fácil superar el max_connections de PostgreSQL o el límite de clientes de Redis. Es la misma aritmética que nos obligó en el módulo 10 a dimensionar el pool de Sequelize según el número de trabajadores, y la que volverá a aparecer en 11-05 con los límites de conexiones de los planes gestionados. Añadir instancias sin recalcular ese producto convierte un problema de rendimiento en una caída.

Conclusión

Escena Viva ya no depende de una sesión SSH abierta. PM2 la supervisa: dos aplicaciones declaradas en un fichero de ecosistema versionado —la API en modo cluster y el consumidor de la cola en modo fork, cada uno con su política de recursos—, recarga sin cortes apoyada en el apagado ordenado del módulo 6 y en la señal ready que ahora emitimos, protección contra bucles de reinicio, reinicio por memoria como red de seguridad ante fugas, arranque automático con la máquina y registros rotados sin romper el JSON de pino. Además hemos visto por qué escribir src/cluster.js a mano en el módulo 10 sigue siendo valioso aunque ahora no lo usemos: entender el reparto por procesos es lo que permite configurar instances sin agotar las conexiones de la base de datos. Pero PM2 supervisa el proceso, no la máquina. Sigue haciendo falta que el servidor tenga la versión exacta de Node, las librerías del sistema para compilar bcrypt, las fuentes para los PDF y la configuración correcta — y que el segundo servidor sea idéntico al primero, cosa que nunca ocurre. En la próxima lección, Empaquetado con Docker, hacemos que el entorno viaje con la aplicación: un Dockerfile multietapa explicado línea a línea, usuario no root, CMD en forma de exec para que SIGTERM llegue de verdad al proceso, HEALTHCHECK apoyado en /salud/listo, y un docker compose con API, consumidor, PostgreSQL, MongoDB y Redis que por fin te dará el entorno de desarrollo completo que llevas pidiendo desde el módulo 7.

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