--- title: "Cómo desplegar Express.js en Ubuntu 24.04 con PM2 y Nginx" description: "Despliega una aplicación Express.js en Ubuntu 24.04 con Node.js 24, PM2 en modo cluster, Nginx como proxy inverso, HTTPS de Let's Encrypt y recargas sin cortes." category: "API y Microservicios" language: es source: https://cubepath.com/es/docs/api-y-microservicios/despliegue-de-aplicacion-express-js-en-linux updated: 2026-09-25 --- # Cómo desplegar Express.js en Ubuntu 24.04 con PM2 y Nginx Express.js es el framework web más usado de Node.js para construir APIs y backends. Node ejecuta el código JavaScript en un solo hilo, así que en producción necesitas un gestor de procesos que lo arranque con el sistema, lo reinicie si cae y reparta la carga entre los núcleos. En este tutorial desplegarás una aplicación Express en Ubuntu 24.04 con Node.js 24 LTS, la ejecutarás con PM2 en modo cluster bajo un usuario dedicado y la publicarás detrás de Nginx con HTTPS, con recargas sin cortes en cada actualización. ## Requisitos previos - Un servidor con Ubuntu 24.04 LTS, por ejemplo un VPS de CubePath, con al menos 1 GB de RAM y 2 vCPU para aprovechar el modo cluster. - Un usuario no root con privilegios `sudo`. - Un dominio con un registro A apuntando a la IP del servidor. En esta guía se usa `your_domain`. - Los puertos 80 y 443 accesibles desde Internet. ## Paso 1: Instalar Node.js 24 LTS Ubuntu 24.04 incluye Node.js 18, que ya no tiene soporte. Instala la rama LTS actual desde el repositorio de NodeSource. Primero añade su clave de firma: ```bash sudo apt update sudo apt install ca-certificates curl gnupg sudo install -d -m 0755 /etc/apt/keyrings curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | sudo gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg ``` Añade el repositorio de Node.js 24 e instala el paquete, que incluye `npm`: ```bash echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_24.x nodistro main" | sudo tee /etc/apt/sources.list.d/nodesource.list sudo apt update sudo apt install nodejs ``` Comprueba la versión instalada: ```bash node --version ``` ```output v24.21.0 ``` Instala PM2 de forma global: ```bash sudo npm install -g pm2 pm2 --version ``` ## Paso 2: Crear el usuario de la aplicación La aplicación y PM2 se ejecutarán con un usuario propio, sin privilegios de administrador. Su directorio personal, `/srv/expressapp`, contendrá el código y la carpeta `.pm2` con el estado y los logs de PM2: ```bash sudo adduser --system --group --home /srv/expressapp --shell /bin/bash expressapp ``` A partir de aquí, los comandos marcados como "como `expressapp`" se ejecutan en una shell de ese usuario, que abres con: ```bash sudo -iu expressapp ``` Sal de ella con `exit` cuando el paso vuelva a usar `sudo`. ## Paso 3: Preparar la aplicación Express Como `expressapp`, crea el proyecto e instala Express 5 y Helmet, que añade cabeceras de seguridad HTTP: ```bash mkdir ~/app && cd ~/app npm init -y npm pkg set type=commonjs npm install express helmet ``` Si ya tienes tu aplicación en un repositorio, clónala en `~/app` y ejecuta `npm ci --omit=dev` en su lugar; el resto de la guía es igual. Para seguir con el ejemplo, crea `app.js`: ```bash nano ~/app/app.js ``` ```javascript const express = require('express'); const helmet = require('helmet'); // Carga .env del directorio de trabajo si existe try { process.loadEnvFile(); } catch (err) { if (err.code !== 'ENOENT') throw err; } const app = express(); app.set('trust proxy', 'loopback'); app.disable('x-powered-by'); app.use(helmet()); app.use(express.json({ limit: '1mb' })); app.get('/health', (req, res) => { res.json({ status: 'ok', pid: process.pid, uptime: Math.round(process.uptime()) }); }); app.get('/', (req, res) => { res.json({ message: 'Hola desde Express', ip: req.ip }); }); app.use((err, req, res, next) => { console.error(err); res.status(500).json({ error: 'Error interno del servidor' }); }); const port = Number(process.env.PORT) || 3000; const host = process.env.HOST || '127.0.0.1'; const server = app.listen(port, host, (err) => { if (err) throw err; console.log(`Express escuchando en ${host}:${port} (PID ${process.pid})`); // Avisa a PM2 de que la instancia está lista (wait_ready) if (process.send) process.send('ready'); }); function shutdown(signal) { console.log(`${signal} recibido, cerrando servidor`); server.close(() => process.exit(0)); // Cierra también las conexiones keep-alive inactivas server.closeIdleConnections(); } process.on('SIGINT', () => shutdown('SIGINT')); process.on('SIGTERM', () => shutdown('SIGTERM')); ``` Hay tres detalles pensados para producción: - `trust proxy` con `loopback` hace que `req.ip` sea la IP real del cliente que envía Nginx en `X-Forwarded-For`, y no `127.0.0.1`. - `process.send('ready')` le indica a PM2 que la instancia ya acepta conexiones. Durante una recarga, PM2 no detiene la instancia antigua hasta recibir esa señal de la nueva. - PM2 detiene los procesos con `SIGINT`. El manejador deja de aceptar conexiones y sale cuando terminan las peticiones en curso. Guarda la configuración y los secretos en un archivo `.env` que solo pueda leer el usuario de la aplicación. `process.loadEnvFile()` lo carga al arrancar: ```bash nano ~/app/.env ``` ```ini PORT=3000 HOST=127.0.0.1 # DATABASE_URL=postgres://app:your_strong_password@127.0.0.1:5432/app ``` ```bash chmod 600 ~/app/.env ``` Prueba la aplicación antes de meterla en PM2: ```bash node app.js ``` ```output Express escuchando en 127.0.0.1:3000 (PID 5120) ``` Detenla con `Ctrl+C`. ## Paso 4: Ejecutar la aplicación con PM2 en modo cluster El archivo de ecosistema describe cómo arrancar la aplicación y se versiona junto al código. Como `expressapp`, créalo: ```bash nano ~/app/ecosystem.config.js ``` ```javascript module.exports = { apps: [ { name: 'expressapp', script: 'app.js', cwd: '/srv/expressapp/app', exec_mode: 'cluster', instances: 'max', env_production: { NODE_ENV: 'production', }, max_memory_restart: '512M', wait_ready: true, listen_timeout: 10000, kill_timeout: 10000, time: true, }, ], }; ``` - `exec_mode: 'cluster'` con `instances: 'max'` arranca un proceso por núcleo y todos comparten el puerto 3000. Usa un número fijo (por ejemplo `2`) si el servidor también ejecuta una base de datos. - `wait_ready` y `listen_timeout` hacen que PM2 espere la señal `ready` hasta 10 segundos. - `kill_timeout` da 10 segundos a cada proceso para cerrar antes de forzarlo. - `max_memory_restart` reinicia una instancia que supere 512 MB, una red de seguridad frente a fugas de memoria. - `time` añade la fecha a cada línea de log. Arranca la aplicación con el entorno de producción: ```bash cd ~/app pm2 start ecosystem.config.js --env production pm2 list ``` ```output id name mode pid uptime ↺ status mem 0 expressapp cluster 5301 2s 0 online 58.1mb 1 expressapp cluster 5308 2s 0 online 57.9mb ``` Llama varias veces a `/health` y verás que responden procesos distintos: ```bash curl -s http://127.0.0.1:3000/health; echo curl -s http://127.0.0.1:3000/health; echo ``` ```output {"status":"ok","pid":5301,"uptime":8} {"status":"ok","pid":5308,"uptime":8} ``` Por defecto PM2 escribe los logs en `~/.pm2/logs` sin límite de tamaño. Instala el módulo de rotación y guarda la lista de procesos para que PM2 la restaure al arrancar: ```bash pm2 install pm2-logrotate pm2 save exit ``` ## Paso 5: Arrancar PM2 con el sistema PM2 genera una unidad systemd que lo arranca con el usuario indicado y restaura los procesos guardados con `pm2 save`. Con tu usuario sudo, ejecuta: ```bash sudo pm2 startup systemd -u expressapp --hp /srv/expressapp ``` El comando crea y activa el servicio `pm2-expressapp`. Compruébalo: ```bash systemctl status pm2-expressapp --no-pager ``` ```output ● pm2-expressapp.service - PM2 process manager Loaded: loaded (/etc/systemd/system/pm2-expressapp.service; enabled; preset: enabled) Active: active (running) since ... ``` Para confirmar que todo vuelve tras un reinicio, ejecuta `sudo reboot` y, al volver a entrar, `sudo -iu expressapp pm2 list`. ## Paso 6: Configurar Nginx como proxy inverso Instala Nginx y permite el tráfico web en UFW: ```bash sudo apt install nginx sudo ufw allow OpenSSH sudo ufw allow 'Nginx Full' sudo ufw enable ``` Crea el sitio: ```bash sudo nano /etc/nginx/sites-available/expressapp ``` ```nginx upstream expressapp { server 127.0.0.1:3000; keepalive 32; } server { listen 80; listen [::]:80; server_name your_domain; client_max_body_size 10m; location / { proxy_pass http://expressapp; proxy_http_version 1.1; proxy_set_header Connection ""; 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; } location = /health { proxy_pass http://expressapp; access_log off; } } ``` Si tu aplicación usa WebSockets (Socket.IO, por ejemplo), añade en `location /` las cabeceras `Upgrade $http_upgrade` y `Connection "upgrade"` en lugar de `Connection ""`. Activa el sitio, desactiva el de por defecto y recarga Nginx: ```bash sudo ln -s /etc/nginx/sites-available/expressapp /etc/nginx/sites-enabled/ sudo rm /etc/nginx/sites-enabled/default sudo nginx -t sudo systemctl reload nginx ``` Desde tu equipo, comprueba que la IP que ve Express es la tuya y no la de Nginx: ```bash curl -s http://your_domain/ ``` ```output {"message":"Hola desde Express","ip":"203.0.113.25"} ``` ## Paso 7: Activar HTTPS Instala Certbot y obtén un certificado de Let's Encrypt. Certbot modifica el sitio para escuchar en 443 y redirigir HTTP a HTTPS: ```bash sudo apt install certbot python3-certbot-nginx sudo certbot --nginx -d your_domain --redirect sudo certbot renew --dry-run ``` Comprueba el resultado: ```bash curl -sI https://your_domain/health ``` ```output HTTP/1.1 200 OK Server: nginx/1.24.0 (Ubuntu) Content-Type: application/json; charset=utf-8 Strict-Transport-Security: max-age=31536000; includeSubDomains ... ``` La cabecera `Strict-Transport-Security` y el resto de cabeceras de seguridad las añade Helmet. ## Paso 8: Desplegar nuevas versiones sin cortes `pm2 reload` reinicia las instancias del cluster de una en una: arranca la nueva, espera su señal `ready` y solo entonces cierra la antigua, así que siempre hay procesos atendiendo peticiones. Un despliegue típico desde Git, como `expressapp`, es: ```bash cd ~/app git pull npm ci --omit=dev pm2 reload ecosystem.config.js --env production ``` Pasar el archivo de ecosistema a `pm2 reload` aplica también los cambios que hayas hecho en él. Verifica que los PID han cambiado y que la aplicación responde: ```bash pm2 list curl -s http://127.0.0.1:3000/health ``` Si la nueva versión falla, vuelve al commit anterior y recarga. Anota antes el hash con `git log --oneline -n 2`: ```bash git checkout previous_commit_hash npm ci --omit=dev pm2 reload ecosystem.config.js --env production ``` Cuando hayas corregido el fallo, vuelve a la rama con `git checkout main` antes del siguiente `git pull`. Usa `pm2 restart` solo cuando quieras parar todas las instancias a la vez, por ejemplo tras actualizar Node.js. Después de una actualización de PM2, ejecuta `pm2 update` para que el demonio en memoria use la nueva versión. ## Solución de problemas **`pm2 list` no muestra procesos tras un reinicio del servidor.** Olvidaste `pm2 save` después de arrancar la aplicación, o ejecutaste `pm2 startup` para otro usuario. Revisa `systemctl status pm2-expressapp` y vuelve a hacer `pm2 save` como `expressapp`. **Las instancias se reinician en bucle (columna `↺` creciendo).** La aplicación falla al arrancar. Mira el error con `pm2 logs expressapp --lines 50`. Si ves `listen_timeout` agotado, la aplicación no está llamando a `process.send('ready')`. **Nginx devuelve 502 Bad Gateway.** La aplicación no escucha en `127.0.0.1:3000`. Comprueba con `sudo ss -ltnp 'sport = :3000'` y revisa `/var/log/nginx/error.log`. **`req.ip` devuelve `127.0.0.1`.** Falta `app.set('trust proxy', 'loopback')` o la cabecera `X-Forwarded-For` en el bloque de Nginx. **`EACCES` al instalar paquetes.** Estás ejecutando `npm install` con tu usuario en un directorio de `expressapp`, o con `sudo`. Instala siempre las dependencias como `expressapp` para que los archivos tengan el propietario correcto. ## Conclusión Tu aplicación Express corre ahora con Node.js 24 LTS bajo un usuario sin privilegios, repartida en todos los núcleos por PM2, arranca con el sistema y se publica con HTTPS a través de Nginx. Las actualizaciones se hacen con `pm2 reload` sin perder peticiones. Como siguientes pasos, puedes añadir límites de peticiones en Nginx con `limit_req`, conectar la aplicación a una base de datos PostgreSQL o automatizar el despliegue desde tu CI con los comandos del paso 8.