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ón | Estado en agosto de 2026 | Fin de vida | ¿Usar en producción? |
|---|---|---|---|
| Node.js 18 | Fin de vida | 30 abril 2025 | No, sin parches de seguridad |
| Node.js 22 | Maintenance LTS | 30 abril 2027 | Solo si ya está en uso |
| Node.js 24 | Active LTS | 30 abril 2028 | Sí, opción recomendada |
| Node.js 26 | Current | Pasa a LTS en octubre 2026 | Todavía no |
Actualiza el sistema y añade el repositorio:
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:
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.
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.
mkdir ~/mi_app_node && cd ~/mi_app_node
npm init -y
npm install express Crea index.js:
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:
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.
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:
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:
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:
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.
sudo nano /etc/nginx/sites-available/mi_app_node 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:
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.
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.
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:
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:
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:
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:
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:
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.
| Criterio | PM2 | systemd |
|---|---|---|
| Instalación | npm install -g pm2 | Ya viene en el sistema |
| Modo cluster multi-núcleo | Incluido (-i max) | Manual, con plantillas de unidad |
| Recarga sin cortes | pm2 reload | Requiere configuración adicional |
| Logs | pm2 logs en archivos propios | journalctl -u tu-app |
| Métricas de CPU y RAM | pm2 monit | systemd-cgtop u otras herramientas |
| Arranque tras reinicio | pm2 startup + pm2 save | systemctl enable |
| Dependencia añadida | Sí, un paquete npm global | Ninguna |
Un ejemplo mínimo de unidad systemd, por si prefieres esa vía. Guárdalo en /etc/systemd/system/mi-app.service:
[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 sudo systemctl daemon-reload
sudo systemctl enable --now mi-app Errores frecuentes y cómo diagnosticarlos
| Síntoma | Causa habitual | Solución |
|---|---|---|
| Error 502 Bad Gateway | La app Node no está corriendo o escucha en otro puerto | pm2 list y curl http://127.0.0.1:3000 |
| Todas las visitas aparecen como 127.0.0.1 | Faltan las cabeceras X-Real-IP y X-Forwarded-For | Añádelas al bloque location |
req.protocol devuelve http con HTTPS activo | Falta X-Forwarded-Proto o trust proxy | Ambos ajustes, en Nginx y en Express |
| La app no arranca tras reiniciar el servidor | Se ejecutó pm2 startup pero no pm2 save | pm2 save con la app corriendo |
| Error 413 al subir archivos | client_max_body_size por defecto de 1 MB | Súbelo en el bloque server |
| Los WebSockets se desconectan | Faltan proxy_http_version 1.1 y cabeceras Upgrade | Añádelas al location |
| Error 504 Gateway Timeout | La app tarda más que proxy_read_timeout | Sube el valor u optimiza el endpoint |
Los dos comandos que resuelven la mayoría de estos casos:
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.
Hosting con LiteSpeed, NVMe y soporte 24/7 desde $3/mes.