SSD Nodes Learn 🎉 VPS desde $5.50/mes
Guías Matt ConnorPor Matt Connor

Configuración de proxy inverso nginx, línea por línea

Configura nginx como proxy inverso en Ubuntu 24.04: usa proxy_pass, cuatro cabeceras, WebSocket y revisa las barras finales y las subidas.

Qué hace una configuración de proxy inverso de nginx

Un proxy inverso de nginx recibe las peticiones que llegan por los puertos 80 y 443 y entrega cada una a una aplicación que ya escucha en un puerto local. Después devuelve al navegador la respuesta de esa aplicación. La configuración es un único bloque server y el bloque es corto. Casi toda la dificultad se concentra en cinco o seis líneas que indican a la aplicación quién era el cliente real y qué protocolo utilizó ese cliente.

Todo lo que sigue se construye desde cero en Ubuntu 24.04, con el paquete nginx de la distribución. El punto de partida es una aplicación que ya responde en 127.0.0.1:3000. Si todavía no ha elegido un proxy, la comparación entre nginx, Caddy y Traefik es lo primero que debe leer. A continuación se muestra la configuración de nginx, línea por línea.

Ejecute estas configuraciones en su propio servidor. Pruebe cada cambio con sudo nginx -t antes de recargar nginx y revise lo que muestra.

Dónde guarda nginx su configuración en Ubuntu

sudo apt update
sudo apt install -y nginx
ls -l /etc/nginx/sites-enabled/

