Un proxy inverso recibe las peticiones de los clientes y las reenvía a una o varias aplicaciones que escuchan en la red interna. Colocar Nginx delante de una aplicación Node.js, Python, Go o de un contenedor te permite terminar HTTPS en un único punto, servir varias aplicaciones con la misma IP, repartir la carga entre instancias y no exponer los puertos de la aplicación a Internet. En este tutorial configurarás Nginx como proxy inverso en Ubuntu 24.04, primero con una sola aplicación y después con HTTPS, WebSockets, balanceo de carga y caché.

Requisitos previos

Para seguir esta guía necesitas:

  • Un servidor con Ubuntu 24.04 LTS, por ejemplo un VPS de CubePath, y un usuario no root con privilegios sudo.
  • Un dominio con un registro DNS A apuntando a la IP del servidor. En la guía se usa your_domain como marcador.
  • Una aplicación que escuche en un puerto local. Si todavía no tienes una, en el paso 2 se levanta un backend de prueba con Python, que viene instalado en Ubuntu.

Paso 1: Instalar Nginx y abrir el firewall

Instala Nginx desde los repositorios de Ubuntu:

sudo apt update
sudo apt install nginx

El servicio arranca y queda habilitado automáticamente. Compruébalo:

systemctl is-active nginx
active

Permite el tráfico HTTP y HTTPS en UFW:

sudo ufw allow 'Nginx Full'

Paso 2: Preparar una aplicación de prueba

Para comprobar el proxy necesitas algo detrás. Si ya tienes una aplicación en 127.0.0.1:3000, salta al paso 3. Si no, crea un directorio con una página y sírvela con el servidor HTTP de Python, escuchando solo en la interfaz local:

mkdir -p ~/app1
echo "Respuesta del backend 1" > ~/app1/index.html
python3 -m http.server 3000 --bind 127.0.0.1 --directory ~/app1 > /dev/null 2>&1 &

El & deja el proceso en segundo plano en tu sesión. Es suficiente para este tutorial; una aplicación real debe ejecutarse como servicio de systemd. Comprueba que responde:

curl http://127.0.0.1:3000
Respuesta del backend 1

Que la aplicación escuche en 127.0.0.1 y no en 0.0.0.0 es importante: así solo es accesible a través de Nginx, y nadie puede saltarse el proxy conectándose directamente al puerto 3000.

Paso 3: Crear el bloque de servidor del proxy

Crea un archivo de configuración para el sitio:

sudo nano /etc/nginx/sites-available/your_domain
server {
    listen 80;
    listen [::]:80;
    server_name your_domain www.your_domain;

    location / {
        proxy_pass http://127.0.0.1:3000;
        include proxy_params;
    }
}

proxy_pass indica a dónde se reenvían las peticiones. El archivo /etc/nginx/proxy_params, incluido en el paquete de Ubuntu, añade las cabeceras que la aplicación necesita para conocer la petición original:

proxy_set_header Host $http_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;

Sin ellas, la aplicación vería todas las peticiones como procedentes de 127.0.0.1 y no sabría si el cliente usó HTTPS, lo que rompe redirecciones, enlaces absolutos y registros de acceso. Configura tu framework para que confíe en estas cabeceras cuando vengan del proxy (por ejemplo, app.set('trust proxy', 'loopback') en Express).

Activa el sitio y desactiva el sitio por defecto de Nginx:

sudo ln -s /etc/nginx/sites-available/your_domain /etc/nginx/sites-enabled/
sudo rm /etc/nginx/sites-enabled/default

Valida la configuración y recarga:

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

Prueba el proxy desde tu equipo:

curl -i http://your_domain
HTTP/1.1 200 OK
Server: nginx/1.24.0 (Ubuntu)
Content-Type: text/html
...

Respuesta del backend 1

La cabecera Server: nginx y el contenido del backend confirman que la petición ha pasado por el proxy.

Paso 4: Activar HTTPS

Con el proxy funcionando por HTTP, obtén un certificado de Let's Encrypt. Certbot añade la configuración TLS y la redirección al mismo bloque de servidor:

sudo apt install certbot python3-certbot-nginx
sudo certbot --nginx -d your_domain -d www.your_domain

