Proxy inverso PM2 + Nginx: conecta una app de Node.js al puerto 80
En un servidor Ubuntu 24.04 montamos un proxy inverso Node.js con PM2 y Nginx: PM2 se encarga de ejecutar la app y Nginx pasa a la app las peticiones que llegan al puerto 80. También comprobamos que PM2 vuelve a iniciar la app aunque se mate su proceso o se reinicie el servidor.
Las cajas de resultados de abajo son la salida de comandos ejecutados con el usuario normal deploy. En lugar del dominio se usa el valor de ejemplo example.com, y las peticiones se enviaron desde dentro del servidor con la cabecera Host.
1️⃣ Instalar Node.js
Node.js se instala registrando el repositorio de NodeSource en apt, y elegimos Node.js 26, la versión LTS a noviembre de 2026.
🔲 NodeSource setup_26.x
export DEBIAN_FRONTEND=noninteractiveSi defines este valor primero, no aparece la ventana que pide ajustes durante la instalación de paquetes. La opción -E del comando sudo de abajo pasa este valor también al comando que se ejecuta con permisos de root.
curl -fsSL https://deb.nodesource.com/setup_26.x | sudo -E bash -2026-11-07 16:08:16 - Repository configured successfully.
2026-11-07 16:08:16 - To install Node.js, run: apt install nodejs -yCuando el script termina de registrar el repositorio, puedes instalar Node.js con apt como cualquier paquete de Ubuntu.
🔲 apt-get install nodejs
sudo -E apt-get install -y nodejsSetting up libatomic1:amd64 (14.2.0-4ubuntu2~24.04.1) ...
Setting up nodejs (26.13.0-1nodesource1) ...
Processing triggers for libc-bin (2.39-0ubuntu8.9) ...
Processing triggers for man-db (2.12.0-4build2) ...🔲 node -v, npm -v
node -vv26.13.0npm -v11.20.0npm viene incluido en el paquete nodejs, así que no hace falta instalarlo aparte.
2️⃣ App de prueba de Node.js
Para ver si PM2 y Nginx se conectan bien, crea una app de prueba que devuelve una línea de texto al recibir una petición.
🔲 app.js
mkdir -p ~/helloCrea la carpeta hello dentro de tu carpeta personal y escribe en ella el archivo app.js.
cat > ~/hello/app.js <<'APP'
const http = require('http');
http.createServer((req, res) => {
console.log(req.method, req.url);
res.end('hello from node\n');
}).listen(3000, '127.0.0.1');
APPLa app solo acepta peticiones en el puerto 3000 de 127.0.0.1, así que no se puede abrir directamente desde fuera del servidor. Configuraremos Nginx para que reciba las peticiones de fuera y las pase a este puerto.
3️⃣ Ejecutar la app con PM2 e inicio automático
PM2 es un gestor de procesos que ejecuta apps de Node.js en segundo plano y vuelve a iniciar una app cuando su proceso se detiene. En esta sección también configuramos el inicio automático, que vuelve a levantar la app cuando arranca el servidor.
🔲 npm install -g pm2
sudo npm install -g pm2added 45 packages in 5spm2 -v[PM2] Spawning PM2 daemon with pm2_home=/home/deploy/.pm2
[PM2] PM2 Successfully daemonized
7.1.2Al ejecutar un comando pm2 por primera vez también se inicia el demonio de PM2, y la versión de PM2 instalada es la 7.1.2.
🔲 pm2 start
cd ~/helloVe a la carpeta donde está app.js y ejecuta la app con PM2.
pm2 start app.js --name hello[PM2] Starting /home/deploy/hello/app.js in fork_mode (1 instance)
[PM2] Done.
┌────┬──────────┬─────────────┬─────────┬─────────┬──────────┬────────┬──────┬───────────┬──────────┬──────────┬──────────┬──────────┐
│ id │ name │ namespace │ version │ mode │ pid │ uptime │ ↺ │ status │ cpu │ mem │ user │ watching │
├────┼──────────┼─────────────┼─────────┼─────────┼──────────┼────────┼──────┼───────────┼──────────┼──────────┼──────────┼──────────┤
│ 0 │ hello │ default │ N/A │ fork │ 17257 │ 0s │ 0 │ online │ 0% │ 26.3mb │ deploy │ disabled │
└────┴──────────┴─────────────┴─────────┴─────────┴──────────┴────────┴──────┴───────────┴──────────┴──────────┴──────────┴──────────┘--name es el nombre de la app que se verá en la lista de PM2, y los comandos siguientes indican la app con este nombre.
pm2 list┌────┬──────────┬─────────────┬─────────┬─────────┬──────────┬────────┬──────┬───────────┬──────────┬──────────┬──────────┬──────────┐
│ id │ name │ namespace │ version │ mode │ pid │ uptime │ ↺ │ status │ cpu │ mem │ user │ watching │
├────┼──────────┼─────────────┼─────────┼─────────┼──────────┼────────┼──────┼───────────┼──────────┼──────────┼──────────┼──────────┤
│ 0 │ hello │ default │ N/A │ fork │ 17257 │ 2s │ 0 │ online │ 0% │ 58.5mb │ deploy │ disabled │
└────┴──────────┴─────────────┴─────────┴─────────┴──────────┴────────┴──────┴───────────┴──────────┴──────────┴──────────┴──────────┘
host metrics | cpu: 0% | ram usage: 38.8% | disk: ⇓ 0mb/s ⇑ 0mb/sonline en la columna status significa que la app está en ejecución, y la columna ↺ es el número de veces que PM2 ha reiniciado la app.
🔲 curl 127.0.0.1:3000
Si envías una petición desde dentro del servidor, aparece tal cual la línea de texto que devuelve la app.
curl -sS http://127.0.0.1:3000/hello from node🔲 pm2 restart
Después de cambiar el código de la app, reiníciala para aplicar los cambios.
pm2 restart helloUse --update-env to update environment variables
[PM2] Applying action restartProcessId on app [hello](ids: [ 0 ])
[PM2] [hello](0) ✓
┌────┬──────────┬─────────────┬─────────┬─────────┬──────────┬────────┬──────┬───────────┬──────────┬──────────┬──────────┬──────────┐
│ id │ name │ namespace │ version │ mode │ pid │ uptime │ ↺ │ status │ cpu │ mem │ user │ watching │
├────┼──────────┼─────────────┼─────────┼─────────┼──────────┼────────┼──────┼───────────┼──────────┼──────────┼──────────┼──────────┤
│ 0 │ hello │ default │ N/A │ fork │ 17306 │ 0s │ 1 │ online │ 0% │ 19.3mb │ deploy │ disabled │
└────┴──────────┴─────────────┴─────────┴─────────┴──────────┴────────┴──────┴───────────┴──────────┴──────────┴──────────┴──────────┘
host metrics | cpu: 0% | ram usage: 38.8% | disk: ⇓ 0mb/s ⇑ 0mb/spm2 list┌────┬──────────┬─────────────┬─────────┬─────────┬──────────┬────────┬──────┬───────────┬──────────┬──────────┬──────────┬──────────┐
│ id │ name │ namespace │ version │ mode │ pid │ uptime │ ↺ │ status │ cpu │ mem │ user │ watching │
├────┼──────────┼─────────────┼─────────┼─────────┼──────────┼────────┼──────┼───────────┼──────────┼──────────┼──────────┼──────────┤
│ 0 │ hello │ default │ N/A │ fork │ 17306 │ 2s │ 1 │ online │ 0% │ 58.3mb │ deploy │ disabled │
└────┴──────────┴─────────────┴─────────┴─────────┴──────────┴────────┴──────┴───────────┴──────────┴──────────┴──────────┴──────────┘
host metrics | cpu: 0% | ram usage: 38.8% | disk: ⇓ 0mb/s ⇑ 0mb/sTras reiniciar, el pid cambió de 17257 a 17306 y la columna ↺ subió a 1.
🔲 kill -9
Puedes provocar que el proceso de la app se detenga de golpe con kill -9, y pm2 pid muestra el número de proceso de la app.
kill -9 "$(pm2 pid hello)"Vuelve a revisar la lista de PM2 al cabo de 3 segundos.
pm2 list┌────┬──────────┬─────────────┬─────────┬─────────┬──────────┬────────┬──────┬───────────┬──────────┬──────────┬──────────┬──────────┐
│ id │ name │ namespace │ version │ mode │ pid │ uptime │ ↺ │ status │ cpu │ mem │ user │ watching │
├────┼──────────┼─────────────┼─────────┼─────────┼──────────┼────────┼──────┼───────────┼──────────┼──────────┼──────────┼──────────┤
│ 0 │ hello │ default │ N/A │ fork │ 17797 │ 3s │ 2 │ online │ 0% │ 58.6mb │ deploy │ disabled │
└────┴──────────┴─────────────┴─────────┴─────────┴──────────┴────────┴──────┴───────────┴──────────┴──────────┴──────────┴──────────┘
host metrics | cpu: 51.2% | ram usage: 35.8% | eth0: ⇓ 1kb/s ⇑ 1kb/s | eth1: ⇓ 19kb/s ⇑ 0mb/s | disk: ⇓ 728kb/s ⇑ 3.1mb/sPM2 había vuelto a iniciar la app con el nuevo pid 17797 y estaba online, y la columna ↺ había subido a 2.
🔲 pm2 save
pm2 save[PM2] Saving current process list...
[PM2] Successfully saved in /home/deploy/.pm2/dump.pm2pm2 save guarda la lista de apps en ejecución en el archivo dump.pm2, y al arrancar PM2 vuelve a iniciar las apps según esta lista.
🔲 pm2 startup
Para que PM2 se ejecute al arrancar, registra PM2 como servicio de systemd. El comando de abajo crea el servicio con el nombre de la cuenta actual y su carpeta personal.
sudo env PATH="$PATH" pm2 startup systemd -u "$(id -un)" --hp "$HOME"ExecStart=/usr/lib/node_modules/pm2/bin/pm2 resurrect
ExecReload=/usr/lib/node_modules/pm2/bin/pm2 reload all
ExecStop=/usr/lib/node_modules/pm2/bin/pm2 kill
[Install]
WantedBy=multi-user.target
Target path
/etc/systemd/system/pm2-deploy.service
Command list
[ 'systemctl enable pm2-deploy' ]
[PM2] Writing init configuration in /etc/systemd/system/pm2-deploy.service
[PM2] Making script booting at startup...
[PM2] [-] Executing: systemctl enable pm2-deploy...
Created symlink /etc/systemd/system/multi-user.target.wants/pm2-deploy.service → /etc/systemd/system/pm2-deploy.service.
[PM2] [v] Command successfully executed.env PATH es la parte que permite que pm2, ejecutado con permisos de root, encuentre la ruta de node de la cuenta actual, y -u y --hp son la cuenta y la carpeta personal con las que se ejecuta el servicio. La línea ExecStart del archivo de servicio es pm2 resurrect, que restaura al arrancar la lista de apps guardada.
systemctl status pm2-deploy --no-pager | head -n 5○ pm2-deploy.service - PM2 process manager
Loaded: loaded (/etc/systemd/system/pm2-deploy.service; enabled; preset: enabled)
Active: inactive (dead)
Docs: https://pm2.keymetrics.io/enabled significa que el servicio se ejecuta al arrancar. pm2 startup solo registra el servicio y no lo inicia ahora, por eso aparece como inactive (dead), y el servicio se ejecuta a partir del siguiente arranque.
🔲 pm2 list tras reiniciar
sudo systemctl rebootUnos 2 minutos después, vuelve a conectarte como deploy y comprueba el estado de la app.
uptime -pup 2 minutespm2 list┌────┬──────────┬─────────────┬─────────┬─────────┬──────────┬────────┬──────┬───────────┬──────────┬──────────┬──────────┬──────────┐
│ id │ name │ namespace │ version │ mode │ pid │ uptime │ ↺ │ status │ cpu │ mem │ user │ watching │
├────┼──────────┼─────────────┼─────────┼─────────┼──────────┼────────┼──────┼───────────┼──────────┼──────────┼──────────┼──────────┤
│ 0 │ hello │ default │ N/A │ fork │ 976 │ 108s │ 0 │ online │ 0% │ 63.0mb │ deploy │ disabled │
└────┴──────────┴─────────────┴─────────┴─────────┴──────────┴────────┴──────┴───────────┴──────────┴──────────┴──────────┴──────────┘
host metrics | cpu: 0.3% | ram usage: 35.1% | disk: ⇓ 4kb/s ⇑ 4kb/sA los 2 minutos de encenderse el servidor, la app ya llevaba 108 segundos online: PM2 la había vuelto a iniciar junto con el arranque.
4️⃣ Configurar el proxy inverso de Nginx
Como la app solo acepta peticiones en 127.0.0.1:3000, Nginx recibe las peticiones que llegan al puerto 80 y las pasa a la app. Esta configuración, en la que un servidor delante recibe las peticiones y las reenvía a un servidor detrás, se llama proxy inverso.
🔲 apt-get install nginx
sudo -E apt-get install -y nginxSetting up nginx-common (1.24.0-2ubuntu7.19) ...
Created symlink /etc/systemd/system/multi-user.target.wants/nginx.service → /usr/lib/systemd/system/nginx.service.
Setting up nginx (1.24.0-2ubuntu7.19) ...nginx -vnginx version: nginx/1.24.0 (Ubuntu)El Nginx del repositorio de Ubuntu 24.04 es la versión 1.24.0, y al instalarlo queda registrado para ejecutarse al arrancar.
🔲 /etc/nginx/sites-available/hello
Crea el archivo /etc/nginx/sites-available/hello y pon dentro el contenido siguiente.
server {
listen 80;
server_name example.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";
}
}proxy_pass es la dirección de la app a la que se pasan las peticiones. Las cabeceras Host, X-Real-IP, X-Forwarded-For y X-Forwarded-Proto son valores que se pasan para que la app sepa el dominio de la petición original, la IP del visitante y si era http o https.
proxy_http_version 1.1 y las cabeceras Upgrade y Connection hacen falta para pasar las conexiones WebSocket hasta la app. Las cabeceras Upgrade y Connection no llegan a la app tras pasar por un proxy, así que se añaden directamente en la configuración.
sudo ln -sf /etc/nginx/sites-available/hello /etc/nginx/sites-enabled/helloNginx solo lee las configuraciones que tienen un enlace en la carpeta sites-enabled.
🔲 nginx -t
sudo nginx -tnginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successfulsudo systemctl reload nginxComprueba primero la sintaxis de la configuración con nginx -t y, si pasa, aplica la nueva configuración con reload sin detener Nginx.
🔲 Petición curl con cabecera Host
curl -sS -i -H 'Host: example.com' http://127.0.0.1/ | head -n 4HTTP/1.1 200 OK
Server: nginx/1.24.0 (Ubuntu)
Date: Sat, 07 Nov 2026 08:09:38 GMT
Content-Length: 16Después de recargar la configuración, la petición enviada con la cabecera Host recibió la respuesta de la app. La línea Server de las cabeceras de respuesta es nginx, y Content-Length 16 tiene el mismo tamaño que la línea hello from node que devuelve la app (16 bytes con el salto de línea). Para conectarte desde fuera del servidor, el puerto 80 tiene que estar abierto en el cortafuegos.
5️⃣ Aplicar HTTPS con Certbot
Con el plugin de Nginx de Certbot puedes emitir el certificado y cambiar la configuración de Nginx de una vez. Ejecuta el comando de abajo cuando el dominio ya apunte a la IP del servidor.
sudo certbot --nginxLa instalación de Certbot y la prueba de renovación automática están en el artículo de abajo; el comando de emisión de ese artículo es para Apache, así que cambia solo el paso de emisión por el comando de arriba.
🔢 FAQ & contenido recomendado
Logs de servicios
Comando journalctl: uso, registros por servicio y limpieza del tamaño del journal
Artículo sobre cómo leer por servicio los logs de servicios de systemd como nginx o pm2-deploy.
ℹ️ Aviso de afiliados
El contenido de este sitio incluye enlaces de afiliado. Cuando un visitante compra un producto o servicio a través de uno de ellos, el sitio recibe una comisión del vendedor. El importe que paga el comprador(baja durante las promociones ↓)no aumenta. Los precios, descuentos y existencias publicados corresponden al momento de redacción y pueden variar, así que confírmalos con el vendedor antes de comprar. Los productos se eligen y evalúan con criterios propios, y las comisiones no influyen en el orden ni en el contenido de las reseñas.