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/*.confy 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 cona2ensite, y logs en/var/log/apache2/. - El nombre en DNS:
fichajes.corp.exampleapuntando 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:
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; }}<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_serveren Nginx, el primerVirtualHostdel puerto en Apache.apachectl -Slos lista. - El formato
upstreamdel log de acceso y elproxyde 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,
SSLCertificateFileadmite la cadena completa;SSLCertificateChainFileestá obsoleto. - La clave privada con permisos
600. En RHEL, Apache necesitamod_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 concertbot renew --dry-run.
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 GMTSin -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:
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
HostyX-Forwarded-Proto, la aplicación redirige ahttp://127.0.0.1:8080/loginy el usuario acaba en una página que no carga. En Apache,ProxyPreserveHost Onpasa elHost,ProxyPassReversereescribe las redirecciones ymod_proxyañadeX-Forwarded-Forpor su cuenta.
4. Timeouts
| Qué controla | Nginx (por defecto) | Apache (por defecto) |
|---|---|---|
| Conectar con el backend | proxy_connect_timeout (60 s) | connectiontimeout= en ProxyPass |
| Esperar la respuesta | proxy_read_timeout (60 s) | timeout= en ProxyPass, o ProxyTimeout; si no, Timeout (60 s) |
| Tamaño máximo de una subida | client_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
locationde 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:
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';rtes el tiempo total yurtel del backend: sirtes alto yurtbajo, 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
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 -Tvuelca la configuración completa con todos losinclude. - 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:
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 CET111: Connection refused: nadie escucha en el 8080.ssno 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 deloom-kill.13: Permission deniedwhile connecting to upstream, en un RHEL recién montado y con el backend escuchando: es SELinux.ausearch -m avc -ts recentmuestra eldenied { name_connect }denginx(ohttpd). La solución essetsebool -P httpd_can_network_connect 1, nosetenforce 0. En Apache aparece como(13)Permission denied: AH00957.- 504 Gateway Time-out, solo en el informe mensual:
upstream timed out (110: Connection timed out) while reading response header from upstreamyurt=60.001. El backend tarda más queproxy_read_timeout: se sube a 300 s solo enlocation /informes/y se pide que el informe sea asíncrono. En Apache esAH01102: 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/*.confwith 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 witha2ensite, and logs in/var/log/apache2/. - The DNS name:
fichajes.corp.examplepointing 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:
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; }}<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_serverin Nginx, the firstVirtualHoston the port in Apache.apachectl -Slists them. - The
upstreamaccess log format and Apache'sproxyformat 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,
SSLCertificateFileaccepts the full chain;SSLCertificateChainFileis obsolete. - The private key with
600permissions. On RHEL, Apache needsmod_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 withcertbot renew --dry-run.
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 GMTWithout -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:
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
HostandX-Forwarded-Proto, the app redirects tohttp://127.0.0.1:8080/loginand the user ends up on a page that does not load. In Apache,ProxyPreserveHost Onpasses theHost,ProxyPassReverserewrites redirects andmod_proxyaddsX-Forwarded-Foron its own.
4. Timeouts
| What it controls | Nginx (default) | Apache (default) |
|---|---|---|
| Connecting to the backend | proxy_connect_timeout (60 s) | connectiontimeout= on ProxyPass |
| Waiting for the response | proxy_read_timeout (60 s) | timeout= on ProxyPass, or ProxyTimeout; otherwise Timeout (60 s) |
| Maximum upload size | client_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:
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';rtis the total time andurtthe backend's: ifrtis high andurtlow, 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
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 -Tdumps the full configuration with everyinclude. - 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:
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 CET111: Connection refused: nothing is listening on 8080.ssreturns 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 theoom-kill.13: Permission deniedwhile connecting to upstream, on a freshly built RHEL with the backend listening: it is SELinux.ausearch -m avc -ts recentshows thedenied { name_connect }fornginx(orhttpd). The fix issetsebool -P httpd_can_network_connect 1, notsetenforce 0. In Apache it shows up as(13)Permission denied: AH00957.- 504 Gateway Time-out, only on the monthly report:
upstream timed out (110: Connection timed out) while reading response header from upstreamandurt=60.001. The backend takes longer thanproxy_read_timeout: it is raised to 300 s only inlocation /informes/and the report is asked to become asynchronous. In Apache it isAH01102: 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 →