Skip to content

🎯 Meta: entender el servidor web que está delante de tu aplicación: recibe todas las peticiones de internet, sirve archivos, gestiona HTTPS y reparte carga. Nginx y Apache son los "porteros" de tu backend.

Versiones: Nginx 1.30 (stable) · Apache 2.4.


13.1 · ¿Por qué necesito un servidor web delante de mi app?

Tu app (Laravel, FastAPI, etc.) puede recibir peticiones directamente, pero en producción no debe. Delante va Nginx/Apache porque:

Internet  →  ┌─────────┐  →  ┌──────────────┐
             │  NGINX   │      │  Tu app       │
   🌍  →→→   │ (portero)│      │ (Laravel...)  │
             └─────────┘      └──────────────┘

             Se encarga de:
             · HTTPS/TLS (el candado 🔒)
             · Servir archivos estáticos (imágenes, CSS) rápido
             · Repartir carga entre varias copias de tu app
             · Comprimir, cachear, limitar peticiones
             · Ocultar y proteger tu app

🧠 Analogía: tu app es el cocinero especializado; Nginx es el maître que recibe a todos en la puerta, gestiona la cola, revisa reservas (HTTPS) y solo pasa a cocina lo que hace falta. Que el cocinero atienda la puerta es ineficiente y peligroso.


13.2 · Nginx vs Apache

NginxApache
ArquitecturaAsíncrona (event-driven)Procesos/hilos por conexión
Rendimiento con mucha concurrencia✅ Excelente⚠️ Consume más
Archivos estáticos✅ RapidísimoBueno
ConfiguraciónEn archivos centrales.htaccess por carpeta (flexible)
Uso típico 2026Reverse proxy, la opción por defectoHosting PHP clásico, apps legadas

💡 Recomendación 2026: para proyectos nuevos, Nginx. Es el estándar como reverse proxy y el que verás en el 90% de despliegues modernos. Aprende Apache si mantienes sistemas PHP tradicionales o hosting compartido. Cubrimos ambos, pero el foco es Nginx.


13.3 · Los 3 roles de Nginx

Rol 1 — Servir archivos estáticos

Lo más básico: entregar HTML, CSS, JS, imágenes directamente desde el disco (más rápido que cualquier app):

nginx
server {
    listen 80;
    server_name misitio.com;
    root /var/www/misitio/public;      # carpeta con los archivos
    index index.html;

    location / {
        try_files $uri $uri/ =404;     # busca el archivo; si no, 404
    }
}

Rol 2 — Reverse proxy (lo más importante)

Recibe la petición y la reenvía a tu app que corre en otro puerto (ej. FastAPI en el 8000):

nginx
server {
    listen 80;
    server_name api.misitio.com;

    location / {
        proxy_pass http://127.0.0.1:8000;          # tu app
        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;
    }
}

🧠 "Reverse" proxy: un proxy normal representa al cliente (oculta quién navega). Un reverse proxy representa al servidor (oculta y protege tu app; el mundo solo ve Nginx). Los headers X-Forwarded-* le dicen a tu app la IP real del cliente y si vino por HTTPS.

Rol 3 — Balanceador de carga

Reparte peticiones entre varias copias de tu app (para aguantar más tráfico):

nginx
upstream backend {
    server 127.0.0.1:8000;
    server 127.0.0.1:8001;
    server 127.0.0.1:8002;
    # least_conn;   ← estrategia: a la que tenga menos conexiones
}

server {
    listen 80;
    location / {
        proxy_pass http://backend;    # Nginx reparte entre las 3
    }
}

Estrategias: round-robin (por defecto, una a cada una por turnos), least_conn (a la menos ocupada), ip_hash (misma IP siempre al mismo servidor).


13.4 · HTTPS/TLS gratis con Let's Encrypt

En producción, HTTPS es obligatorio (el candado 🔒). Con Certbot + Let's Encrypt es gratis y automático:

bash
sudo apt install certbot python3-certbot-nginx
sudo certbot --nginx -d misitio.com -d www.misitio.com

Certbot modifica tu config de Nginx automáticamente para servir HTTPS y renueva el certificado solo (cada 90 días). La config resultante se ve así:

nginx
server {
    listen 443 ssl;
    server_name misitio.com;

    ssl_certificate     /etc/letsencrypt/live/misitio.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/misitio.com/privkey.pem;

    location / {
        proxy_pass http://127.0.0.1:8000;
        # ...headers...
    }
}

# Redirige todo el HTTP a HTTPS
server {
    listen 80;
    server_name misitio.com;
    return 301 https://$host$request_uri;
}

💡 Tip: comprueba tu configuración TLS en ssllabs.com/ssltest. Apunta a una nota "A". La seguridad de TLS (versiones, cifrados) la profundizas en tu Manual de Ciberseguridad, capítulo de Criptografía.

HTTP/2 y HTTP/3: más rápido sobre el mismo HTTPS

