Saltar al contenido
VPS 2024-12-29 14 min de lectura

Cómo Montar un Servidor de Node.js Detrás de Nginx

Node.js es conocido por su velocidad y su modelo de entrada/salida no bloqueante, pero exponerlo directamente a internet en producción no es la práctica recomendada. Lo habitual es montar un servidor de Node.js detrás de Nginx actuando como proxy reverso: un servidor intermedio que recibe todas las peticiones de los visitantes y las reenvía a la aplicación real, encargándose de camino del cifrado TLS, la compresión, los archivos estáticos y una primera capa de filtrado. En esta guía montamos ese esquema completo sobre Ubuntu 24.04, desde la instalación hasta los ajustes de producción que la mayoría de tutoriales pasa por alto y que causan errores difíciles de diagnosticar semanas después.

Cómo Montar un Servidor de Node.js Detrás de Nginx
#VPS#Node.js#Nginx#Producción
T
Equipo Terranode
Editorial

Qué necesitas antes de empezar

  • Un VPS con Ubuntu 24.04 o Debian 13 y al menos 1 GB de RAM.
  • Acceso SSH con permisos de sudo.
  • Un dominio apuntando con un registro A a la IP pública del servidor (necesario para el certificado SSL).
  • El puerto 80 accesible desde internet durante la emisión del certificado.

Paso 1: Instalar Node.js 24 LTS

Instala la línea LTS activa, que en 2026 es Node.js 24, desde el repositorio de NodeSource. El paquete nodejs de los repositorios oficiales de Ubuntu suele ir varias versiones por detrás, y las guías que todavía instalan setup_18.x están recomendando una versión sin soporte: Node.js 18 llegó a su fin de vida el 30 de abril de 2025.

VersiónEstado en agosto de 2026Fin de vida¿Usar en producción?
Node.js 18Fin de vida30 abril 2025No, sin parches de seguridad
Node.js 22Maintenance LTS30 abril 2027Solo si ya está en uso
Node.js 24Active LTS30 abril 2028Sí, opción recomendada
Node.js 26CurrentPasa a LTS en octubre 2026Todavía no

Actualiza el sistema y añade el repositorio:

bash
sudo apt update && sudo apt upgrade -y
curl -fsSL https://deb.nodesource.com/setup_24.x | sudo -E bash -
sudo apt install -y nodejs

Verifica la instalación:

bash
node -v
npm -v

Paso 2: Instalar Nginx

Nginx se instala desde los repositorios de la distribución y arranca ya funcionando con una página por defecto. Esa página sirve para confirmar que el tráfico externo llega al servidor antes de complicar la configuración con el proxy.

bash
sudo apt install nginx -y
sudo systemctl enable --now nginx
systemctl status nginx --no-pager

Si visitas http://tu_ip y ves la página «Welcome to nginx», la red y el firewall están bien y el problema, si aparece más adelante, estará en tu configuración y no en la infraestructura.

Paso 3: Crear la aplicación Node.js

La aplicación debe escuchar solo en la interfaz de loopback (127.0.0.1), no en todas las interfaces. Así, aunque el firewall tenga un hueco, nadie puede llegar al puerto 3000 desde fuera y saltarse el proxy, el TLS y los límites de tasa.

bash
mkdir ~/mi_app_node && cd ~/mi_app_node
npm init -y
npm install express

Crea index.js:

javascript
const express = require('express');
const app = express();
const PORT = process.env.PORT || 3000;

// Necesario cuando la app corre detrás de un proxy reverso:
// hace que req.ip y req.protocol usen las cabeceras X-Forwarded-*
app.set('trust proxy', 1);

app.get('/', (req, res) => {
  res.send(`Servidor Node.js funcionando. IP del visitante: ${req.ip}`);
});

// Escuchar solo en loopback: nadie llega al puerto 3000 desde internet
app.listen(PORT, '127.0.0.1', () => {
  console.log(`Servidor escuchando en http://127.0.0.1:${PORT}`);
});

Pruébala:

bash
node index.js

Desde otra terminal del mismo servidor, curl http://127.0.0.1:3000 debe devolver el mensaje. Desde tu máquina no debe responder nada: eso es exactamente lo que buscamos.

Paso 4: Mantener la aplicación viva con PM2

PM2 es un gestor de procesos que mantiene tu aplicación Node ejecutándose en segundo plano, la reinicia si se cae y la vuelve a levantar tras un reinicio del servidor. Sin él, node index.js muere en cuanto cierras la sesión SSH o en cuanto ocurre un error no capturado.

bash
sudo npm install -g pm2
pm2 start index.js --name mi-app
pm2 startup