Comprueba que el proxy responde por HTTPS:

curl -I https://your_domain
HTTP/1.1 200 OK
Server: nginx/1.24.0 (Ubuntu)

A partir de aquí la aplicación recibe X-Forwarded-Proto: https, aunque la conexión entre Nginx y el backend sea HTTP sin cifrar dentro del servidor.

Paso 5: Ajustar tiempos de espera y tamaño de subida

Los valores por defecto de Nginx cortan una respuesta que tarde más de 60 segundos y rechazan cuerpos de petición de más de 1 MB. Si tu aplicación genera informes largos o acepta subidas de archivos, ajústalos en el bloque server del puerto 443:

sudo nano /etc/nginx/sites-available/your_domain
    client_max_body_size 50m;

    location / {
        proxy_pass http://127.0.0.1:3000;
        include proxy_params;

        proxy_connect_timeout 5s;
        proxy_read_timeout 120s;
        proxy_send_timeout 120s;
    }
  • client_max_body_size: tamaño máximo del cuerpo de la petición. Si se supera, Nginx responde 413 Request Entity Too Large.
  • proxy_connect_timeout: tiempo para establecer la conexión con el backend. En local debe ser casi inmediato, así que un valor bajo detecta antes un backend caído.
  • proxy_read_timeout: tiempo máximo entre dos lecturas de la respuesta del backend. Si se supera, Nginx devuelve 504 Gateway Timeout.

Valida y recarga con sudo nginx -t && sudo systemctl reload nginx.

Paso 6: Soportar WebSockets

Las conexiones WebSocket empiezan como una petición HTTP con las cabeceras Upgrade y Connection, que Nginx no reenvía por defecto. Crea un map en el contexto http para fijar Connection según la petición:

sudo nano /etc/nginx/conf.d/websocket-map.conf
map $http_upgrade $connection_upgrade {
    default upgrade;
    ''      '';
}

Si la petición trae Upgrade, se envía Connection: upgrade; si no, la cabecera Connection se omite, lo que permite reutilizar conexiones con el backend (paso 7). Los archivos de /etc/nginx/conf.d/ se incluyen dentro del bloque http, por eso el map va aquí y no en el sitio. Añade después al location del proxy:

        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $connection_upgrade;
        proxy_read_timeout 1h;

proxy_http_version 1.1 es necesario porque el mecanismo de Upgrade no existe en HTTP/1.0, que es lo que Nginx usa por defecto con el backend. El proxy_read_timeout largo evita que Nginx cierre conexiones WebSocket inactivas a los 60 segundos. Si solo una ruta usa WebSockets (por ejemplo /socket.io/), pon estas líneas en un location propio para esa ruta.

Valida y recarga Nginx.

Paso 7: Repartir la carga entre varias instancias

Un bloque upstream agrupa varios backends bajo un nombre y Nginx reparte las peticiones entre ellos. Para probarlo, levanta una segunda instancia del backend de prueba en el puerto 3001:

mkdir -p ~/app2
echo "Respuesta del backend 2" > ~/app2/index.html
python3 -m http.server 3001 --bind 127.0.0.1 --directory ~/app2 > /dev/null 2>&1 &

Define el grupo en el archivo del sitio, fuera de cualquier bloque server:

sudo nano /etc/nginx/sites-available/your_domain
upstream app_backend {
    server 127.0.0.1:3000;
    server 127.0.0.1:3001;
    keepalive 16;
}

Y cambia el proxy_pass del location para que apunte al grupo:

        proxy_pass http://app_backend;

keepalive 16 mantiene abiertas hasta 16 conexiones inactivas por proceso de Nginx con los backends, lo que evita abrir una conexión TCP por petición. Requiere proxy_http_version 1.1, que ya añadiste en el paso 6.

Valida, recarga y haz varias peticiones seguidas:

sudo nginx -t && sudo systemctl reload nginx
for i in 1 2 3 4; do curl -s https://your_domain; done
Respuesta del backend 1
Respuesta del backend 2
Respuesta del backend 1
Respuesta del backend 2

Por defecto Nginx usa reparto circular (round robin). Puedes cambiarlo con una directiva al inicio del bloque upstream:

