Despliegue de aplicaciones Node.js con PM2 y clustering en modo producción
Introducción
En entornos de producción, una aplicación Node.js debe ejecutarse con alta disponibilidad, gestión de fallos y escalabilidad horizontal. El runtime de Node.js es monohilo por naturaleza, lo que significa que una instancia única no puede aprovechar completamente los núcleos de un servidor multicore. PM2, combinado con su modo cluster, resuelve este problema permitiendo ejecutar múltiples procesos hijos que comparten el mismo puerto, distribuyendo la carga y garantizando la resiliencia.
Este artículo detalla el proceso completo de despliegue de aplicaciones Node.js con PM2 y clustering en modo producción, abarcando desde la instalación y configuración básica hasta estrategias avanzadas de monitorización, reinicio automático y balanceo de carga con Nginx.
¿Por qué PM2 y clustering?
PM2 es un gestor de procesos para Node.js con funcionalidades que van más allá de un simple forever o nodemon. Ofrece:
- Reinicio automático tras fallos.
- Modo cluster nativo que replica la aplicación en todos los núcleos de la CPU.
- Monitorización integrada (CPU, memoria, logs).
- Gestión de logs con rotación y agregación.
- Recarga en caliente sin tiempo de inactividad (
pm2 reload).
El clustering es especialmente crítico en producción porque:
- Aumenta el rendimiento: al repartir las peticiones entre N procesos, se reduce el tiempo de respuesta y se maximiza el uso de la CPU.
- Mejora la tolerancia a fallos: si un worker muere, PM2 lo reinicia automáticamente sin afectar al resto.
- Permite escalado vertical sin modificar el código de la aplicación.
Instalación y configuración base
1. Instalar Node.js y npm
Asumimos un servidor Linux (Ubuntu/Debian) limpio.
# Actualizar repositorios
sudo apt update && sudo apt upgrade -y
# Instalar Node.js (LTS recomendada)
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt install -y nodejs
# Verificar versión
node -v # v20.x
npm -v # 10.x
2. Instalar PM2 globalmente
sudo npm install -g pm2
PM2 se instala globalmente para que esté disponible como comando del sistema. No debe instalarse como dependencia del proyecto.
3. Preparar la aplicación
Supongamos una aplicación Express básica en /var/www/mi-app.
# Clonar o copiar el proyecto
git clone https://github.com/usuario/mi-app.git /var/www/mi-app
cd /var/www/mi-app
# Instalar dependencias de producción
npm install --production
Asegúrate de que el archivo principal (app.js o server.js) escuche en un puerto definido por variable de entorno:
const port = process.env.PORT || 3000;
app.listen(port, () => {
console.log(`Servidor escuchando en puerto ${port}`);
});
Configuración de PM2 para producción
1. Iniciar en modo cluster
El modo cluster se activa con la opción -i <number> o -i max para usar todos los núcleos disponibles.
# Iniciar con 4 workers
pm2 start app.js -i 4 --name "mi-app"
# O usar todos los núcleos
pm2 start app.js -i max --name "mi-app"
Explicación: -i max detecta automáticamente el número de CPUs lógicas en el sistema (por ejemplo, 8 en un servidor con 4 núcleos físicos e Hyper-Threading). Cada worker es un proceso independiente que compite por las mismas conexiones entrantes.
2. Verificar el estado
pm2 list
Salida esperada:
┌─────┬───────────┬──────────┬──────┬───────────┬────────┬──────────┐
│ id │ name │ mode │ ↺ │ status │ cpu │ memory │
├─────┼───────────┼──────────┼──────┼───────────┼────────┼──────────┤
│ 0 │ mi-app │ cluster │ 0 │ online │ 12% │ 45.2 MB │
│ 1 │ mi-app │ cluster │ 0 │ online │ 10% │ 44.8 MB │
│ 2 │ mi-app │ cluster │ 0 │ online │ 14% │ 46.1 MB │
│ 3 │ mi-app │ cluster │ 0 │ online │ 11% │ 45.5 MB │
└─────┴───────────┴──────────┴──────┴───────────┴────────┴──────────┘
3. Configuración persistente con ecosystem.config.js
Para evitar escribir opciones cada vez, se crea un archivo de configuración declarativo.
pm2 init simple
Esto genera un ecosystem.config.js básico. Lo modificamos:
module.exports = {
apps: [{
name: 'mi-app',
script: './app.js',
instances: 'max', // 'max' o número fijo
exec_mode: 'cluster', // obligatorio para clustering
env: {
NODE_ENV: 'production',
PORT: 3000
},
env_file: '.env', // opcional
max_memory_restart: '500M', // reinicia si supera 500MB
log_date_format: 'YYYY-MM-DD HH:mm:ss',
error_file: './logs/err.log',
out_file: './logs/out.log',
merge_logs: true,
autorestart: true,
watch: false // desactivado en producción
}]
};
Explicación de parámetros clave:
exec_mode: 'cluster': fuerza el modo cluster.instances: 'max': usa todos los núcleos. Puede ser un entero.max_memory_restart: evita fugas de memoria matando y reiniciando el worker.watch: false: en producción no se debe recargar automáticamente por cambios en archivos.merge_logs: evita logs duplicados cuando se usa cluster.
Iniciar con el archivo:
pm2 start ecosystem.config.js
Gestión del ciclo de vida en producción
Comandos esenciales
| Comando | Descripción |
|---|---|
pm2 start <script> | Inicia la aplicación |
pm2 stop <nombre> | Detiene la aplicación (no elimina de la lista) |
pm2 restart <nombre> | Detiene e inicia de nuevo |
pm2 reload <nombre> | Recarga en caliente (sin downtime) |
pm2 delete <nombre> | Elimina de la lista de procesos |
pm2 save | Guarda la lista actual para pm2 resurrect |
pm2 startup | Genera script de inicio automático al arrancar el sistema |
Recarga en caliente (zero-downtime reload)
El reload envía la señal SIGINT a los workers de uno en uno, esperando que el siguiente worker esté listo antes de matar el anterior. Es la forma correcta de actualizar la aplicación sin perder peticiones.
# Después de modificar el código
pm2 reload mi-app
Cuándo usar restart vs reload:
restart: mata todos los workers simultáneamente y los reinicia. Causa downtime breve.reload: reinicia uno a uno. No hay downtime, pero requiere que la aplicación maneje correctamente la señal SIGINT y cierre conexiones de forma ordenada.
Startup automático
Para que PM2 arranque al iniciar el sistema:
pm2 startup
Este comando genera un script systemd y muestra instrucciones para ejecutarlo. Luego:
pm2 save
Guarda la lista actual de procesos para que pm2 resurrect los restaure tras un reinicio del sistema.
Monitorización y logs
1. Dashboard en tiempo real
pm2 monit
Muestra una interfaz con:
- CPU y memoria por proceso.
- Logs en vivo.
- Peticiones por segundo (si se habilita).
2. Logs centralizados
# Ver logs de todos los procesos
pm2 logs
# Logs de un proceso específico
pm2 logs mi-app
# Últimas 100 líneas
pm2 logs mi-app --lines 100
3. Rotación de logs
Instalar el módulo de rotación:
pm2 install pm2-logrotate
Configurar (en ecosystem.config.js o mediante comandos):
pm2 set pm2-logrotate:max_size 10M
pm2 set pm2-logrotate:retain 7
pm2 set pm2-logrotate:compress true
Esto evita que los logs llenen el disco.
Balanceo de carga con Nginx (proxy inverso)
Aunque PM2 en modo cluster distribuye las peticiones entre workers, es recomendable poner Nginx como proxy inverso por varias razones:
- Terminación SSL.
- Servir archivos estáticos.
- Rate limiting.
- Redirección a puerto 80/443.
Configuración de Nginx
upstream mi_app_upstream {
least_conn; # balanceo por menor conexiones activas
server 127.0.0.1:3000;
# Si usas múltiples puertos (no recomendado con cluster de PM2)
# server 127.0.0.1:3001;
}
server {
listen 80;
server_name miapp.com;
location / {
proxy_pass http://mi_app_upstream;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
}
# Archivos estáticos (si aplica)
location /static/ {
alias /var/www/mi-app/public/;
expires 30d;
add_header Cache-Control "public, immutable";
}
}
Explicación:
least_conn: envía la petición al worker con menos conexiones activas.proxy_http_version 1.1y los headersUpgrade/Connectionson necesarios para WebSockets.- Los headers
X-Forwarded-*permiten que la aplicación Node.js conozca la IP real del cliente.
Estrategias de escalado y alta disponibilidad
1. Escalado manual
# Aumentar workers a 8
pm2 scale mi-app 8
# Reducir a 2
pm2 scale mi-app 2
2. Escalado basado en carga (auto-scaling)
PM2 no tiene auto-scaling nativo, pero combinado con herramientas como pm2-auto-scale (módulo de pago) o scripts personalizados que monitoricen la CPU y ejecuten pm2 scale, se puede lograr.
Ejemplo básico con cron:
# Cada minuto, si la CPU media supera el 70%, escala a +2
*/1 * * * * /usr/bin/pm2 scale mi-app +2 --cpu 70
3. Health checks
PM2 puede ejecutar un comando de health check para decidir si un worker está sano:
// En ecosystem.config.js
apps: [{
// ...
health_check: {
url: 'http://localhost:3000/health',
interval: 30000, // cada 30 segundos
timeout: 5000,
fallback: 3 // tras 3 fallos, reinicia
}
}]
Buenas prácticas de seguridad
- No ejecutar PM2 como root: crea un usuario dedicado (
nodeapp) y ejecuta PM2 con ese usuario. - Usar variables de entorno: nunca hardcodees credenciales. Usa
.envo un gestor de secretos. - Limitar puertos: no expongas el puerto de Node.js directamente. Siempre detrás de Nginx.
- Actualizar dependencias:
npm audit fixperiódicamente. - Firewall: permitir solo puertos 80/443 desde internet, y el puerto de la app solo desde localhost.
Solución de problemas comunes
Error: "EADDRINUSE" al iniciar cluster
Si cada worker intenta ocupar el mismo puerto, PM2 en modo cluster maneja esto internamente mediante un socket compartido. Sin embargo, si usas exec_mode: 'fork' en lugar de cluster, cada worker intentará abrir el puerto por separado.
Solución: verificar que exec_mode sea cluster.
Workers que se reinician constantemente
pm2 logs mi-app --err
Causas típicas:
- Excepción no capturada.
- Fuga de memoria (aumentar
max_memory_restart). - Dependencia que falla al cargar.
Alto uso de memoria
- Revisar si hay fugas con
pm2 monit. - Ajustar
max_memory_restarta un valor razonable (por ejemplo, 200MB por worker). - Considerar el uso de
--node-args="--max-old-space-size=256"para limitar el heap de V8.
Conclusión
PM2 con clustering es la solución estándar de facto para desplegar aplicaciones Node.js en producción. La combinación de balanceo de carga entre procesos, reinicio automático y monitorización integrada proporciona una base sólida para servicios web de alto rendimiento. Cuando se complementa con un proxy inverso como Nginx, se obtiene una arquitectura robusta, escalable y segura.
Implementar correctamente estas configuraciones desde el inicio evita problemas de disponibilidad y rendimiento en el futuro. Recuerda siempre probar el mecanismo de recarga en caliente y los health checks antes de poner el sistema en producción.
Nota final: Aunque PM2 es excelente para entornos basados en máquinas virtuales o servidores dedicados, para arquitecturas de contenedores (Kubernetes, Docker Swarm) se recomienda usar el orquestador nativo para el escalado y gestión de procesos, dejando PM2 solo para el manejo interno dentro del contenedor si es necesario.