El comando pm2 startup imprime en pantalla otro comando, largo y con tu nombre de usuario dentro. Hay que copiarlo y ejecutarlo: es el que registra el servicio en systemd. Y después, el paso que más se olvida:

bash
pm2 save

pm2 startup crea el servicio; pm2 save graba qué aplicaciones debe levantar. Si ejecutas el primero y no el segundo, tras reiniciar el servidor PM2 arranca vacío y tu sitio queda caído sin ninguna señal de error evidente. Comprueba el resultado con un reinicio real:

bash
sudo reboot
# al volver a conectar:
pm2 list

Para producción con varios núcleos, el modo cluster reparte las peticiones entre tantos procesos como CPUs tenga el servidor y habilita las recargas sin cortes:

bash
pm2 delete mi-app
pm2 start index.js --name mi-app -i max
pm2 save

Paso 5: Configurar Nginx como proxy reverso

El bloque server de Nginx recibe el tráfico del dominio y lo reenvía a 127.0.0.1:3000, añadiendo las cabeceras que informan a Node quién es el visitante real y por qué protocolo llegó. Sin esas cabeceras, tu aplicación verá todas las peticiones como si vinieran de 127.0.0.1 por HTTP, lo que rompe los logs, la geolocalización, los límites por IP y las redirecciones a HTTPS.

bash
sudo nano /etc/nginx/sites-available/mi_app_node
nginx
server {
    listen 80;
    server_name tu_dominio.com;

    location / {
        proxy_pass http://127.0.0.1:3000;
        proxy_http_version 1.1;

        # Soporte para WebSockets
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';

        # Identidad real del visitante y del protocolo
        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;
    }
}

Activa el sitio, desactiva el sitio por defecto y comprueba la sintaxis antes de recargar:

bash
sudo ln -s /etc/nginx/sites-available/mi_app_node /etc/nginx/sites-enabled/
sudo rm /etc/nginx/sites-enabled/default
sudo nginx -t
sudo systemctl reload nginx

Usa reload en lugar de restart: recarga la configuración sin cortar las conexiones que estén en curso. Y no te saltes nginx -t: si la configuración tiene un error de sintaxis y haces restart, Nginx no arranca y el sitio queda caído; con reload sobre una configuración inválida, Nginx rechaza el cambio y sigue sirviendo la versión anterior.

Paso 6: Configurar el firewall

Permite únicamente SSH y el tráfico web; el puerto 3000 debe permanecer cerrado desde fuera. Nginx recibe en los puertos 80 y 443 y llega a la aplicación por loopback, que no atraviesa el firewall.

bash
sudo ufw allow OpenSSH
sudo ufw allow 'Nginx Full'
sudo ufw enable
sudo ufw status

El orden importa: permite OpenSSH antes de ejecutar ufw enable, o perderás el acceso al servidor. Si quieres profundizar en este punto, lo cubrimos en detalle en la guía de firewalls en Linux.

Paso 7: Habilitar HTTPS con Let's Encrypt

Certbot obtiene el certificado, modifica tu configuración de Nginx para servir HTTPS y programa la renovación automática, todo en un comando. No hace falta añadir ninguna tarea de cron manual: el paquete instala su propio temporizador de systemd.

bash
sudo apt install certbot python3-certbot-nginx -y
sudo certbot --nginx -d tu_dominio.com -d www.tu_dominio.com

Comprueba que la renovación automática está activa y que funciona:

bash
systemctl list-timers | grep certbot
sudo certbot renew --dry-run

Este punto es cada vez más importante: Let's Encrypt está acortando la vida de sus certificados. Hoy los certificados del perfil clásico duran 90 días, el perfil tlsserver pasó a 45 días el 13 de mayo de 2026, y el perfil clásico bajará a 64 días el 10 de febrero de 2027, camino de los 45 días en 2028. Renovar a mano deja de ser viable: la automatización es obligatoria.

Paso 8: Ajustes de Nginx para producción

Tres ajustes marcan la diferencia entre una configuración de tutorial y una de producción: tiempos de espera acordes a tu aplicación, compresión activada y archivos estáticos servidos por Nginx en lugar de por Node.

Tiempos de espera. Los valores por defecto de Nginx (60 segundos) cortan peticiones largas como generación de informes o subidas de archivos:

nginx
location / {
    proxy_pass http://127.0.0.1:3000;
    proxy_read_timeout 90s;
    proxy_connect_timeout 10s;
    proxy_send_timeout 90s;
    # ...resto de cabeceras
}

Nota que proxy_connect_timeout conviene dejarlo bajo: si Node no acepta la conexión en 10 segundos es que está caído, y esperar 90 solo alarga el error para el visitante.

