Tema
Desplegar Node con PM2 y Nginx
Ejecutar node server.js en el servidor funciona… hasta que cierras la terminal, la app truena o el servidor se reinicia. En producción necesitas un gestor de procesos que mantenga tu app viva, la reinicie si falla y la arranque con el sistema. Ese es PM2. Delante pondremos Nginx como reverse proxy con HTTPS.
Concepto
txt
Usuario ──HTTPS──► Nginx (443) ──HTTP──► PM2 ──► node app (127.0.0.1:3000)
│ │
│ ├─ reinicia si se cae
│ ├─ arranca al encender el servidor
│ └─ guarda logs
└─ SSL, gzip, estáticos, dominio- La app escucha solo en
127.0.0.1(no es accesible desde fuera). - Nginx recibe el tráfico público y lo reenvía.
- PM2 vigila el proceso de Node.
Paso 1: instalar Node.js LTS
Dos opciones recomendadas:
bash
# Instala nvm (revisa la última versión en github.com/nvm-sh/nvm)
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash
source ~/.bashrc
nvm install --lts
nvm alias default 'lts/*'
node -v && npm -vbash
# Node 22 LTS desde el repositorio de NodeSource
curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
sudo apt install -y nodejs
node -v && npm -v¿Cuál elijo?
nvm si tendrás varias apps con distintas versiones de Node. NodeSource si quieres una sola versión instalada para todo el sistema. No mezcles ambas en el mismo usuario.
Paso 2: una app de ejemplo
bash
mkdir -p ~/apps/api-tienda && cd ~/apps/api-tienda
npm init -y
npm install express dotenvjs
// ~/apps/api-tienda/server.js
require('dotenv').config();
const express = require('express');
const app = express();
const PORT = process.env.PORT || 3000;
const HOST = process.env.HOST || '127.0.0.1'; // solo accesible localmente
// Detrás de Nginx: confiar en X-Forwarded-* para obtener IP y protocolo reales
app.set('trust proxy', 1);
app.use(express.json());
app.get('/api/salud', (req, res) => {
res.json({ ok: true, entorno: process.env.NODE_ENV, ip: req.ip });
});
app.get('/api/productos', (req, res) => {
res.json([
{ id: 1, nombre: 'Café de olla', precio: 45 },
{ id: 2, nombre: 'Pan dulce', precio: 18 },
]);
});
app.listen(PORT, HOST, () => {
console.log(`API escuchando en http://${HOST}:${PORT}`);
});env
# ~/apps/api-tienda/.env (no subir a Git)
NODE_ENV=production
PORT=3000
DATABASE_URL=postgres://usuario:clave@localhost:5432/tiendaPrueba rápida: node server.js y en otra terminal curl http://127.0.0.1:3000/api/salud.
Paso 3: PM2
bash
npm install -g pm2Comandos esenciales
bash
pm2 start server.js --name api-tienda # iniciar
pm2 list # ver procesos
pm2 logs api-tienda # logs en tiempo real
pm2 logs api-tienda --lines 100 # últimas 100 líneas
pm2 restart api-tienda # reiniciar
pm2 reload api-tienda # reinicio sin caída (modo cluster)
pm2 stop api-tienda # detener
pm2 delete api-tienda # quitar de la lista
pm2 monit # monitor de CPU/memoriaArchivo ecosystem (recomendado)
En lugar de recordar parámetros, define todo en un archivo versionado:
js
// ~/apps/api-tienda/ecosystem.config.js
module.exports = {
apps: [
{
name: 'api-tienda',
script: './server.js',
cwd: '/home/deploy/apps/api-tienda',
// 'max' usa todos los núcleos (modo cluster); 1 para modo simple
instances: 'max',
exec_mode: 'cluster',
// Reiniciar si supera esta memoria
max_memory_restart: '300M',
// Variables de entorno
env: {
NODE_ENV: 'production',
PORT: 3000,
},
// Logs
out_file: '/home/deploy/logs/api-tienda.out.log',
error_file: '/home/deploy/logs/api-tienda.err.log',
time: true, // prefija cada línea con fecha/hora
// Reintentos si la app truena al iniciar
max_restarts: 10,
restart_delay: 3000,
},
],
};bash
mkdir -p ~/logs
pm2 start ecosystem.config.js
pm2 listModo cluster
Node usa un solo núcleo por proceso. Con exec_mode: 'cluster' PM2 lanza varias copias y reparte las peticiones. Ojo: si guardas sesiones o datos en memoria, cada copia tendrá los suyos; usa Redis o la base de datos para estado compartido.
Arrancar con el servidor
bash
pm2 startup
# PM2 imprime un comando que empieza con "sudo env PATH=..." → cópialo y ejecútalo
pm2 save # guarda la lista actual de procesos para restaurarla al reiniciarPrueba con sudo reboot y, al volver, pm2 list.
Rotación de logs
bash
pm2 install pm2-logrotate
pm2 set pm2-logrotate:max_size 10M
pm2 set pm2-logrotate:retain 14Paso 4: Nginx como reverse proxy
nginx
# /etc/nginx/sites-available/api.tienda.com
server {
listen 80;
listen [::]:80;
server_name api.tienda.com;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
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_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}bash
sudo ln -s /etc/nginx/sites-available/api.tienda.com /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
curl http://api.tienda.com/api/saludDespués activa HTTPS con Let's Encrypt: sudo certbot --nginx -d api.tienda.com.
Desplegar una app Next.js
bash
cd ~/apps
git clone git@github.com:tu-usuario/web-next.git
cd web-next
npm ci
npm run buildjs
// ecosystem.config.js de Next.js
module.exports = {
apps: [
{
name: 'web-next',
script: 'node_modules/next/dist/bin/next',
args: 'start -p 3001 -H 127.0.0.1',
cwd: '/home/deploy/apps/web-next',
instances: 1,
env: { NODE_ENV: 'production' },
},
],
};bash
pm2 start ecosystem.config.js && pm2 saveY en Nginx, el mismo bloque de proxy apuntando a 127.0.0.1:3001.
Desplegar una SPA de React/Vite
Una SPA no necesita Node en producción: compílala y sirve los archivos con Nginx.
bash
npm ci && npm run build # genera dist/
sudo mkdir -p /var/www/app && sudo chown $USER:www-data /var/www/app
rsync -a --delete dist/ /var/www/app/nginx
server {
listen 80;
server_name app.tienda.com;
root /var/www/app;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
}Script de despliegue
Automatiza las actualizaciones con un script en el servidor:
bash
#!/usr/bin/env bash
# ~/apps/api-tienda/deploy.sh — actualiza y recarga sin caída
set -euo pipefail
cd /home/deploy/apps/api-tienda
echo "==> Descargando cambios"
git fetch origin main
git reset --hard origin/main
echo "==> Instalando dependencias"
npm ci --omit=dev
# Si tienes paso de build (TypeScript, Next.js):
# npm ci && npm run build
echo "==> Recargando PM2"
pm2 reload ecosystem.config.js --update-env
pm2 save
echo "==> Despliegue terminado: $(date)"bash
chmod +x deploy.sh
./deploy.shDesde tu computadora: ssh deploy@tienda.com '~/apps/api-tienda/deploy.sh'. En GitHub Actions lo ejecutaremos automáticamente en cada push.
Error común: 502 Bad Gateway
Nginx no encuentra tu app. Revisa: pm2 list (¿está online?), pm2 logs (¿truena al iniciar?), y que el puerto de proxy_pass coincida con el de la app. Prueba directo con curl http://127.0.0.1:3000.
Error común: la app escucha en 0.0.0.0
Si tu app escucha en 0.0.0.0:3000 y abriste ese puerto, cualquiera puede saltarse Nginx (y el HTTPS). Escucha en 127.0.0.1 y deja cerrado el puerto en ufw.
Ejercicios
- Escribe un
ecosystem.config.jspara una appbot-whatsappconindex.js, una sola instancia, reinicio a los 200 MB yNODE_ENV=production. - ¿Qué dos comandos garantizan que tus apps vuelvan tras un reinicio del servidor?
- Tienes frontend Vite y API Express en el mismo dominio. Escribe el server block (API en
/api/→ puerto 3000).
Ver solución
js
module.exports = {
apps: [{
name: 'bot-whatsapp',
script: './index.js',
instances: 1,
max_memory_restart: '200M',
env: { NODE_ENV: 'production' },
}],
};pm2 startup(y ejecutar el comando que imprime) ypm2 save.
nginx
server {
listen 80;
server_name tienda.com;
root /var/www/tienda/dist;
index index.html;
location /api/ {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location / {
try_files $uri $uri/ /index.html;
}
}Resumen
- Instala Node LTS con nvm o NodeSource.
- PM2 mantiene la app viva, la reinicia y la arranca con el sistema (
startup+save). - Usa
ecosystem.config.jspara versionar la configuración. - La app escucha en
127.0.0.1; Nginx hace de reverse proxy y pone el HTTPS. - Las SPAs se sirven como estáticos; Next.js corre con
next startbajo PM2. - Siguiente: SSL con Let's Encrypt.