Ubuntu 24.04: Conflicto Apache vs Nginx — arregla la vinculación de puertos y bucles de proxy de forma limpia

¿Te fue útil?

Instalaste “un servidor web” en Ubuntu 24.04 y de repente tienes dos. Uno se vincula al puerto 80, el otro jura que fue el primero, y tu navegador entra en un bucle de redirecciones que se siente como una cinta de correr con papeleo.

El caso n.º 34 es el clásico: Apache y Nginx habilitados, ambos intentando poseer 80/443, y una configuración de reverse proxy que accidentalmente se proxifica a sí misma. La buena noticia: esto se puede arreglar sin drama. La mejor noticia: puedes hacerlo aburrido y repetible, como les gusta a los sistemas en producción.

Cómo se manifiesta en producción (síntomas que realmente verás)

Este problema no se anuncia educadamente. Aparece como:

  • Nginx no arranca: bind() to 0.0.0.0:80 failed (98: Address already in use)
  • Apache no arranca: (98)Address already in use: AH00072: make_sock: could not bind to address
  • Navegador en bucle: interminable ERR_TOO_MANY_REDIRECTS
  • Nginx 502/504: backend inalcanzable, o accesible pero con protocolo incorrecto
  • Haces curl a localhost y obtienes el sitio “equivocado”: página por defecto del otro servidor
  • Funciona en localhost, falla externamente: porque la vinculación difiere entre 127.0.0.1 y 0.0.0.0 o IPv6

Las causas raíz casi siempre entran en uno de estos grupos:

  • Conflicto de propiedad de puertos: tanto Apache como Nginx intentan escuchar en 80 y/o 443.
  • Recursión accidental de proxy: Nginx se proxya a sí mismo, o Apache se proxya a sí mismo, frecuentemente vía localhost:80.
  • Confusión HTTP↔HTTPS: el backend cree que es HTTP, el front-end cree que es HTTPS, y las redirecciones rebotan para siempre.
  • Desajuste en vhost por nombre: encabezado Host no preservado o bloque por defecto captura el tráfico.

Un chiste corto, porque ya vamos a ponernos serios: cuando dos demonios pelean por el puerto 80, Linux no elige un ganador; simplemente te entrega un archivo de registro y mira.

Guion de diagnóstico rápido (primero/segundo/tercero)

Cuando estás de guardia, no necesitas una lección. Necesitas una secuencia que revele el cuello de botella rápidamente.
Esta es la secuencia que sigo cuando Ubuntu 24.04 aloja una pila web confusa.

Primero: ¿quién posee 80/443 ahora mismo?

  • Comprueba los sockets en escucha para :80 y :443 (IPv4 e IPv6).
  • Decide qué proceso debería poseer esos puertos (el front-end) y obliga al otro a vincularse en otro puerto o a no hacerlo.

Segundo: ¿hay un bucle de proxy?

  • Busca proxy_pass http://localhost (Nginx) o ProxyPass http://localhost (Apache) que apunten a un puerto que el propio proxy posee.
  • Sigue redirecciones con curl. Si ves un patrón repetido en Location, estás en un bucle.

Tercero: ¿se comunican correctamente cabeceras y esquema?

  • Confirma que X-Forwarded-Proto y Host llegan al backend.
  • Confirma que el backend confía en el proxy y no “actualiza” a HTTPS incorrectamente.

Cuarto (solo si hace falta): comportamiento del vhost por defecto y SNI

  • Revisa qué vhost es el por defecto y si tu server_name/ServerName coincide.
  • En TLS, confirma que SNI selecciona el certificado y bloque correctos.

Datos y contexto interesantes (lo que explica por qué ocurre)

Unos cuantos hechos concretos te ayudan a razonar sobre el desorden en lugar de hurgar a ciegas.
Aquí hay nueve que importan en la práctica:

  1. Apache es anterior a Nginx por años: Apache HTTP Server comenzó a mediados de los 90; Nginx llegó a principios de los 2000 para manejar concurrencia elevada de forma eficiente.
  2. El problema “C10k” influyó en el diseño de Nginx: las arquitecturas basadas en eventos se volvieron relevantes cuando manejar diez mil conexiones concurrentes dejó de ser teórico.
  3. Ambos servidores ahora pueden hacer ambos roles: Apache puede reverse proxy y terminar TLS; Nginx puede servir estático y proxyar servidores de aplicaciones. La elección es sobre todo preferencia operativa.
  4. Linux solo permite un listener por tupla IP:puerto: salvo que uses trucos avanzados como SO_REUSEPORT (y la mayoría de pilas web no deberían).
  5. IPv6 puede ser el propietario oculto: puedes “liberar” IPv4 :80 pero Apache aún vincule [::]:80, y Nginx fallará igual.
  6. systemd cambia la visibilidad de fallos: no “inicias un demonio”; inicias una unidad, y las dependencias de la unidad pueden reiniciar o mantener cosas medio vivas.
  7. Los sitios por defecto están diseñados para atraparte: Debian/Ubuntu incluyen sitios habilitados por defecto para Apache y Nginx. Son útiles hasta que dejan de serlo.
  8. Los bucles de redirección suelen ser por un desajuste de esquema: el backend piensa que la solicitud es HTTP y redirige a HTTPS, pero el proxy ya terminó TLS y envió HTTP al backend.
  9. “localhost” es una trampa en proxies: en la misma máquina, “localhost:80” a menudo apunta de nuevo al propio proxy, no al backend previsto.