Compresión Gzip. Reduce el tamaño de las respuestas de texto entre un 60% y un 80% según el contenido. Edita /etc/nginx/nginx.conf:

nginx
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml image/svg+xml;

gzip_min_length evita comprimir respuestas diminutas, donde el coste de CPU supera el ahorro de bytes. No incluyas imágenes JPEG o PNG en gzip_types: ya vienen comprimidas y volver a comprimirlas gasta CPU sin reducir el tamaño.

Archivos estáticos directamente desde Nginx. Cada archivo estático que sirve Node es una vuelta innecesaria por el bucle de eventos:

nginx
location /static/ {
    alias /home/tu_usuario/mi_app_node/public/;
    expires 30d;
    add_header Cache-Control "public, immutable";
}

Y para tamaño de subidas, si tu aplicación acepta archivos, recuerda que el límite por defecto de Nginx es 1 MB y devuelve un error 413 sin explicación clara:

nginx
client_max_body_size 20M;

Aplica todo con sudo nginx -t && sudo systemctl reload nginx.

PM2 o systemd: cuál usar para mantener la app viva

PM2 conviene cuando administras varias aplicaciones Node en el mismo servidor; systemd conviene cuando solo tienes una y quieres cero dependencias externas. Ambos cumplen el requisito básico de reiniciar la aplicación si falla y levantarla al arrancar el servidor.

CriterioPM2systemd
Instalaciónnpm install -g pm2Ya viene en el sistema
Modo cluster multi-núcleoIncluido (-i max)Manual, con plantillas de unidad
Recarga sin cortespm2 reloadRequiere configuración adicional
Logspm2 logs en archivos propiosjournalctl -u tu-app
Métricas de CPU y RAMpm2 monitsystemd-cgtop u otras herramientas
Arranque tras reiniciopm2 startup + pm2 savesystemctl enable
Dependencia añadidaSí, un paquete npm globalNinguna

Un ejemplo mínimo de unidad systemd, por si prefieres esa vía. Guárdalo en /etc/systemd/system/mi-app.service:

ini
[Unit]
Description=Aplicacion Node.js
After=network.target

[Service]
Type=simple
User=tu_usuario
WorkingDirectory=/home/tu_usuario/mi_app_node
ExecStart=/usr/bin/node index.js
Restart=on-failure
Environment=NODE_ENV=production

[Install]
WantedBy=multi-user.target
bash
sudo systemctl daemon-reload
sudo systemctl enable --now mi-app

Errores frecuentes y cómo diagnosticarlos

SíntomaCausa habitualSolución
Error 502 Bad GatewayLa app Node no está corriendo o escucha en otro puertopm2 list y curl http://127.0.0.1:3000
Todas las visitas aparecen como 127.0.0.1Faltan las cabeceras X-Real-IP y X-Forwarded-ForAñádelas al bloque location
req.protocol devuelve http con HTTPS activoFalta X-Forwarded-Proto o trust proxyAmbos ajustes, en Nginx y en Express
La app no arranca tras reiniciar el servidorSe ejecutó pm2 startup pero no pm2 savepm2 save con la app corriendo
Error 413 al subir archivosclient_max_body_size por defecto de 1 MBSúbelo en el bloque server
Los WebSockets se desconectanFaltan proxy_http_version 1.1 y cabeceras UpgradeAñádelas al location
Error 504 Gateway TimeoutLa app tarda más que proxy_read_timeoutSube el valor u optimiza el endpoint

Los dos comandos que resuelven la mayoría de estos casos:

bash
sudo tail -f /var/log/nginx/error.log
pm2 logs mi-app --lines 100

Poner Nginx delante de Node.js te da lo mejor de ambos: la agilidad de Node para la lógica de tu aplicación y la robustez de Nginx para TLS, compresión, archivos estáticos y filtrado. Los detalles que separan una instalación que sobrevive en producción de una que da problemas son concretos y fáciles de recordar: usa Node.js 24 LTS y no una versión sin soporte, haz que la aplicación escuche solo en 127.0.0.1, envía las cuatro cabeceras X-Forwarded-* desde Nginx con trust proxy activado en Express, y no olvides pm2 save después de pm2 startup. Si vas a alojar la aplicación y aún no decides el tipo de servidor, en hosting compartido vs VPS vs dedicado explicamos por qué Node.js prácticamente obliga a un VPS con acceso root. Si tienes dudas durante la implementación, nuestro equipo de soporte técnico está disponible 24/7 para ayudarte.

¿Listo para llevar tu sitio al siguiente nivel?

Hosting con LiteSpeed, NVMe y soporte 24/7 desde $3/mes.

Ver planes de hosting

Preguntas frecuentes