DirectivaComportamiento
(ninguna)Round robin: cada petición va al siguiente servidor.
least_conn;Envía la petición al servidor con menos conexiones activas. Útil con peticiones de duración muy variable.
ip_hash;La misma IP de cliente va siempre al mismo servidor. Sirve para aplicaciones con sesiones en memoria.

También puedes ajustar cada servidor: weight=3 le da más peticiones, max_fails=3 fail_timeout=30s lo retira temporalmente tras tres fallos, y backup solo lo usa cuando los demás están caídos. Para comprobarlo, detén una instancia con kill %1 (o el identificador de trabajo que muestre jobs) y verás que todas las respuestas pasan a venir del otro backend.

Paso 8 (opcional): Cachear respuestas del backend

Si la aplicación sirve contenido que no cambia en cada petición, Nginx puede guardar las respuestas y servirlas sin consultar al backend. Define la zona de caché en el contexto http:

sudo nano /etc/nginx/conf.d/proxy-cache.conf
proxy_cache_path /var/cache/nginx/app levels=1:2 keys_zone=app_cache:10m max_size=1g inactive=60m use_temp_path=off;

keys_zone reserva 10 MB de memoria para las claves (unas 80.000 entradas), max_size limita el disco a 1 GB e inactive elimina lo que no se pide en 60 minutos. Nginx crea el directorio al arrancar.

Activa la caché en el location del proxy:

        proxy_cache app_cache;
        proxy_cache_valid 200 301 10m;
        proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
        add_header X-Cache-Status $upstream_cache_status always;

proxy_cache_use_stale sirve la copia guardada si el backend falla, lo que da cierta tolerancia a caídas. Nginx respeta las cabeceras Cache-Control y Set-Cookie del backend: no cachea respuestas marcadas como private o no-store, ni las que establecen cookies, así que las páginas personalizadas no se mezclan entre usuarios siempre que la aplicación las marque correctamente.

Valida, recarga y pide la misma URL dos veces:

sudo nginx -t && sudo systemctl reload nginx
curl -sI https://your_domain | grep X-Cache-Status
curl -sI https://your_domain | grep X-Cache-Status
X-Cache-Status: MISS
X-Cache-Status: HIT

Solución de problemas

Revisa siempre primero el log de errores de Nginx, que indica la causa exacta:

sudo tail -n 20 /var/log/nginx/error.log
  • 502 Bad Gateway con connect() failed (111: Connection refused): el backend no está escuchando en la dirección de proxy_pass. Compruébalo con sudo ss -tlnp | grep 3000 y curl http://127.0.0.1:3000.
  • 502 con upstream prematurely closed connection: la aplicación ha cerrado la conexión o se ha caído durante la petición. Revisa los logs de la aplicación.
  • 504 Gateway Timeout: el backend tarda más que proxy_read_timeout. Súbelo para esa ruta o haz que la aplicación responda antes.
  • 413 Request Entity Too Large: aumenta client_max_body_size.
  • La aplicación genera redirecciones a http:// o a 127.0.0.1:3000: no está leyendo Host y X-Forwarded-Proto. Comprueba que include proxy_params; está en el location y que el framework confía en el proxy.
  • Las conexiones WebSocket se cierran nada más abrirse: falta proxy_http_version 1.1 o las cabeceras Upgrade y Connection.
  • En Rocky Linux o AlmaLinux, 502 con (13: Permission denied): SELinux impide a Nginx abrir conexiones de red. Permítelo con sudo setsebool -P httpd_can_network_connect 1, sin desactivar SELinux.

Conclusión

Has configurado Nginx como proxy inverso delante de una aplicación local, con las cabeceras que la aplicación necesita, HTTPS, soporte para WebSockets, balanceo de carga entre varias instancias y caché de respuestas. La aplicación queda accesible solo a través de Nginx, que concentra TLS, límites y registros.

Como siguientes pasos puedes:

  • Ejecutar tu aplicación como servicio de systemd para que arranque con el servidor y se reinicie si falla.
  • Limitar el número de peticiones por IP con limit_req_zone y limit_req para proteger los endpoints sensibles.
  • Activar HTTP/2 en el bloque del puerto 443.