Una cita para mantenerte honesto: “La esperanza no es una estrategia.” — General Gordon R. Sullivan.

Elige una arquitectura sensata (y cúmplela)

La decisión principal: ¿qué demonio es la puerta de entrada para 80/443?
Si mantienes ambos, uno es el proxy edge y el otro es un servicio backend vinculado a una dirección loopback o un puerto alternativo. Cualquier otra cosa es un club de la pelea.

Patrones recomendados

  • Patrón A (común): Nginx en 80/443 → Apache en 127.0.0.1:8080
    Usa Nginx para terminación TLS, HTTP/2/3 (si lo deseas), buffering y caché estático. Apache maneja apps legacy, .htaccess o aplicaciones diseñadas alrededor de módulos de Apache.
  • Patrón B: Apache en 80/443 → backends de app (PHP-FPM, upstream services)
    Si no necesitas Nginx, no lo ejecutes. Apache puede hacer reverse proxy y TLS perfectamente.
  • Patrón C: solo Nginx
    Lo más limpio para muchas implementaciones: Nginx + PHP-FPM (o upstream app) y sin Apache en absoluto.

Qué evitar

  • Que Apache y Nginx escuchen ambos en 80/443. Si “funciona”, es solo porque uno falló al arrancar o no está realmente vinculado.
  • Proxyar a localhost:80 desde el proxy edge. Así construyes una máquina de recursión infinita.
  • Mezclar responsabilidades de terminación TLS. O TLS termina en el proxy, o termina en el backend. No improvises a mitad de vuelo.

Tareas prácticas: comandos, salida esperada y decisiones (12+)

Estas son tareas reales que puedes ejecutar en Ubuntu 24.04 para diagnosticar y arreglar el problema.
Cada tarea incluye: comando, ejemplo de salida, qué significa y la decisión que tomas.

Task 1: Identify who is listening on ports 80 and 443 (IPv4/IPv6)

cr0x@server:~$ sudo ss -ltnp '( sport = :80 or sport = :443 )'
State  Recv-Q Send-Q Local Address:Port  Peer Address:Port Process
LISTEN 0      511          0.0.0.0:80         0.0.0.0:*     users:(("apache2",pid=1421,fd=4))
LISTEN 0      511             [::]:80            [::]:*     users:(("apache2",pid=1421,fd=5))
LISTEN 0      511          0.0.0.0:443        0.0.0.0:*     users:(("nginx",pid=1550,fd=6))

Meaning: Apache owns port 80 (both v4 and v6). Nginx owns 443. That’s already inconsistent.

Decision: Pick one front-end for both ports; usually Nginx on both 80/443, and move Apache to 127.0.0.1:8080.

Task 2: Confirm service state and recent failures

cr0x@server:~$ systemctl status nginx --no-pager
● nginx.service - A high performance web server and a reverse proxy server
     Loaded: loaded (/usr/lib/systemd/system/nginx.service; enabled; preset: enabled)
     Active: active (running) since Mon 2025-12-30 09:12:01 UTC; 5min ago
       Docs: man:nginx(8)
   Main PID: 1550 (nginx)
      Tasks: 3 (limit: 18906)
     Memory: 8.4M
        CPU: 120ms
cr0x@server:~$ systemctl status apache2 --no-pager
● apache2.service - The Apache HTTP Server
     Loaded: loaded (/usr/lib/systemd/system/apache2.service; enabled; preset: enabled)
     Active: active (running) since Mon 2025-12-30 09:10:44 UTC; 6min ago
       Docs: https://httpd.apache.org/docs/2.4/
   Main PID: 1421 (apache2)

Meaning: Both are enabled and running. That’s okay only if they are not fighting for the same ports.

Decision: If you want a proxy architecture, keep both but rebind Apache. If you don’t, stop and disable one.

Task 3: Look for “address already in use” in logs quickly

cr0x@server:~$ sudo journalctl -u nginx -u apache2 --since "1 hour ago" | tail -n 40
Dec 30 09:08:11 server nginx[1490]: nginx: [emerg] bind() to 0.0.0.0:80 failed (98: Address already in use)
Dec 30 09:08:11 server nginx[1490]: nginx: [emerg] still could not bind()
Dec 30 09:08:12 server systemd[1]: nginx.service: Failed with result 'exit-code'.
Dec 30 09:11:59 server systemd[1]: Started nginx.service - A high performance web server and a reverse proxy server.