Desde Nginx 1.25+, HTTP/2 se activa con una directiva separada (ya no como parámetro de listen):

nginx
server {
    listen 443 ssl;
    listen 443 quic reuseport;    # HTTP/3 (QUIC, sobre UDP) — Nginx 1.25+
    http2 on;                     # HTTP/2 sobre la conexión TLS de arriba
    add_header Alt-Svc 'h3=":443"; ma=86400';   # le dice al navegador que pruebe HTTP/3

    server_name misitio.com;
    # ...ssl_certificate, proxy_pass, etc.
}

🧠 Por qué importa: HTTP/1.1 abre una conexión por recurso (o las serializa); HTTP/2 multiplexa muchas peticiones en una sola conexión TCP (sin head-of-line blocking a nivel de petición), y HTTP/3 va un paso más allá corriendo sobre QUIC/UDP, eliminando el head-of-line blocking también a nivel de paquete perdido. Para una API con muchas peticiones concurrentes (dashboards, SPAs), la diferencia se nota en la latencia percibida, no solo en benchmarks.


13.5 · Cabeceras de seguridad y hardening

Añade cabeceras que protegen a tus usuarios (defensa en profundidad):

nginx
server {
    # ...

    # Seguridad
    add_header X-Frame-Options "SAMEORIGIN" always;              # anti clickjacking
    add_header X-Content-Type-Options "nosniff" always;         # anti MIME sniffing
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;  # fuerza HTTPS
    # Content-Security-Policy: la más potente (y delicada) — configúrala con cuidado

    server_tokens off;      # oculta la versión de Nginx (menos info al atacante)
}

🔗 Estas cabeceras son defensa web pura: mitigan clickjacking, XSS, downgrade a HTTP. En tu Manual de Ciberseguridad (cap. Defensa) verás el lado atacante de cada una. Aquí las pones; allí entiendes de qué protegen.


13.6 · Compresión y caché (rendimiento)

nginx
# Comprimir respuestas (menos datos = más rápido)
gzip on;
gzip_types text/plain text/css application/json application/javascript;
gzip_min_length 1000;

# Brotli (módulo aparte, ngx_brotli): comprime ~15-20% mejor que gzip para texto/JS/CSS
brotli on;
brotli_types text/plain text/css application/json application/javascript;

# Cachear archivos estáticos en el navegador del usuario
location ~* \.(jpg|jpeg|png|gif|css|js|woff2)$ {
    expires 30d;                              # que el navegador los guarde 30 días
    add_header Cache-Control "public, immutable";
}

# Limitar peticiones (anti abuso / rate limiting básico)
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
server {
    location /api/ {
        limit_req zone=api burst=20 nodelay;   # máx 10 req/s por IP, ráfaga de 20
        proxy_pass http://127.0.0.1:8000;
    }
}

💡 Rate limiting en Nginx es tu primera línea contra abusos y fuerza bruta, antes de que la petición llegue a tu app. Combínalo con el rate limiting de tu framework para defensa en capas.


13.7 · Nginx + PHP (Laravel) con PHP-FPM

PHP no corre "dentro" de Nginx; se usa PHP-FPM (FastCGI Process Manager) y Nginx le pasa las peticiones PHP:

nginx
server {
    listen 80;
    server_name laravel.test;
    root /var/www/laravel/public;      # ¡ojo! la carpeta public
    index index.php;

    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    location ~ \.php$ {
        fastcgi_pass unix:/var/run/php/php8.5-fpm.sock;   # PHP-FPM
        fastcgi_index index.php;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
    }
}

⚠️ El root apunta a public/, nunca a la raíz del proyecto. Si apuntas a la raíz, expones el .env, el código y todo. Este es un error de configuración clásico y grave en Laravel.


13.8 · Apache (lo esencial)

Apache sigue muy vivo, sobre todo con PHP. Su equivalente al reverse proxy:

apache
# /etc/apache2/sites-available/miapp.conf
<VirtualHost *:80>
    ServerName api.misitio.com

    ProxyPreserveHost On
    ProxyPass / http://127.0.0.1:8000/
    ProxyPassReverse / http://127.0.0.1:8000/
</VirtualHost>

Su característica distintiva: el archivo .htaccess (config por carpeta, sin reiniciar el servidor). Laravel trae uno en public/ para reescribir URLs:

apache
# public/.htaccess (viene con Laravel)
<IfModule mod_rewrite.c>
    RewriteEngine On
    RewriteCond %{REQUEST_FILENAME} !-d
    RewriteCond %{REQUEST_FILENAME} !-f
    RewriteRule ^ index.php [L]
</IfModule>

Módulos clave a habilitar: a2enmod rewrite proxy proxy_http ssl headers.

🧠 Nginx vs Apache config: Nginx centraliza todo en archivos que se recargan a propósito (nginx -s reload); Apache permite .htaccess por carpeta (cómodo en hosting compartido, pero más lento porque los relee en cada petición). Filosofías distintas.


