Skip to content

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 -v
bash
# 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 dotenv
js
// ~/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/tienda

Prueba 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 pm2

Comandos 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/memoria

Archivo 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 list

Modo 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 reiniciar

Prueba 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 14

Paso 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/salud

Despué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 build
js
// 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 save

Y 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.sh

Desde 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 ​

  1. Escribe un ecosystem.config.js para una app bot-whatsapp con index.js, una sola instancia, reinicio a los 200 MB y NODE_ENV=production.
  2. ¿Qué dos comandos garantizan que tus apps vuelvan tras un reinicio del servidor?
  3. 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' },
  }],
};
  1. pm2 startup (y ejecutar el comando que imprime) y pm2 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.js para 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 start bajo PM2.
  • Siguiente: SSL con Let's Encrypt.

Academia Web — aprende, copia y construye.