Meaning: Nginx tried to bind 80 and lost at least once. Something changed later (maybe Apache stopped briefly, or Nginx config changed).

Decision: Fix port ownership so restarts are deterministic.

Task 4: Check what Apache is configured to listen on

cr0x@server:~$ sudo grep -R "^[[:space:]]*Listen" -n /etc/apache2/ports.conf /etc/apache2/sites-enabled/*.conf
/etc/apache2/ports.conf:5:Listen 80
/etc/apache2/ports.conf:8:Listen 443

Meaning: Apache is explicitly listening on both 80 and 443.

Decision: If Nginx is the edge, remove these and move Apache to loopback:8080 (and possibly 127.0.0.1:8443 if you insist on TLS internally).

Task 5: Check what Nginx is configured to listen on

cr0x@server:~$ sudo nginx -T 2>/dev/null | grep -R --line-number -E "listen\s+(80|443)" /etc/nginx/sites-enabled /etc/nginx/conf.d
/etc/nginx/sites-enabled/default:22:	listen 80 default_server;
/etc/nginx/sites-enabled/default:23:	listen [::]:80 default_server;
/etc/nginx/sites-enabled/default:30:	listen 443 ssl default_server;
/etc/nginx/sites-enabled/default:31:	listen [::]:443 ssl default_server;

Meaning: Nginx is also configured for both ports as default server. Classic collision.

Decision: Keep Nginx on 80/443; move Apache off those ports.

Task 6: Detect an accidental proxy loop in Nginx

cr0x@server:~$ sudo grep -R --line-number -E "proxy_pass\s+http://(localhost|127\.0\.0\.1)(:80)?/?;" /etc/nginx/sites-enabled
/etc/nginx/sites-enabled/app.conf:41:    proxy_pass http://localhost;

Meaning: If Nginx listens on 80 and proxies to http://localhost (which defaults to port 80), it’s proxying to itself. That’s a loop generator.

Decision: Proxy to a backend port like 127.0.0.1:8080, or to a Unix socket, or to an upstream service.

Task 7: Detect an accidental proxy loop in Apache

cr0x@server:~$ sudo grep -R --line-number -E "ProxyPass\s+/" /etc/apache2/sites-enabled
/etc/apache2/sites-enabled/000-default.conf:18:ProxyPass / http://127.0.0.1:80/
/etc/apache2/sites-enabled/000-default.conf:19:ProxyPassReverse / http://127.0.0.1:80/

Meaning: Apache is proxying “/” to 127.0.0.1:80. If Apache itself owns 80, that’s recursion.

Decision: Either remove Apache proxying or ensure the target is a different port/service.

Task 8: Follow redirects and see the loop with curl

cr0x@server:~$ curl -I -L --max-redirs 10 http://example.internal/
HTTP/1.1 301 Moved Permanently
Server: nginx
Location: https://example.internal/

HTTP/2 301
server: nginx
location: http://example.internal/

curl: (47) Maximum (10) redirects followed

Meaning: HTTP redirects to HTTPS, then HTTPS redirects back to HTTP. That’s almost always an “X-Forwarded-Proto” or backend scheme detection issue.

Decision: Ensure one component is responsible for redirects, and the backend understands the original scheme.

Task 9: Confirm the Host header and forwarding headers from the edge

cr0x@server:~$ sudo nginx -T 2>/dev/null | sed -n '1,120p'
# configuration file /etc/nginx/nginx.conf:
user www-data;
worker_processes auto;
...
cr0x@server:~$ sudo grep -R --line-number -E "proxy_set_header\s+(Host|X-Forwarded-Proto|X-Forwarded-For)" /etc/nginx/sites-enabled
/etc/nginx/sites-enabled/app.conf:35:    proxy_set_header Host $host;
/etc/nginx/sites-enabled/app.conf:36:    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
/etc/nginx/sites-enabled/app.conf:37:    proxy_set_header X-Forwarded-Proto $scheme;

Meaning: These headers are present. Good. If missing, backends often mis-detect scheme/host and redirect.

Decision: If redirect loops persist, confirm the backend trusts these headers and isn’t overriding them.

Task 10: Verify Apache vhost bindings after changes (sanity check)

cr0x@server:~$ sudo apachectl -S
VirtualHost configuration:
*:8080                 is a NameVirtualHost
         default server app.internal (/etc/apache2/sites-enabled/001-app.conf:1)
         port 8080 namevhost app.internal (/etc/apache2/sites-enabled/001-app.conf:1)
ServerRoot: "/etc/apache2"
Main DocumentRoot: "/var/www/html"
Main ErrorLog: "/var/log/apache2/error.log"

Meaning: Apache is now bound to 8080 for vhosts, not 80/443. That’s what you want when Nginx is in front.

Decision: Proceed to restart services and validate end-to-end.

Task 11: Validate Nginx config and reload without guessing

cr0x@server:~$ sudo nginx -t
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

Meaning: Syntax is OK. This doesn’t prove your proxy target is correct, but it prevents “reload broke prod” mistakes.

Decision: Reload Nginx only after a successful test.

Task 12: Confirm backend reachability directly (bypass proxy)

cr0x@server:~$ curl -sS -D- http://127.0.0.1:8080/ -o /dev/null | head
HTTP/1.1 200 OK
Date: Tue, 30 Dec 2025 09:20:17 GMT
Server: Apache/2.4.58 (Ubuntu)
Content-Type: text/html; charset=UTF-8

Meaning: Apache backend is reachable on 8080 and returns 200. Good baseline.

Decision: If this fails, fix the backend first. Don’t debug the proxy while the backend is down.

Task 13: Confirm edge behavior (Nginx on 80/443)

cr0x@server:~$ curl -sS -D- http://127.0.0.1/ -o /dev/null | head
HTTP/1.1 301 Moved Permanently
Server: nginx
Location: https://app.internal/
cr0x@server:~$ curl -k -sS -D- https://127.0.0.1/ -o /dev/null | head
HTTP/2 200
server: nginx
content-type: text/html; charset=UTF-8

Meaning: Nginx is the edge. HTTP redirects to HTTPS; HTTPS returns 200.

Decision: If HTTPS returns 301 back to HTTP, fix forwarded proto / backend redirect logic.

Task 14: Verify no other process is squatting on ports

cr0x@server:~$ sudo lsof -nP -iTCP:80 -sTCP:LISTEN
COMMAND  PID     USER   FD   TYPE DEVICE SIZE/OFF NODE NAME
nginx   1550     root    6u  IPv4  29826      0t0  TCP *:80 (LISTEN)
nginx   1550     root    7u  IPv6  29827      0t0  TCP *:80 (LISTEN)

Meaning: Only Nginx listens on 80. That’s the “boring” state you want.

Decision: If you see Apache or something else here, stop and re-check configs and enabled units.

Corrige la vinculación de puertos de forma limpia (Apache o Nginx, elige tu propietario)

La solución no es “reinicia hasta que funcione.” La solución es: un servicio posee los puertos públicos, el otro no.
Abajo hay dos soluciones limpias. Elige una. Comprométete con ella.

Solución 1 (recomendada para pilas mixtas): Nginx posee 80/443, Apache se mueve a 127.0.0.1:8080

Este es el patrón más común en producción cuando necesitas Apache por razones de aplicación (rewrites legacy, módulos, .htaccess)
pero prefieres Nginx por su comportamiento como edge proxy.

Paso A: Mover Apache fuera de 80/443

Edita /etc/apache2/ports.conf para vincular Apache solo al loopback:

cr0x@server:~$ sudo sed -n '1,120p' /etc/apache2/ports.conf
# If you just change the port or add more ports here, you will likely also
# have to change the VirtualHost statement in
# /etc/apache2/sites-enabled/000-default.conf

Listen 80
Listen 443

Reemplaza por:

cr0x@server:~$ sudo bash -lc 'cat > /etc/apache2/ports.conf <<EOF
Listen 127.0.0.1:8080

<IfModule ssl_module>
    Listen 127.0.0.1:8443
</IfModule>

<IfModule mod_gnutls.c>
    Listen 127.0.0.1:8443
</IfModule>
EOF'

¿Por qué loopback? Porque evita que Apache sea accesible directamente desde la red.
Eso elimina toda una categoría de errores “bypass del proxy” y sorpresas de seguridad.

Paso B: Actualizar vhosts de Apache para coincidir con el nuevo puerto

Tus sitios habilitados probablemente contienen <VirtualHost *:80>. Cámbialos a 127.0.0.1:8080 o *:8080 según prefieras.
Me gusta el loopback explícito para evitar errores.

cr0x@server:~$ sudo grep -R --line-number "<VirtualHost" /etc/apache2/sites-enabled
/etc/apache2/sites-enabled/000-default.conf:1:<VirtualHost *:80>
cr0x@server:~$ sudo sed -i 's/<VirtualHost \*:80>/<VirtualHost 127.0.0.1:8080>/' /etc/apache2/sites-enabled/000-default.conf

Si estabas usando vhosts TLS en Apache (*:443), decide si todavía los necesitas.
En un diseño típico “Nginx termina TLS”, deshabilitas los vhosts TLS de Apache y mantienes Apache solo HTTP internamente.

Paso C: Deshabilitar SSL en Apache y listeners 443 si Nginx termina TLS

cr0x@server:~$ sudo a2dismod ssl
Module ssl disabled.
To activate the new configuration, you need to run:
  systemctl restart apache2

Meaning: Apache won’t load mod_ssl. If you still need internal TLS, keep it enabled and use 8443 loopback, but be consistent.

Decision: Most teams: terminate TLS at Nginx, run plain HTTP to Apache on 127.0.0.1:8080.

Paso D: Reiniciar Apache y asegurar que ya no vincula 80/443

cr0x@server:~$ sudo systemctl restart apache2
cr0x@server:~$ sudo ss -ltnp '( sport = :80 or sport = :443 or sport = :8080 )'
State  Recv-Q Send-Q Local Address:Port   Peer Address:Port Process
LISTEN 0      511    127.0.0.1:8080        0.0.0.0:*     users:(("apache2",pid=2012,fd=3))
LISTEN 0      511      0.0.0.0:80          0.0.0.0:*     users:(("nginx",pid=1550,fd=6))
LISTEN 0      511      0.0.0.0:443         0.0.0.0:*     users:(("nginx",pid=1550,fd=7))

Meaning: Perfect separation. Nginx is the edge. Apache is internal.

Decision: Now configure Nginx upstream to 127.0.0.1:8080.

Paso E: Configurar el reverse proxy de Nginx correctamente

En tu sitio de Nginx, el bloque de proxy debería verse así (lo importante es el puerto backend y las cabeceras):

cr0x@server:~$ sudo bash -lc 'cat > /etc/nginx/sites-available/app.conf <<EOF
server {
    listen 80;
    listen [::]:80;
    server_name app.internal;

    return 301 https://\$host\$request_uri;
}

server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name app.internal;

    ssl_certificate     /etc/ssl/certs/ssl-cert-snakeoil.pem;
    ssl_certificate_key /etc/ssl/private/ssl-cert-snakeoil.key;

    location / {
        proxy_pass http://127.0.0.1:8080;
        proxy_set_header Host \$host;
        proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto \$scheme;
    }
}
EOF'
cr0x@server:~$ sudo ln -sf /etc/nginx/sites-available/app.conf /etc/nginx/sites-enabled/app.conf
cr0x@server:~$ sudo nginx -t
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
cr0x@server:~$ sudo systemctl reload nginx

Solución 2: Apache posee 80/443, eliminar Nginx

Este es el movimiento correcto cuando Nginx se instaló “porque alguien dijo que era más rápido”
y luego nadie lo usó intencionadamente.

Paso A: Detener y deshabilitar Nginx

cr0x@server:~$ sudo systemctl stop nginx
cr0x@server:~$ sudo systemctl disable --now nginx
Removed "/etc/systemd/system/multi-user.target.wants/nginx.service".

Meaning: Nginx won’t start on reboot or by default targets.

Decision: If you never needed Nginx, keep it off. Less moving parts, fewer 3 a.m. surprises.

Paso B: Confirmar que Apache se vincula a los puertos públicos

cr0x@server:~$ sudo ss -ltnp '( sport = :80 or sport = :443 )'
State  Recv-Q Send-Q Local Address:Port  Peer Address:Port Process
LISTEN 0      511          0.0.0.0:80         0.0.0.0:*     users:(("apache2",pid=1421,fd=4))
LISTEN 0      511          0.0.0.0:443        0.0.0.0:*     users:(("apache2",pid=1421,fd=5))

Meaning: Apache is the edge now.

Decision: Remove any reverse proxy configuration that was only needed because Nginx existed, and validate redirects/certs in Apache.

Elimina bucles de proxy y redirecciones (de una vez)

Los conflictos de puertos son ruidosos. Los bucles de proxy son sigilosos.
El sistema puede estar “arriba” mientras los usuarios son rebotados en círculos o el tráfico consume CPU con recursión inútil.

Bucles de proxy: la mecánica

Un bucle de proxy ocurre cuando un proxy reenvía una solicitud a una dirección que vuelve a enrutar al propio proxy.
En un solo host, eso suele ser localhost o 127.0.0.1 en el mismo puerto en el que el proxy escucha.

Ejemplo de mala configuración en Nginx:
listen 80; y proxy_pass http://localhost;.
Eso dice: “Recibe la solicitud en 80, envíala a 80.” Lo único que falta es una carta de disculpa.

Bucles de redirección: el culpable habitual

Los bucles de redirección suelen ser por “confusión de esquema.” Tu backend ve HTTP (porque el proxy habla HTTP con él),
pero los usuarios están en HTTPS (porque el proxy termina TLS). Si el backend fuerza HTTPS mediante una redirección,
envía una redirección a HTTPS. El proxy recibe esa solicitud y otra vez envía HTTP al backend. Repite hasta que el navegador se rinde.

Arregla la confusión de esquema: haz tres cosas, no una

  • Envía X-Forwarded-Proto desde el proxy. Nginx: proxy_set_header X-Forwarded-Proto $scheme;
  • Haz que el backend confíe en las cabeceras del proxy. Muchos frameworks requieren configuración explícita de “trusted proxies”.
  • Asegura que las redirecciones ocurran en un solo lugar. Normalmente el proxy edge, porque sabe qué usó el cliente.

Prueba bucles a propósito con curl

No pruebes refrescando el navegador y entrecerrando los ojos. Usa curl -I -L y limita las redirecciones.
Quieres ver una cadena pequeña y sensata: quizá un 301 de HTTP→HTTPS, y luego 200.

cr0x@server:~$ curl -I -L --max-redirs 5 http://app.internal/ | sed -n '1,20p'
HTTP/1.1 301 Moved Permanently
Server: nginx
Location: https://app.internal/

HTTP/2 200
server: nginx

Meaning: One redirect, then success. That’s clean.

Decision: If you see alternating http/https locations, fix forwarded proto and backend redirect logic.

Segundo chiste corto (y terminamos con los chistes): un bucle de redirección es como una reunión corporativa—todos están “alineando” y nada se envía.

Tres mini-historias corporativas (porque producción tiene memoria)

1) Incidente por una suposición errónea: “localhost significa el backend”

Una empresa mediana ejecutaba un único host Ubuntu para una herramienta interna. Añadieron Nginx “solo para TLS” delante de Apache.
Un ingeniero copió un snippet de proxy de una wiki antigua: proxy_pass http://localhost;. Parecía inofensivo.
La suposición: localhost equivale a Apache.

Pero Apache aún escuchaba en el puerto 80, y Nginx también. Tras una secuencia de reinicios, Nginx quedó como el listener en 80.
Ahora localhost:80 significaba el propio Nginx. El bucle no fue obvio inmediatamente porque los health checks eran ingenuos:
consultaban / y aceptaban un 301.

Bajo carga, el bucle se manifestó como picos de CPU y acumulación de conexiones. Los workers de Nginx no estaban “lentos”; estaban ocupados hablándose a sí mismos.
Los logs de Apache parecían tranquilos, porque Apache no hacía nada. El on-call vio 502s y empezó a escalar la VM,
lo que no mejoró nada salvo la factura de la nube.

El arreglo tomó cinco minutos una vez que alguien ejecutó ss -ltnp y siguió las redirecciones con curl.
La lección no fue “no uses localhost.” Fue: nunca proxyes al mismo puerto en el que escuchas, y siempre haz explícitos los puertos backend.

2) Optimización que salió mal: “Forcemos HTTPS en todas partes”

Otra organización tenía Nginx en el borde y Apache detrás. Decidieron estandarizar en HTTPS y agregaron reglas de redirección en ambas capas.
Nginx redirigía HTTP a HTTPS. Apache también redirigía HTTP a HTTPS, “por si acaso.”
Nadie documentó la fuente de la verdad.

Funcionó en staging donde todo era plain HTTP. En producción, TLS terminaba en Nginx.
Nginx hablaba HTTP con Apache. Apache vio una solicitud HTTP e hizo su “útil” redirección a HTTPS.
No conocía el esquema original del cliente. Solo sabía lo que veía.

La URL de redirección que generó rebotó al cliente de vuelta a Nginx, que entonces envió HTTP a Apache otra vez.
Bucle infinito. El monitoreo mostró aumento de respuestas 301, que parecía “saludable” si solo mirabas códigos de estado.
Los usuarios quedaron bloqueados fuera de la app y los tickets de soporte mostraron el clásico “demasiadas redirecciones.”

El rollback fue sencillo: eliminar la imposición de HTTPS en Apache y confiar en Nginx para las redirecciones.
Luego añadieron X-Forwarded-Proto y configuraron la aplicación para que lo confíe,
así las URLs generadas por la app fueron correctas. La “optimización” no fue hacer cumplir HTTPS; fue hacerlo dos veces.

3) Práctica aburrida pero correcta que salvó el día: vincular el backend al loopback y documentar puertos

Un equipo cercano a finanzas tenía fama de moverse despacio. También tenían menos incidentes, irritantemente.
Su regla: servicios edge vinculan puertos públicos; backends se vinculan a 127.0.0.1 o a una VLAN privada solo. Siempre.
También mantenían un pequeño /etc/services.d/README describiendo qué daemon poseía qué puertos.

Durante una actualización del SO, una dependencia de paquete tiró dentro Apache.
Se habilitó un sitio por defecto automáticamente. En muchos sistemas eso habría causado una caída: nuevo demonio, nuevos puertos, listeners sorpresa.
En el suyo, Apache no pudo vincular a 80/443 porque Nginx ya los poseía, y Apache estaba configurado (por política) para vincular solo loopback.

El efecto neto fue una advertencia inocua en journald y cero impacto al cliente.
El on-call investigó a la mañana siguiente con café en lugar de adrenalina.
Deshabilitaron Apache y siguieron con su vida.

La moraleja es dolorosamente poco sexy: las políticas aburridas de vinculación de puertos previenen incidentes emocionantes. Los incidentes emocionantes no impresionan a los auditores.

Errores comunes: síntoma → causa raíz → solución

1) Nginx no arranca: “Address already in use”

Síntoma: nginx: [emerg] bind() to 0.0.0.0:80 failed (98: Address already in use)

Causa raíz: Apache (u otro servicio) ya está escuchando en 80/443 (posiblemente solo en IPv6).

Solución: Decide el propietario del puerto; mueve el otro demonio a 127.0.0.1:8080 o deshabilítalo. Verifica con ss -ltnp.

2) Apache no arranca: errores AH00072 de bind

Síntoma: Los logs de Apache muestran AH00072 y el servicio no arranca.

Causa raíz: Nginx ya está escuchando en los puertos que Apache está configurado para usar.

Solución: Misma disciplina: un edge. Re-bindea Apache en /etc/apache2/ports.conf y actualiza vhosts, o deshabilita Nginx.

3) Navegador: ERR_TOO_MANY_REDIRECTS

Síntoma: El navegador reporta bucle de redirecciones; curl muestra ubicaciones alternadas http/https.

Causa raíz: Duplicación de enforcement HTTPS, o backend no respeta X-Forwarded-Proto, o URL base de la app incorrecta.

Solución: Haz que las redirecciones ocurran en una sola capa (normalmente edge). Configura cabeceras de reenvío. Configura backend/app para confiar en el proxy y generar URLs con el esquema correcto.

4) Nginx 502 Bad Gateway

Síntoma: Nginx devuelve 502; el log de errores menciona connect() failed o upstream prematurely closed connection.

Causa raíz: Backend caído, puerto incorrecto, protocolo equivocado (proxyando HTTP a un backend HTTPS o viceversa), o vinculación solo en IPv6/IPv4 mismatch.

Solución: Prueba el backend directamente con curl; verifica sockets en escucha; corrige el objetivo de proxy_pass; asegura que el backend se vincule a 127.0.0.1 si es local.

5) “Funciona en localhost pero no desde fuera”

Síntoma: curl local funciona; remoto falla o llega a un sitio por defecto.

Causa raíz: Servicio vinculado solo al loopback o solo a IPv6; reglas de firewall; vhost por defecto captura el Host header no coincidente.

Solución: Verifica vinculaciones con ss; verifica el ruteo de Host con curl -H "Host: ..."; ajusta server_name/ServerName y default_server.

6) Sitio equivocado servido (página por defecto)

Síntoma: Obtienes “Apache2 Ubuntu Default Page” o la página de bienvenida de Nginx.

Causa raíz: Sitio por defecto habilitado y ganando prioridad; mismatch de server_name; SNI mismatch en TLS.

Solución: Deshabilita los defaults, asegura el orden correcto de vhosts, establece explícitamente server_name/ServerName, valida SNI con curl y dumps de vhost.

Listas de verificación / plan paso a paso

Checklist A: “Solo quiero que funcione” (Nginx edge, Apache backend)

  1. Ejecuta ss -ltnp para 80/443 y anota los propietarios.
  2. Mueve Apache a 127.0.0.1:8080 en /etc/apache2/ports.conf.
  3. Actualiza vhosts de Apache de *:80 a 127.0.0.1:8080.
  4. Deshabilita vhosts SSL de Apache salvo que tengas motivo para TLS interno.
  5. Reinicia Apache, confirma que solo escucha en 8080.
  6. Configura Nginx para escuchar en 80/443 y hacer proxy a http://127.0.0.1:8080.
  7. Asegura que Nginx establezca Host, X-Forwarded-For, X-Forwarded-Proto.
  8. Prueba backend directamente (curl a 127.0.0.1:8080), luego prueba edge (curl a 127.0.0.1:80/443).
  9. Sigue redirecciones con curl y limita redirecciones. Máximo una redirección para HTTP→HTTPS.
  10. Solo entonces expón al tráfico real.

Checklist B: “Quiero un solo servidor web” (solo Apache)

  1. Detén y deshabilita Nginx.
  2. Confirma que Apache escucha en 80/443 y sirve el vhost correcto.
  3. Elimina la lógica de redirección de la era Nginx si es duplicativa.
  4. Valida certificados TLS y selección de vhost.
  5. Confirma que no haya reglas de firewall sobrantes que esperen comportamiento de Nginx.

Checklist C: Pruebas de regresión tras cualquier cambio

  • Puertos: verifica listeners en 80/443/8080 con ss.
  • Sintaxis de config: nginx -t, apachectl configtest.
  • Prueba directa del backend: curl al puerto backend directamente.
  • Prueba del edge: curl edge HTTP y HTTPS, sigue redirecciones, confirma estado final.
  • Ruteo por Host: curl con un encabezado Host para asegurar match de vhost.
  • Logs: revisa error log de Nginx y error log de Apache por regresiones inmediatas.
cr0x@server:~$ sudo apachectl configtest
Syntax OK

Meaning: Apache config parses. It doesn’t mean your vhosts are correct, but it reduces “reload roulette.”

Preguntas frecuentes (las que te harán a las 2 a.m.)

1) ¿Pueden Apache y Nginx ejecutarse en el mismo servidor?

Sí. Solo que no pueden ambos escuchar en la misma IP:puerto. Elige un edge (80/443) y mueve el otro a loopback o un puerto diferente.

2) ¿Por qué Nginx dice que el puerto 80 está en uso cuando no veo Apache en IPv4?

Porque algo puede estar escuchando en IPv6 ([::]:80). ss -ltnp mostrará ambas pilas.
Arregla cambiando el listener o deshabilitando el otro servicio.

3) ¿Debo proxyar a “localhost” o a “127.0.0.1”?

Usa 127.0.0.1:8080 (puerto explícito) o un socket Unix. “localhost” sin puerto invita a ambigüedad y bucles.

4) ¿Por qué obtengo la página por defecto de Apache/Nginx en lugar de mi app?

Tu solicitud aterriza en un vhost por defecto (server_name/ServerName incorrecto, orden erróneo o Host header no coincidente).
Deshabilita sitios por defecto y haz los nombres explícitos.

5) ¿Qué provoca bucles HTTP↔HTTPS detrás de un reverse proxy?

Generalmente desajuste de esquema. El backend ve HTTP y fuerza HTTPS, pero el proxy ya hizo HTTPS y reenvía HTTP internamente.
Arregla configurando X-Forwarded-Proto y haciendo que el backend lo confíe; aplica redirecciones en una sola capa.

6) Si Nginx termina TLS, ¿debería Apache seguir escuchando en 443?

Normalmente no. Mantén Apache interno solo HTTP en 127.0.0.1:8080. TLS interno es una opción válida, pero debe ser intencional y consistente.

7) ¿Cómo confirmo que el proxy realmente envía el encabezado Host?

Revisa la configuración de Nginx para proxy_set_header Host $host;. Después verifica el comportamiento del backend haciendo curl a través de Nginx con diferentes Host headers.

cr0x@server:~$ curl -sS -H "Host: app.internal" http://127.0.0.1/ -o /dev/null -D- | head
HTTP/1.1 301 Moved Permanently
Server: nginx
Location: https://app.internal/

8) ¿Cuál es la forma más limpia de asegurar que Apache no sea accesible directamente?

Vincula Apache a 127.0.0.1 solamente. No confíes en reglas de firewall como primera línea de defensa; haz que el socket sea inaccesible desde la red.

9) ¿Cambian esta historia las unidades socket de systemd?

Pueden. La activación por socket puede mantener puertos abiertos independientemente del proceso del servicio.
En el mundo Apache/Nginx en Ubuntu, normalmente verás servicios llanos, pero siempre verifica con ss y systemctl list-sockets.

cr0x@server:~$ systemctl list-sockets --no-pager | head
LISTEN               UNIT                        ACTIVATES
/dev/log             systemd-journald.socket     systemd-journald.service
...

10) Arreglé los puertos, pero todavía obtengo 502. ¿Ahora qué?

Confirma la alcanzabilidad y protocolo del backend. Haz curl al backend directamente. Luego revisa los logs de error de Nginx para errores de conexión a upstream.
502 suele ser “no puede alcanzar upstream” o “upstream respondió algo inesperado.”

cr0x@server:~$ sudo tail -n 20 /var/log/nginx/error.log
2025/12/30 09:24:08 [error] 1552#1552: *12 connect() failed (111: Connection refused) while connecting to upstream, client: 10.0.2.15, server: app.internal, request: "GET / HTTP/2.0", upstream: "http://127.0.0.1:8080/", host: "app.internal"

Conclusión: siguientes pasos para no volver a este lío

La solución al conflicto Apache vs Nginx en Ubuntu 24.04 no es ingenio. Es propiedad.
Decide quién posee 80/443. Vincula el backend al loopback. Haz explícitos los objetivos del proxy. Luego prueba con curl como si te lo tomaras en serio.

Pasos prácticos siguientes:

  • Escribe tu arquitectura prevista (Nginx edge → Apache 8080, o solo Apache, o solo Nginx).
  • Haz cumplir la propiedad de puertos en la configuración, no en la memoria tribal.
  • Añade una pequeña prueba de humo post-cambio: listeners con ss + cadena de redirección con curl + curl directo al backend.
  • Deshabilita sitios por defecto que no uses. Los defaults son amigables hasta que no lo son.
  • Mantén las redirecciones en una sola capa y sé explícito con las cabeceras de reenvío.

Si haces eso, el caso n.º 34 queda resuelto. Permanentemente. El mejor tipo de incidente es el que dejas de tener.

← Anterior
Proxmox Ceph «monitor caído/fuera de quórum»: restaurar monitores y quórum sin pánico
Siguiente →
Planificación del ancho de vdev en ZFS: por qué más discos por vdev tiene un coste

Deja un comentario