13.9 · Nginx en Docker (juntándolo todo)

En un stack dockerizado, Nginx es otro servicio del compose.yaml que hace de proxy a tu API:

yaml
services:
  nginx:
    image: nginx:1.30-alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
      - ./certs:/etc/nginx/certs:ro
    depends_on:
      - api

  api:
    build: .
    expose:
      - "8000"          # visible solo dentro de la red de Docker, no al exterior
nginx
# nginx.conf
server {
    listen 80;
    location / {
        proxy_pass http://api:8000;    # "api" = nombre del servicio en Compose
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}

🧠 Fíjate: la API usa expose (no ports), así solo Nginx puede alcanzarla; el mundo exterior nunca habla directo con tu app. Nginx es la única puerta. Eso es arquitectura segura por diseño.


13.10 · Comandos y checklist

bash
sudo nginx -t                  # comprobar que la config es válida (¡SIEMPRE antes de recargar!)
sudo nginx -s reload           # recargar sin cortar conexiones
sudo systemctl restart nginx   # reiniciar
sudo tail -f /var/log/nginx/error.log      # ver errores en vivo
sudo tail -f /var/log/nginx/access.log     # ver tráfico

Checklist de producción:

  1. root apunta a public/, nunca a la raíz del proyecto.
  2. ✅ HTTPS con redirección forzada de HTTP → HTTPS.
  3. ✅ Cabeceras de seguridad (HSTS, X-Frame-Options…).
  4. server_tokens off (ocultar versión).
  5. ✅ Rate limiting en endpoints sensibles (login, API).
  6. ✅ gzip y caché de estáticos activados.
  7. nginx -t antes de cada reload.
  8. ✅ Logs monitorizados.

✅ Ejercicio del capítulo

Pon Nginx delante de una de tus APIs dockerizadas (cap. 12):

1. compose.yaml con nginx + tu api + postgres. La api solo con "expose".
2. Nginx como reverse proxy a la api por el nombre del servicio.
3. Sirve un archivo estático (una página de estado) directamente desde Nginx.
4. Añade las cabeceras de seguridad y server_tokens off.
5. Configura rate limiting en /api/login (ej. 5 req/s).
6. (Con dominio real) genera HTTPS con Certbot y fuerza HTTP→HTTPS.
7. Rompe la config a propósito y usa `nginx -t` para verlo; luego arréglalo.

Tu app ya está lista para recibir al mundo de forma segura y rápida. Último paso: llevarla a un servidor real y mantenerla viva. DevOps.

💡 Pistas de la solución
  • Para el paso 1: la API debe usar expose en compose.yaml, nunca ports — así solo Nginx (dentro de la misma red de Docker) puede alcanzarla; verifica que curl localhost:8000 desde tu máquina host FALLA, y que funciona a través de Nginx en el puerto 80.
  • Al probar el rate limiting del paso 5, usa ab -n 50 -c 10 o un bucle de curl rápido contra /api/login y observa las respuestas 503 una vez superado el límite.
  • Rompe la config del paso 7 quitando un punto y coma o cerrando mal una llave {nginx -t debe señalar la línea exacta del error antes de que intentes recargar.

🧠 Autoevaluación

Un proxy normal representa al cliente (oculta quién navega, típico de una VPN o proxy corporativo). Un reverse proxy representa al servidor: el mundo exterior solo ve Nginx, nunca tu aplicación directamente — Nginx es quien decide a qué backend reenviar cada petición.

La raíz del proyecto contiene el archivo .env con secretos, el código fuente completo y carpetas que no deben ser accesibles por HTTP. Solo public/ está pensada para servirse directamente — apuntar a la raíz expone archivos sensibles a cualquiera que conozca la URL.

El rate limiting de Nginx bloquea peticiones abusivas ANTES de que lleguen a tu aplicación, ahorrando recursos del proceso backend (conexiones a BD, CPU). El de tu framework puede ser más granular (por usuario autenticado, por endpoint específico). Usar ambos es defensa en capas: Nginx filtra el grueso del abuso, tu app afina el resto.

nginx -t valida la sintaxis de la configuración sin aplicarla. Si recargas directamente con una config rota, Nginx puede fallar al recargar y dejar el servidor sin responder — validar antes evita tumbar producción por un error tipográfico en el archivo de configuración.

En HTTP/1.1 el navegador abre varias conexiones (o serializa peticiones) para no bloquearse esperando una respuesta lenta. HTTP/2 multiplexa muchas peticiones sobre una sola conexión TCP, eliminando ese bloqueo a nivel de petición — pero un paquete TCP perdido sigue bloqueando TODAS las peticiones multiplexadas de esa conexión. HTTP/3, al correr sobre QUIC/UDP, hace que la pérdida de un paquete solo afecte al stream al que pertenece, no a los demás.


Siguiente: 14-servidores-devops.md — del git push a producción.