Proxy inverso PM2 + Nginx: conecta una app de Node.js al puerto 80

Entorno de pruebaFamilia DebianUbuntu 24.04.5 LTScuenta de usuario normal
Operación de la app Node.js y proxy inverso comprobados

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.

🎯 Puntos clave
🗂️ Flujo de la petición
📂 NginxProxy inverso
📄 CertbotCertificado HTTPS
📂 PM2Ejecución e inicio automático
📄 App de Node.js
Ejecutar la app con PM2 e iniciarla al arrancar
Nginx pasa a la app las peticiones del puerto 80
Requisitos previos
ServidorUbuntu 24.04 con la configuración inicial hechasudo habilitado en una cuenta de usuario normalDominio necesario para HTTPS

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=noninteractive

Si 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 -y

Cuando 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 nodejs
Setting 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 -v
v26.13.0
npm -v
11.20.0

npm 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 ~/hello

Crea 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');
APP

La 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 pm2
added 45 packages in 5s
pm2 -v
[PM2] Spawning PM2 daemon with pm2_home=/home/deploy/.pm2
[PM2] PM2 Successfully daemonized
7.1.2

Al 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 ~/hello

Ve 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/s

online 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 hello
Use --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/s
pm2 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/s

Tras 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/s

PM2 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.pm2

pm2 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 reboot

Unos 2 minutos después, vuelve a conectarte como deploy y comprueba el estado de la app.

uptime -p
up 2 minutes
pm2 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/s

A 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 nginx
Setting 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 -v
nginx 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/hello

Nginx solo lee las configuraciones que tienen un enlace en la carpeta sites-enabled.

🔲 nginx -t

sudo nginx -t
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
sudo systemctl reload nginx

Comprueba 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 4
HTTP/1.1 200 OK
Server: nginx/1.24.0 (Ubuntu)
Date: Sat, 07 Nov 2026 08:09:38 GMT
Content-Length: 16

Despué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.

🧭 Peticiones a la app Node.js y reinicio
Diagrama explicativo
🧭 Situación
Flujo de la petición
🌐NavegadorPetición a example.com
➜
🧱NginxPuerto 80, proxy_pass
➜
🟢App de Node.js127.0.0.1:3000
Gestión de procesos
⚙️systemdServicio pm2-deploy
➜
🔁PM2Lista de apps dump.pm2
➜
🟢App de Node.jshello
Nginx en el puerto 80 recibe la petición del navegador, la pasa a 127.0.0.1:3000 indicado en proxy_pass y devuelve tal cual la respuesta de la app.
Cuando el proceso de la app se detiene, PM2 vuelve a iniciar la misma app al momento. Tras detenerla con kill -9 y mirar pm2 list 3 segundos después, la app estaba online con un pid nuevo.
Cuando arranca el servidor, systemd ejecuta el servicio pm2-deploy, y PM2 vuelve a iniciar las apps según la lista guardada con pm2 save.

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 --nginx

La 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

Si Nginx no puede conectar con la dirección de la app indicada en proxy_pass, devuelve 502 Bad Gateway. Al bajar la app con pm2 stop hello y enviar la misma petición salió un 502, y en /var/log/nginx/error.log quedó la línea [connect() failed (111: Connection refused) while connecting to upstream] junto con la dirección a la que intentaba conectar, http://127.0.0.1:3000/. Empieza por comprobar en pm2 list que la app está online y que el puerto en el que la app recibe peticiones coincide con la dirección de proxy_pass. Al reiniciar la app, la misma petición volvió con 200.

PM2 guarda la salida de la app en archivos con el nombre de cada app dentro de la carpeta .pm2/logs, bajo la carpeta personal de la cuenta que la ejecutó. Al ejecutar pm2 logs hello --nostream --lines 5 aparecieron las últimas líneas de dos archivos, /home/deploy/.pm2/logs/hello-out.log y hello-error.log, y el archivo out tenía la petición GET / que registró la app. Sin --nostream, sigue mostrando los logs nuevos a medida que llegan.

El servicio de inicio automático se elimina con pm2 unstartup systemd. Después de añadir o quitar apps, vuelve a ejecutar pm2 save para que al arrancar se ejecuten según la lista cambiada. Al ejecutar pm2 startup, estos dos comandos también aparecían al final de la salida.

ℹ️ 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.

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Demuestra que eres humano: 7   +   7   =