El archivo principal es /etc/nginx/nginx.conf. Define las opciones globales dentro de un bloque http { } y después incluye dos directorios: /etc/nginx/conf.d/*.conf y /etc/nginx/sites-enabled/*. En Ubuntu y Debian se crea un archivo por sitio en /etc/nginx/sites-available/ y se habilita mediante un enlace simbólico en /etc/nginx/sites-enabled/. Al eliminar el enlace simbólico se deshabilita el sitio, pero se conserva el archivo.

Dos directivas que se usan más adelante sólo funcionan en el contexto http, nunca dentro de un bloque server: map y upstream. Colóquelas en un archivo independiente bajo /etc/nginx/conf.d/, porque ese directorio se incluye en el nivel http.

El paquete incluye un sitio habilitado llamado default. Está marcado como default_server, lo que significa que responde a cualquier solicitud cuyo encabezado Host no coincida con ningún server_name de la configuración. Mientras siga habilitado, una solicitud que no coincida con sus nombres llegará a ese sitio en lugar de a su aplicación. Elimine el enlace simbólico cuando su propio sitio funcione.

sudo rm /etc/nginx/sites-enabled/default
sudo nginx -t
sudo systemctl reload nginx

El bloque de servidor más pequeño que hace proxy para una aplicación

server {
    listen 80;
    listen [::]:80;
    server_name app.example.com;

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

Guárdelo como /etc/nginx/sites-available/app.example.com, habilítelo y cárguelo.

sudo ln -s /etc/nginx/sites-available/app.example.com /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
curl -sI -H 'Host: app.example.com' http://127.0.0.1/

listen 80; enlaza IPv4 y listen [::]:80; enlaza IPv6. Si omite la segunda línea, un visitante cuya consulta de DNS (sistema de nombres de dominio) devuelva un registro AAAA para su servidor recibirá un rechazo de conexión, mientras que todos los usuarios de IPv4 verán un sitio operativo. El informe de errores que recibirá dirá: «a mí me funciona».

server_name se compara con la cabecera Host que envía el navegador. Puede indicar varios nombres separados por espacios. Si ningún bloque coincide, nginx usa el bloque que sea default_server. Por eso había que eliminar el sitio incluido en el paquete.

location / es una coincidencia por prefijo sobre la ruta de la petición, y / coincide con cualquier ruta. proxy_pass es la dirección a la que nginx abre una conexión. Mantenga la aplicación enlazada a 127.0.0.1 para que la única vía de acceso sea nginx. Si la aplicación se ejecuta en un contenedor, publíquela como 127.0.0.1:3000:3000 y no como 3000:3000, porque Docker escribe sus propias reglas y publica los puertos directamente fuera de ufw, de modo que un puerto publicado sin más queda accesible desde Internet independientemente de lo que indique el firewall.

La línea curl envía la cabecera Host correcta desde el propio servidor, por lo que puede probar el bloque antes de que DNS apunte a él.

Qué envía nginx al upstream cuando no se configura nada más

proxy_pass por sí solo oculta cuatro aspectos a la aplicación.

De forma predeterminada, nginx usa HTTP/1.0 con el backend y envía Connection: close. Por tanto, cada petición abre una conexión upstream nueva y no es posible actualizar el protocolo.

La cabecera Host se reescribe con el valor de proxy_pass, que es 127.0.0.1:3000. Una aplicación que crea enlaces absolutos a partir de Host genera enlaces que nadie fuera del servidor puede abrir.

La conexión que llega a la aplicación procede de nginx, por lo que la aplicación ve la dirección del cliente como 127.0.0.1. Cada línea del registro y cada límite de tasa dentro de la aplicación registra el proxy en lugar del visitante.

La aplicación no puede saber que el navegador usó HTTPS, porque la conexión que recibe es HTTP sin cifrar en una dirección de loopback.

Cuatro líneas solucionan todo eso.

Los cuatro encabezados que debe configurar y lo que cada uno permite ver al backend

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

    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;
}

Host transporta el nombre que escribió el visitante. $host es el nombre de la solicitud, sin el puerto y con las letras en minúscula. Configúrelo para que la aplicación genere URL absolutas correctas, como la redirección posterior al inicio de sesión o el enlace de un correo electrónico para restablecer la contraseña. Si lo omite, esas URL apuntan a 127.0.0.1:3000 y, al iniciar sesión, el navegador se dirige a una dirección que rechaza la conexión. Si la aplicación también necesita el puerto porque se sirve en 8080, use $http_host, que es el encabezado exactamente como lo envió el cliente.

X-Real-IP transporta un valor: $remote_addr, la dirección desde la que nginx aceptó la conexión. Las aplicaciones lo leen para sus propios registros de acceso y para sus propios límites de tasa.

X-Forwarded-For transporta una lista. $proxy_add_x_forwarded_for añade $remote_addr a lo que el cliente ya incluyó en ese encabezado, por lo que el valor queda separado por comas y la entrada añadida por nginx es la última. Este detalle determina si se puede confiar en el encabezado: un cliente puede enviar cualquier X-Forwarded-For, por lo que una aplicación que lea la primera entrada puede recibir cualquier dirección. Cuando nginx es el servidor perimetral, escriba $remote_addr en su lugar y descarte la versión del cliente. Cuando haya una CDN u otro proxy delante, use set_real_ip_from y real_ip_header del módulo realip, de modo que $remote_addr pase a ser la dirección real del cliente.

X-Forwarded-Proto transporta http o https. Los frameworks lo leen para decidir si deben marcar las cookies como Secure y si deben forzar una redirección a HTTPS. Si lo omite en un sitio TLS y la aplicación está configurada para forzar HTTPS, ve http, responde con una redirección a la dirección HTTPS, recibe la siguiente solicitud a través de nginx, sigue viendo http y vuelve a redirigir. El navegador abandona el proceso y muestra ERR_TOO_MANY_REDIRECTS.

Repetir esas cuatro líneas en cada location hace que terminen desincronizadas. Colóquelas en un solo archivo e inclúyalo.

# /etc/nginx/snippets/proxy-headers.conf
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 / {
    include snippets/proxy-headers.conf;
    proxy_pass http://127.0.0.1:3000;
}

La herencia tiene una trampa. Una location hereda las directivas proxy_set_header de su bloque server sólo mientras no defina ninguna propia. Si añade un proxy_set_header dentro de la location, se descartan todos los encabezados definidos en el nivel server para esa location. Por tanto, manténgalos todos en un único nivel o include el fragmento en cada location que use proxy.

¿Por qué mi aplicación WebSocket se conecta y luego se desconecta?

Porque los valores predeterminados impiden la actualización y el tiempo de espera de lectura predeterminado cierra un túnel inactivo después de 60 segundos. Un WebSocket comienza como una solicitud HTTP que contiene Upgrade: websocket y Connection: Upgrade. Son cabeceras salto a salto, lo que significa que se espera que un proxy las consuma en lugar de reenviarlas, y HTTP/1.0 no tiene ningún mecanismo de actualización. Hay que volver a añadir ambas manualmente.

El map debe estar en el contexto http, en su propio archivo.

# /etc/nginx/conf.d/websocket.conf
map $http_upgrade $connection_upgrade {
    default upgrade;
    ''      close;
}

Después, la location.

location / {
    include snippets/proxy-headers.conf;
    proxy_pass http://127.0.0.1:3000;

    proxy_http_version 1.1;
    proxy_set_header Upgrade    $http_upgrade;
    proxy_set_header Connection $connection_upgrade;

    proxy_read_timeout 3600s;
    proxy_send_timeout 3600s;
}

El map permite que una sola location atienda ambos tipos de tráfico. En una solicitud normal, $http_upgrade está vacío, por lo que $connection_upgrade se convierte en close. En una solicitud de actualización contiene websocket, por lo que la cabecera enviada al upstream es Connection: upgrade. Fijar proxy_set_header Connection "upgrade"; envía esa cabecera también en todas las solicitudes de páginas normales, y algunos backends responden a esas solicitudes con un error 400.

proxy_read_timeout es lo que provoca los informes de «carga y luego deja de actualizarse». Su valor predeterminado es 60 segundos y mide el intervalo entre dos lecturas del backend, no la duración de la conexión. Un WebSocket que permanece inactivo durante 60 segundos es cerrado por nginx, y la consola del navegador muestra que el socket se cierra con el código 1006. Las aplicaciones que envían su propio heartbeat con una frecuencia inferior a un minuto nunca lo detectan. Las que no lo hacen, se cierran al cumplirse el minuto. Esto aparece primero en editores en tiempo real y dashboards; una instancia de n8n autohospedada detrás de HTTPS es un ejemplo habitual.

¿Por qué una barra final en proxy_pass cambia mis URL?

La regla se resume en una frase. Si proxy_pass termina con una URI (identificador uniforme de recursos), incluso con una / independiente, nginx elimina la parte de la ruta de la petición que coincide con el prefijo location y coloca esa URI en su lugar. Si proxy_pass termina en el host y el puerto, la ruta de la petición se reenvía sin cambios.

location /app/ {
    proxy_pass http://127.0.0.1:3000/;
}

Una petición a /app/status llega al backend como /status.

location /app/ {
    proxy_pass http://127.0.0.1:3000;
}

Una petición a /app/status llega al backend como /app/status.

La forma adecuada depende de la aplicación. Una aplicación con una configuración de ruta base o subdirectorio necesita la segunda forma, y esa configuración debe conocer /app. Una aplicación que no admite prefijos necesita la primera. La primera forma tiene un problema inmediato: el HTML que devuelve la aplicación todavía contiene rutas absolutas como /static/main.css, el navegador las solicita en la raíz del sitio, ninguna ubicación coincide y la página se muestra sin estilos. La pestaña de red del navegador muestra que esas peticiones de recursos devuelven 404. La solución es configurar la ruta base propia de la aplicación o añadir un segundo location /static/ que apunte al mismo backend.

Una ubicación con expresión regular no puede incluir una URI en proxy_pass. sudo nginx -t rechaza la configuración e indica el motivo: "proxy_pass" cannot have URI part in location given by regular expression, or inside named location, or inside "if" statement, or inside "limit_except" block.

Esta clase de problemas desaparece cuando cada aplicación tiene su propio nombre, app.example.com, y se configura el proxy desde location /. Las subrutas sólo compensan cuando no se pueden añadir registros DNS.

¿Cómo coloco más de un backend detrás de un mismo nombre?

Con un bloque upstream. Pertenece al contexto http, así que debe escribirlo encima del bloque server en el mismo archivo o en /etc/nginx/conf.d/.

upstream app_backend {
    least_conn;
    server 127.0.0.1:3000 max_fails=3 fail_timeout=30s;
    server 127.0.0.1:3001 max_fails=3 fail_timeout=30s;
    keepalive 32;
}

Después, la ubicación lo referencia: proxy_pass http://app_backend;.

El método predeterminado es round robin. least_conn envía cada solicitud al backend que tiene menos conexiones activas, lo que resulta adecuado para solicitudes de duración desigual. ip_hash fija la dirección de un cliente a un backend. Necesita ip_hash cuando la aplicación mantiene las sesiones en su propia memoria, porque el round robin entre dos backends de este tipo cierra las sesiones de forma aleatoria cuando las solicitudes llegan a la instancia que nunca las recibió. La mejor solución es mover las sesiones a un almacenamiento compartido.

max_fails=3 fail_timeout=30s significa que tres intentos fallidos en 30 segundos retiran ese servidor durante 30 segundos. Cuando todos los servidores del bloque se encuentran en ese estado, los clientes reciben 502 y el registro de errores indica no live upstreams while connecting to upstream.

keepalive 32 mantiene abiertas hasta 32 conexiones inactivas a los backends por cada proceso worker, lo que elimina el handshake TCP de la mayoría de las solicitudes. Sólo funciona con proxy_http_version 1.1 y cuando no se envía ningún Connection: close al upstream. Si la misma ubicación también usa el mapa de WebSocket, cambie el caso vacío de close a una cadena vacía, para que las solicitudes normales no incluyan la cabecera Connection y se reutilice la conexión agrupada.

map $http_upgrade $connection_upgrade {
    default upgrade;
    ''      '';
}

Los nombres dentro de un bloque upstream se resuelven cuando nginx se inicia. Si el backend es un contenedor que recibe una dirección nueva al reiniciarse, nginx seguirá usando la dirección anterior hasta que lo recargue. Dentro de una red Docker, puede trasladar la resolución al momento de la solicitud mediante el resolver integrado.

resolver 127.0.0.11 valid=10s;
set $backend http://app:3000;
proxy_pass $backend;

Cuando los contenedores aparecen y desaparecen con suficiente frecuencia como para que tenga que editar nginx continuamente, un proxy que lea las etiquetas de los contenedores es una herramienta más adecuada. Traefik delante de varias aplicaciones de Docker Compose crea sus rutas a partir de los propios contenedores.

¿Por qué fallan las cargas con 413 Request Entity Too Large?

client_max_body_size tiene un valor predeterminado de 1 megabyte. nginx rechaza un cuerpo de petición más grande antes de que la aplicación reciba ningún dato, y el registro de errores registra client intended to send too large body. Aumente este valor en el bloque server o en el bloque location donde se realizan las cargas.

client_max_body_size 512m;

Un valor de 0 desactiva completamente esta comprobación. La aplicación también tiene su propio límite. Por tanto, si el error 413 persiste después de este cambio, procede del backend. El siguiente lugar que debe revisar es la configuración de carga de la aplicación.

De forma predeterminada, nginx lee todo el cuerpo de la petición antes de abrir la conexión con el upstream. Primero escribe los datos grandes en un archivo temporal del disco. Esto protege la aplicación frente a clientes lentos, porque el backend recibe la carga a la velocidad local completa. Para cargas muy grandes, puede transmitir los datos directamente.

proxy_request_buffering off;

El backend recibe entonces el cuerpo a medida que llega y debe poder procesarlo de ese modo. nginx también pierde la capacidad de reintentar la petición contra otro upstream, porque el cuerpo ya no está disponible.

client_body_timeout, cuyo valor predeterminado es de 60 segundos, se aplica entre dos lecturas consecutivas del cuerpo, no a la carga completa. Una carga lenta pero constante no supera este límite. Una carga detenida se descarta.

Búferes de respuesta y el ajuste que interrumpe la salida en tiempo real

proxy_buffering está habilitado de forma predeterminada y normalmente es lo que necesita. nginx lee la respuesta de su aplicación tan rápido como la aplicación puede escribirla, la almacena y la entrega a un cliente lento al ritmo de ese cliente. El worker de la aplicación termina antes en lugar de permanecer ocupado durante toda la descarga lenta.

Esto interrumpe las respuestas en streaming. Los eventos enviados por el servidor y la salida de registros en tiempo real no muestran nada al lector hasta que se llena un búfer. Deshabilite el almacenamiento en búfer sólo en esa ubicación.

proxy_buffering off;

Si controla la aplicación, la mejor opción es enviar la cabecera X-Accel-Buffering: no únicamente en las respuestas en streaming. nginx lee esa cabecera en cada respuesta y deshabilita el almacenamiento en búfer sólo para ella. Así, las páginas normales mantienen sus ventajas.

Cuando el registro de errores muestra upstream sent too big header while reading response header from upstream, las cabeceras de respuesta no cabían en un solo búfer. proxy_buffer_size tiene como valor predeterminado una sola página de memoria, de 4 u 8 kilobytes según la plataforma, y las cookies largas o las cabeceras de autenticación grandes pueden desbordarla. Aumente ambos valores.

proxy_buffer_size 16k;
proxy_buffers 8 16k;

¿Dónde debe configurarse TLS?

En Nginx, delante de todo lo anterior. TLS (seguridad de la capa de transporte) termina en el proxy, y la conexión de Nginx con la aplicación sigue siendo HTTP sin cifrar a través de la dirección de loopback, donde ningún otro sistema de la red puede leerla. La aplicación sabe que el visitante usó HTTPS mediante X-Forwarded-Proto, el cuarto de los cuatro encabezados.

No escriba manualmente las rutas de los certificados. Apunte el registro DNS al servidor, abra el firewall y deje que Certbot edite este mismo bloque de servidor: añadirá la línea listen 443 ssl con las rutas ssl_certificate, además de una redirección desde el puerto 80. Emitir un certificado de Let's Encrypt para Nginx con Certbot explica la emisión y el temporizador de renovación.

sudo ufw allow 'Nginx Full'
sudo ufw status

Nginx Full es un perfil de aplicación que instala el paquete de Nginx y que abre conjuntamente los puertos 80 y 443. El puerto 80 debe permanecer abierto para el desafío de renovación HTTP-01, incluso después de redirigir a HTTPS a todos los visitantes.

Pruebe la configuración y después recárguela

sudo nginx -t
sudo systemctl reload nginx

nginx -t analiza todos los archivos incluidos e informa si la prueba se ha realizado correctamente o muestra el archivo y la línea donde se detuvo. Lea esa salida antes de recargar. Una recarga con una configuración incorrecta no se aplica: nginx sigue sirviendo la configuración anterior, por lo que el sitio permanece disponible mientras el cambio no tiene ningún efecto. systemctl restart funciona de otra forma y es más arriesgado, porque un reinicio detiene primero el servidor en ejecución. Si la configuración contiene un error, nginx queda detenido. Use la recarga de forma predeterminada y reserve el reinicio para los cambios que realmente lo requieran.

sudo tail -f /var/log/nginx/error.log
sudo ss -lntp | grep -E ':(80|443|3000)'

La línea ss muestra qué proceso mantiene cada puerto, por lo que puede confirmar que la aplicación realmente está escuchando donde indica proxy_pass.

Los fallos que realmente encontrará

502 Bad Gateway, con connect() failed (111: Connection refused) while connecting to upstream en el registro de errores. No hay ningún proceso escuchando en la dirección indicada en proxy_pass. La aplicación está detenida, escucha en otro puerto o está vinculada a una dirección interna del contenedor que el host no puede alcanzar.

502 con no live upstreams while connecting to upstream. Todos los servidores del bloque upstream están marcados como fallidos por max_fails. Repare los backends. nginx volverá a intentarlo cuando expire fail_timeout.

504 Gateway Time-out, con upstream timed out (110: Connection timed out) while reading response header from upstream. El backend aceptó la conexión y después no envió nada durante proxy_read_timeout segundos. Aumentar el tiempo de espera es correcto para un informe que realmente tarda en generarse, pero no para una aplicación bloqueada.

Todas las rutas devuelven 404 desde la aplicación. La regla de la barra final reescribió la ruta. Compare la ruta que registra la aplicación con la ruta solicitada.

Responde otro sitio. server_name no coincide con la cabecera Host, por lo que la solicitud terminó en el bloque default_server.

La página carga, pero la interfaz se bloquea al cabo de aproximadamente un minuto. Es el caso de WebSocket: falta la configuración de Upgrade o proxy_read_timeout sigue establecido en 60 segundos.

FAQ

¿Por qué nginx devuelve 502 Bad Gateway después de añadir proxy_pass?

nginx no pudo abrir una conexión con la dirección de proxy_pass. El registro de errores en /var/log/nginx/error.log indica la causa: connect() failed (111: Connection refused) while connecting to upstream significa que no hay ningún proceso escuchando allí, y no live upstreams significa que todos los servidores de un bloque upstream se han marcado como fallidos. Ejecute sudo ss -lntp | grep 3000 para ver qué proceso ocupa el puerto y en qué dirección está enlazado. Una aplicación enlazada a una dirección interna del contenedor, o a un puerto distinto del que escribió, produce este error en todos los casos.

¿Por qué mi aplicación se desconecta después de aproximadamente un minuto detrás de nginx?

La conexión es un WebSocket y proxy_read_timeout mantiene su valor predeterminado de 60 segundos. Este valor mide el intervalo entre dos lecturas del backend. nginx cierra un socket inactivo y la consola del navegador muestra el código de cierre 1006. Configure proxy_http_version 1.1, transmita Upgrade y Connection mediante un map en $http_upgrade, y aumente proxy_read_timeout a un valor como 3600s. Sin la cabecera Upgrade, la actualización nunca se produce, por lo que la aplicación vuelve a usar polling o no muestra actualizaciones en tiempo real.

¿Importa la barra final de proxy_pass?

Sí. Cambia la ruta que recibe el backend. Con location /app/ y proxy_pass http://127.0.0.1:3000/, una petición a /app/status llega al backend como /status, porque cualquier URI posterior al host y al puerto sustituye el prefijo de la ubicación coincidente. Si elimina esa barra final, la misma petición llega como /app/status. Eliminar el prefijo suele romper los enlaces a los recursos de la propia aplicación. Estos enlaces siguen siendo absolutos y después devuelven 404 en la raíz del sitio. Por tanto, una aplicación con una configuración de ruta base funciona mejor con el formato que conserva la ruta.

¿Por qué mi aplicación registra 127.0.0.1 como la dirección IP de todos los visitantes?

Porque la conexión que recibe la aplicación procede realmente de nginx en la dirección de loopback. La dirección del visitante sólo llega a la aplicación mediante una cabecera que usted configure: proxy_set_header X-Real-IP $remote_addr; para un valor único y proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; para la cadena concatenada. Después, la aplicación debe configurarse para confiar en esas cabeceras. Recuerde que un cliente puede enviar su propio X-Forwarded-For. Por eso, cuando nginx es el servidor perimetral, sobrescríbalo con $remote_addr en lugar de añadirlo.

¿Necesito TLS en la conexión entre nginx y mi aplicación?

No si la aplicación se ejecuta en el mismo servidor y está enlazada a 127.0.0.1, porque ese tráfico no sale de la máquina. Termine TLS en nginx, mantenga proxy_pass sobre HTTP sin cifrar mediante loopback y envíe X-Forwarded-Proto $scheme para que la aplicación sepa que el visitante usó HTTPS. Si el backend está en otro host, a través de una red que no controla, ese salto necesita su propia protección. Puede usar HTTPS hacia el backend o un túnel privado entre las dos máquinas.