GuíasGuides · Nivel 3Level 3

Nginx y Apache: publicar una aplicación interna como proxy inverso, con TLS, cabeceras, timeouts y un 502 Bad GatewayNginx and Apache: publishing an internal app as a reverse proxy, with TLS, headers, timeouts and a 502 Bad Gateway

Virtual hosts y server blocks, TLS con un certificado corporativo o de Let's Encrypt, proxy_pass y ProxyPass con sus cabeceras, timeouts, logs con el tiempo del backend, nginx -t y apachectl configtest, reload o restart, y un 502 y un 504 de principio a fin, SELinux incluido.Virtual hosts and server blocks, TLS with a corporate or Let's Encrypt certificate, proxy_pass and ProxyPass with their headers, timeouts, logs with the backend's time, nginx -t and apachectl configtest, reload or restart, and a 502 and a 504 end to end, SELinux included.

Qué vas a conseguir

Publicar una aplicación interna (la de fichajes, en 127.0.0.1:8080) detrás de Nginx o Apache: con su nombre, HTTPS, las cabeceras que la aplicación necesita, timeouts pensados y logs que dicen cuánto tardó el backend. Y, cuando aparezca el 502 Bad Gateway, saber en cinco minutos si es la aplicación, SELinux o un timeout.

Antes de empezar

  • Un servidor RHEL 9 (o Ubuntu) con Nginx o Apache: uno de los dos en el 443, no ambos.
  • Dónde está cada cosa: Nginx en /etc/nginx/conf.d/*.conf y logs en /var/log/nginx/. Apache en RHEL en /etc/httpd/conf.d/ con logs en /var/log/httpd/; en Debian y Ubuntu en /etc/apache2/sites-available/, que se activa con a2ensite, y logs en /var/log/apache2/.
  • El nombre en DNS: fichajes.corp.example apuntando al servidor.
  • El certificado: de la CA corporativa (certificado, cadena intermedia y clave) para nombres internos, o de Let's Encrypt si el nombre es público.

1. Un sitio por nombre: server blocks y virtual hosts

Nginx elige el bloque server por server_name; Apache, el VirtualHost por ServerName. El puerto 80 solo redirige:

/etc/nginx/conf.d/fichajes.conf
server {    listen 80;    server_name fichajes.corp.example;    return 301 https://$host$request_uri;}server {    listen 443 ssl;    server_name fichajes.corp.example;    ssl_certificate     /etc/pki/tls/certs/fichajes-fullchain.pem;    ssl_certificate_key /etc/pki/tls/private/fichajes.key;    ssl_protocols TLSv1.2 TLSv1.3;    access_log /var/log/nginx/fichajes.access.log upstream;    error_log  /var/log/nginx/fichajes.error.log;    location / {        proxy_pass http://127.0.0.1:8080;        include /etc/nginx/proxy-headers.conf;    }}
/etc/httpd/conf.d/fichajes.conf
<VirtualHost *:80>    ServerName fichajes.corp.example    Redirect permanent / https://fichajes.corp.example/</VirtualHost><VirtualHost *:443>    ServerName fichajes.corp.example    SSLEngine on    SSLCertificateFile    /etc/pki/tls/certs/fichajes-fullchain.pem    SSLCertificateKeyFile /etc/pki/tls/private/fichajes.key    ProxyPreserveHost On    ProxyPass        / http://127.0.0.1:8080/ connectiontimeout=5 timeout=60    ProxyPassReverse / http://127.0.0.1:8080/    RequestHeader set X-Forwarded-Proto "https"    ErrorLog  /var/log/httpd/fichajes_error.log    CustomLog /var/log/httpd/fichajes_access.log proxy</VirtualHost>
  • Un nombre que no coincide con ningún sitio cae en el sitio por defecto: default_server en Nginx, el primer VirtualHost del puerto en Apache. apachectl -S los lista.
  • El formato upstream del log de acceso y el proxy de Apache se definen en el apartado 5: sin ellos, el test de configuración falla.

2. TLS: certificado corporativo o Let's Encrypt

  • El fichero del certificado lleva primero el del servidor y después los intermedios. Sin el intermedio, el navegador de IT, que lo tiene en caché, funciona; el móvil de un usuario, no.
  • En Apache 2.4.8 y posteriores, SSLCertificateFile admite la cadena completa; SSLCertificateChainFile está obsoleto.
  • La clave privada con permisos 600. En RHEL, Apache necesita mod_ssl.
  • Let's Encrypt con certbot: certbot --nginx -d fichajes.example.com (o --apache) emite, configura y deja programada la renovación. Hoy los certificados duran 90 días y Let's Encrypt ya ha anunciado que irán siendo más cortos: la renovación tiene que ser automática, nunca manual. Comprueba que funciona con certbot renew --dry-run.
Bash · qué certificado entrega el servidor
openssl s_client -connect fichajes.corp.example:443 -servername fichajes.corp.example </dev/null 2>/dev/null | openssl x509 -noout -subject -issuer -enddatesubject=CN = fichajes.corp.exampleissuer=CN = Contoso Issuing CA 01notAfter=Sep 14 23:59:59 2027 GMT

Sin -servername (SNI) verás el certificado del sitio por defecto.

3. El proxy inverso: las cabeceras

La aplicación ve todas las peticiones llegar desde 127.0.0.1. Para saber el nombre que pidió el usuario, su IP y que venía por HTTPS, el proxy se lo dice en cabeceras:

/etc/nginx/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;proxy_http_version 1.1;
  • Sin Host y X-Forwarded-Proto, la aplicación redirige a http://127.0.0.1:8080/login y el usuario acaba en una página que no carga. En Apache, ProxyPreserveHost On pasa el Host, ProxyPassReverse reescribe las redirecciones y mod_proxy añade X-Forwarded-For por su cuenta.

4. Timeouts

Qué controlaNginx (por defecto)Apache (por defecto)
Conectar con el backendproxy_connect_timeout (60 s)connectiontimeout= en ProxyPass
Esperar la respuestaproxy_read_timeout (60 s)timeout= en ProxyPass, o ProxyTimeout; si no, Timeout (60 s)
Tamaño máximo de una subidaclient_max_body_size (1 MB)LimitRequestBody
  • La conexión, corta (5 segundos): si el backend no acepta en ese tiempo, no lo va a hacer. La lectura, la que necesite la página más lenta de verdad, y solo en la location de esa página, no en todo el sitio.
  • Un fichero de más de 1 MB en Nginx devuelve 413 antes de llegar a la aplicación.

5. Logs que sirven

El log de acceso por defecto no dice cuánto tardó el backend. Añádelo en /etc/nginx/nginx.conf, dentro de http:

nginx.conf · formato con el tiempo del backend
log_format upstream '$remote_addr [$time_local] "$request" $status $body_bytes_sent '                    'rt=$request_time uct=$upstream_connect_time urt=$upstream_response_time '                    'us=$upstream_status ua=$upstream_addr';
  • rt es el tiempo total y urt el del backend: si rt es alto y urt bajo, el lento es el cliente o la red, no la aplicación. us=- indica que la petición ni siquiera llegó al backend.
  • En Apache, %D (microsegundos) da el tiempo total: LogFormat "%h %t \"%r\" %>s %b %D" proxy.
  • El log de errores (error_log, ErrorLog) es donde aparece la causa de un 502. Es lo primero que se abre.

6. Probar y aplicar: configtest, reload o restart

Bash · antes de aplicar un cambio
sudo nginx -tnginx: the configuration file /etc/nginx/nginx.conf syntax is oknginx: configuration file /etc/nginx/nginx.conf test is successfulsudo systemctl reload nginxsudo apachectl configtestSyntax OKsudo systemctl reload httpd
  • Siempre el test antes. nginx -T vuelca la configuración completa con todos los include.
  • reload relee la configuración sin cortar: Nginx arranca procesos nuevos y deja terminar a los viejos; en Apache es un reinicio graceful. Si la configuración nueva tiene un error, Nginx sigue con la anterior.
  • restart para y arranca: corta las conexiones abiertas. Solo cuando el proceso no responde o tras una actualización del paquete.

7. El ticket: "la aplicación de fichajes da 502 Bad Gateway"

Lunes, 9:02. Toda la plantilla ficha a la vez y la página devuelve 502 Bad Gateway. Un 502 lo genera el proxy: Nginx funciona, lo que falla es la conexión con el backend. El log de errores dice cuál de los casos es:

Bash · fichajes.error.log
sudo tail -n 3 /var/log/nginx/fichajes.error.log2026/10/26 09:02:11 [error] 2211#2211: *8812 connect() failed (111: Connection refused) while connecting to upstream, client: 10.20.4.37, server: fichajes.corp.example, request: "GET / HTTP/1.1", upstream: "http://127.0.0.1:8080/"ss -ltnp | grep 8080systemctl status fichajes --no-pagerActive: failed (Result: oom-kill) since Mon 2026-10-26 08:58:40 CET
  1. 111: Connection refused: nadie escucha en el 8080. ss no devuelve nada y el servicio de la aplicación ha muerto por falta de memoria. Se arranca, se confirma que responde (curl -I http://127.0.0.1:8080/) y se abre un ticket con el equipo de la aplicación con la hora del oom-kill.
  2. 13: Permission denied while connecting to upstream, en un RHEL recién montado y con el backend escuchando: es SELinux. ausearch -m avc -ts recent muestra el denied { name_connect } de nginx (o httpd). La solución es setsebool -P httpd_can_network_connect 1, no setenforce 0. En Apache aparece como (13)Permission denied: AH00957.
  3. 504 Gateway Time-out, solo en el informe mensual: upstream timed out (110: Connection timed out) while reading response header from upstream y urt=60.001. El backend tarda más que proxy_read_timeout: se sube a 300 s solo en location /informes/ y se pide que el informe sea asíncrono. En Apache es AH01102: error reading status line from remote server.

Antes de tocar el proxy, curl -I http://127.0.0.1:8080/ desde el propio servidor. Si el backend no responde ahí, el proxy no tiene nada que arreglar.

Siguientes pasos

  • La guía de Red Hat Enterprise Linux del Nivel 2 explica los servicios, el cortafuegos y SELinux que aparecen en el ticket.
  • La de Docker cubre las aplicaciones en contenedores que suelen estar detrás de este proxy.
  • La de IIS es la versión Windows de todo esto: sitios, enlaces, certificados y el 503.
  • Vigilar desde fuera que el certificado no caduca y que el sitio responde es lo que hacen las sondas TLS y HTTP del repositorio 08-edge-health-slo-monitor de mi GitHub.

¿Falta algo o en tu entorno es distinto? Escríbeme y lo corrijo.

What you'll get

Publishing an internal app (the timesheets one, on 127.0.0.1:8080) behind Nginx or Apache: with its own name, HTTPS, the headers the app needs, sensible timeouts and logs that say how long the backend took. And, when the 502 Bad Gateway shows up, knowing within five minutes whether it is the app, SELinux or a timeout.

Before you start

  • A RHEL 9 (or Ubuntu) server with Nginx or Apache: one of them on 443, not both.
  • Where everything lives: Nginx in /etc/nginx/conf.d/*.conf with logs in /var/log/nginx/. Apache on RHEL in /etc/httpd/conf.d/ with logs in /var/log/httpd/; on Debian and Ubuntu in /etc/apache2/sites-available/, enabled with a2ensite, and logs in /var/log/apache2/.
  • The DNS name: fichajes.corp.example pointing at the server.
  • The certificate: from the corporate CA (certificate, intermediate chain and key) for internal names, or from Let's Encrypt if the name is public.

1. One site per name: server blocks and virtual hosts

Nginx picks the server block by server_name; Apache picks the VirtualHost by ServerName. Port 80 only redirects:

/etc/nginx/conf.d/fichajes.conf
server {    listen 80;    server_name fichajes.corp.example;    return 301 https://$host$request_uri;}server {    listen 443 ssl;    server_name fichajes.corp.example;    ssl_certificate     /etc/pki/tls/certs/fichajes-fullchain.pem;    ssl_certificate_key /etc/pki/tls/private/fichajes.key;    ssl_protocols TLSv1.2 TLSv1.3;    access_log /var/log/nginx/fichajes.access.log upstream;    error_log  /var/log/nginx/fichajes.error.log;    location / {        proxy_pass http://127.0.0.1:8080;        include /etc/nginx/proxy-headers.conf;    }}
/etc/httpd/conf.d/fichajes.conf
<VirtualHost *:80>    ServerName fichajes.corp.example    Redirect permanent / https://fichajes.corp.example/</VirtualHost><VirtualHost *:443>    ServerName fichajes.corp.example    SSLEngine on    SSLCertificateFile    /etc/pki/tls/certs/fichajes-fullchain.pem    SSLCertificateKeyFile /etc/pki/tls/private/fichajes.key    ProxyPreserveHost On    ProxyPass        / http://127.0.0.1:8080/ connectiontimeout=5 timeout=60    ProxyPassReverse / http://127.0.0.1:8080/    RequestHeader set X-Forwarded-Proto "https"    ErrorLog  /var/log/httpd/fichajes_error.log    CustomLog /var/log/httpd/fichajes_access.log proxy</VirtualHost>
  • A name that matches no site lands on the default site: default_server in Nginx, the first VirtualHost on the port in Apache. apachectl -S lists them.
  • The upstream access log format and Apache's proxy format are defined in section 5: without them, the configuration test fails.

2. TLS: corporate certificate or Let's Encrypt

  • The certificate file holds the server's certificate first and then the intermediates. Without the intermediate, IT's browser, which has it cached, works; a user's phone does not.
  • In Apache 2.4.8 and later, SSLCertificateFile accepts the full chain; SSLCertificateChainFile is obsolete.
  • The private key with 600 permissions. On RHEL, Apache needs mod_ssl.
  • Let's Encrypt with certbot: certbot --nginx -d fichajes.example.com (or --apache) issues, configures and schedules renewal. Today certificates last 90 days and Let's Encrypt has already announced they will get shorter: renewal must be automatic, never manual. Check it works with certbot renew --dry-run.
Bash · which certificate the server presents
openssl s_client -connect fichajes.corp.example:443 -servername fichajes.corp.example </dev/null 2>/dev/null | openssl x509 -noout -subject -issuer -enddatesubject=CN = fichajes.corp.exampleissuer=CN = Contoso Issuing CA 01notAfter=Sep 14 23:59:59 2027 GMT

Without -servername (SNI) you see the default site's certificate.

3. The reverse proxy: headers

The app sees every request arriving from 127.0.0.1. To know the name the user asked for, their IP and that they came in over HTTPS, the proxy tells it in headers:

/etc/nginx/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;proxy_http_version 1.1;
  • Without Host and X-Forwarded-Proto, the app redirects to http://127.0.0.1:8080/login and the user ends up on a page that does not load. In Apache, ProxyPreserveHost On passes the Host, ProxyPassReverse rewrites redirects and mod_proxy adds X-Forwarded-For on its own.

4. Timeouts

What it controlsNginx (default)Apache (default)
Connecting to the backendproxy_connect_timeout (60 s)connectiontimeout= on ProxyPass
Waiting for the responseproxy_read_timeout (60 s)timeout= on ProxyPass, or ProxyTimeout; otherwise Timeout (60 s)
Maximum upload sizeclient_max_body_size (1 MB)LimitRequestBody
  • Connecting, short (5 seconds): if the backend does not accept in that time, it is not going to. Reading, whatever the slowest page really needs, and only in that page's location, not the whole site.
  • A file over 1 MB in Nginx returns 413 before it reaches the app.

5. Logs that help

The default access log does not say how long the backend took. Add it in /etc/nginx/nginx.conf, inside http:

nginx.conf · format with the backend's time
log_format upstream '$remote_addr [$time_local] "$request" $status $body_bytes_sent '                    'rt=$request_time uct=$upstream_connect_time urt=$upstream_response_time '                    'us=$upstream_status ua=$upstream_addr';
  • rt is the total time and urt the backend's: if rt is high and urt low, the slow part is the client or the network, not the app. us=- means the request never even reached the backend.
  • In Apache, %D (microseconds) gives the total time: LogFormat "%h %t \"%r\" %>s %b %D" proxy.
  • The error log (error_log, ErrorLog) is where the cause of a 502 shows up. It is the first thing you open.

6. Test and apply: configtest, reload or restart

Bash · before applying a change
sudo nginx -tnginx: the configuration file /etc/nginx/nginx.conf syntax is oknginx: configuration file /etc/nginx/nginx.conf test is successfulsudo systemctl reload nginxsudo apachectl configtestSyntax OKsudo systemctl reload httpd
  • Always the test first. nginx -T dumps the full configuration with every include.
  • reload rereads the configuration without cutting anyone off: Nginx starts new processes and lets the old ones finish; in Apache it is a graceful restart. If the new configuration has an error, Nginx carries on with the old one.
  • restart stops and starts: it drops open connections. Only when the process does not respond or after a package update.

7. The ticket: "the timesheets app gives 502 Bad Gateway"

Monday, 9:02. The whole workforce clocks in at once and the page returns 502 Bad Gateway. A 502 is generated by the proxy: Nginx is working, what fails is the connection to the backend. The error log says which case it is:

Bash · fichajes.error.log
sudo tail -n 3 /var/log/nginx/fichajes.error.log2026/10/26 09:02:11 [error] 2211#2211: *8812 connect() failed (111: Connection refused) while connecting to upstream, client: 10.20.4.37, server: fichajes.corp.example, request: "GET / HTTP/1.1", upstream: "http://127.0.0.1:8080/"ss -ltnp | grep 8080systemctl status fichajes --no-pagerActive: failed (Result: oom-kill) since Mon 2026-10-26 08:58:40 CET
  1. 111: Connection refused: nothing is listening on 8080. ss returns nothing and the app's service has died for lack of memory. It is started, you confirm it responds (curl -I http://127.0.0.1:8080/) and a ticket goes to the app team with the time of the oom-kill.
  2. 13: Permission denied while connecting to upstream, on a freshly built RHEL with the backend listening: it is SELinux. ausearch -m avc -ts recent shows the denied { name_connect } for nginx (or httpd). The fix is setsebool -P httpd_can_network_connect 1, not setenforce 0. In Apache it shows up as (13)Permission denied: AH00957.
  3. 504 Gateway Time-out, only on the monthly report: upstream timed out (110: Connection timed out) while reading response header from upstream and urt=60.001. The backend takes longer than proxy_read_timeout: it is raised to 300 s only in location /informes/ and the report is asked to become asynchronous. In Apache it is AH01102: error reading status line from remote server.

Before touching the proxy, curl -I http://127.0.0.1:8080/ from the server itself. If the backend does not answer there, the proxy has nothing to fix.

Next steps

  • The Level 2 Red Hat Enterprise Linux guide explains the services, firewall and SELinux that appear in the ticket.
  • The Docker guide covers the containerised apps that often sit behind this proxy.
  • The IIS guide is the Windows version of all this: sites, bindings, certificates and the 503.
  • Checking from outside that the certificate is not about to expire and that the site responds is what the TLS and HTTP probes in the 08-edge-health-slo-monitor repository on my GitHub do.

Is something missing, or different in your environment? Get in touch and I'll fix it.

Tech Career Lab · newsletter

¿Te ha servido? Cada dos semanas envío una idea práctica para perfiles IT: LinkedIn, proyectos, networking y procesos de selección.Found this useful? Every two weeks I send one practical idea for IT professionals: LinkedIn, projects, networking and hiring processes.

Apúntate y llévate las 33 ideasSign up and get the 33 ideas →