🎯 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
| Nginx | Apache | |
|---|---|---|
| Arquitectura | Asíncrona (event-driven) | Procesos/hilos por conexión |
| Rendimiento con mucha concurrencia | ✅ Excelente | ⚠️ Consume más |
| Archivos estáticos | ✅ Rapidísimo | Bueno |
| Configuración | En archivos centrales | .htaccess por carpeta (flexible) |
| Uso típico 2026 | Reverse proxy, la opción por defecto | Hosting 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):
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):
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):
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:
sudo apt install certbot python3-certbot-nginx
sudo certbot --nginx -d misitio.com -d www.misitio.comCertbot 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í:
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):
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):
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)
# 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:
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
rootapunta apublic/, 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:
# /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:
# 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.htaccesspor 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:
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.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(noports), 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
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áficoChecklist de producción:
- ✅
rootapunta apublic/, nunca a la raíz del proyecto. - ✅ HTTPS con redirección forzada de HTTP → HTTPS.
- ✅ Cabeceras de seguridad (HSTS, X-Frame-Options…).
- ✅
server_tokens off(ocultar versión). - ✅ Rate limiting en endpoints sensibles (login, API).
- ✅ gzip y caché de estáticos activados.
- ✅
nginx -tantes de cadareload. - ✅ 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
exposeencompose.yaml, nuncaports— así solo Nginx (dentro de la misma red de Docker) puede alcanzarla; verifica quecurl localhost:8000desde 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 10o un bucle decurlrápido contra/api/loginy observa las respuestas503una vez superado el límite. - Rompe la config del paso 7 quitando un punto y coma o cerrando mal una llave
{—nginx -tdebe 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.