libro / blog / despliegue-nodejs-produccion

// despliegue

Cómo desplegar una aplicación Node.js en producción

Gustavo Arias10 min de lectura

TL;DR En producción tu app necesita configuración por variables de entorno, un gestor de procesos que la reinicie si cae (PM2 o el propio Docker), un proxy inverso con HTTPS delante (Nginx) y un apagado ordenado. Instala solo las dependencias de producción con npm ci --omit=dev y nunca ejecutes como root.

Tu API funciona en localhost:3000. Llevarla a un servidor real implica resolver preguntas que en desarrollo no existían: ¿quién la vuelve a levantar si se cae?, ¿cómo se sirve por HTTPS?, ¿dónde van las contraseñas?, ¿qué pasa con las peticiones en curso al desplegar una versión nueva? En el libro Node.js desde cero el último capítulo está dedicado al despliegue en producción en Heroku, un VPS o Docker; acá tenés las dos opciones más habituales y un checklist final.

Preparar la aplicación

app.get('/health', (req, res) => {
  res.json({ estado: 'ok', uptime: Math.round(process.uptime()) });
});

Opción 1: VPS con PM2 y Nginx

En un VPS (un servidor Linux propio en la nube), PM2 mantiene la aplicación en marcha, la reinicia si falla, la arranca con el sistema y puede ejecutar una instancia por núcleo de CPU. Nginx se coloca delante: gestiona HTTPS, comprime y reenvía las peticiones a Node.js.

$ git clone https://github.com/tu-usuario/api-blog.git && cd api-blog
$ npm ci --omit=dev                      # instala exactamente el lockfile, sin devDependencies
$ npm install -g pm2
$ pm2 start ecosystem.config.cjs
$ pm2 save && pm2 startup                # arrancar automáticamente al reiniciar el servidor
module.exports = {
  apps: [{
    name: 'api-blog',
    script: 'src/server.js',
    instances: 'max',        // una instancia por núcleo de CPU
    exec_mode: 'cluster',
    max_memory_restart: '300M',
    env: {
      NODE_ENV: 'production',
      PORT: 3000,
    },
  }],
};
server {
    listen 80;
    server_name api.tudominio.com;

    location / {
        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;

        # necesarias para WebSocket / Socket.IO
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
    }
}

Después, sudo certbot --nginx -d api.tudominio.com obtiene un certificado gratuito de Let's Encrypt y configura HTTPS. Como Node.js queda detrás de un proxy, añadí app.set('trust proxy', 1) en Express para que req.ip y req.protocol sean los reales.

Opción 2: Docker

Docker empaqueta la aplicación con su versión exacta de Node.js y sus dependencias. La misma imagen funciona en tu máquina, en el servidor y en cualquier plataforma de contenedores.

FROM node:22-alpine

ENV NODE_ENV=production
WORKDIR /app

# Primero solo las dependencias: Docker reutiliza esta capa si no cambian
COPY package*.json ./
RUN npm ci --omit=dev

COPY src ./src

# No ejecutar como root
USER node
EXPOSE 3000
CMD ["node", "src/server.js"]
$ docker build -t api-blog .
$ docker run -d --name api-blog -p 3000:3000 --env-file .env --restart unless-stopped api-blog

Añadí un .dockerignore con node_modules, .env y .git para que no terminen dentro de la imagen. Con --restart unless-stopped, Docker cumple el papel de PM2: si el proceso se cae, lo vuelve a levantar.

Fijate en que el CMD ejecuta node directamente y no npm start: así las señales de apagado del contenedor llegan a tu proceso.

Apagado ordenado

Al desplegar una versión nueva, el proceso viejo recibe la señal SIGTERM. Si se corta en seco, las peticiones en curso fallan. Mejor dejar de aceptar conexiones nuevas, terminar las actuales y cerrar los recursos:

import app from './app.js';

const server = app.listen(process.env.PORT ?? 3000, () => {
  console.log(`API escuchando en el puerto ${server.address().port}`);
});

function apagar(senal) {
  console.log(`${senal} recibido: cerrando...`);
  server.close(async () => {
    // aquí: cerrar el pool de MySQL, el cliente de MongoDB, etc.
    process.exit(0);
  });
  // si en 10 s no terminó, salir igualmente
  setTimeout(() => process.exit(1), 10_000).unref();
}

process.on('SIGTERM', () => apagar('SIGTERM'));
process.on('SIGINT', () => apagar('SIGINT'));

Checklist de producción

Del código a producción

El libro Node.js desde cero termina con el despliegue en producción en Heroku, un VPS o Docker, además de pruebas y seguridad.

Comprar en Amazon