Introducción
Cuando desplegamos una aplicación Python en producción —ya sea una API con FastAPI/Orionis/Litestar, una app con Django/Orionis o cualquier framework ASGI— casi nunca exponemos el servidor directamente a Internet. En su lugar, colocamos delante un reverse proxy: un servidor que recibe las peticiones del cliente, las redirige a nuestra aplicación y devuelve la respuesta al cliente.
Este patrón es tan común que herramientas como Nginx y Caddy se han convertido en piezas estándar del stack de producción. Sin embargo, configurar mal el reverse proxy genera problemas sutiles y difíciles de depurar: IPs de cliente incorrectas, bucles de redirección HTTPS, cookies que no se envían o WebSockets que no funcionan.
En este artículo verás qué es exactamente un reverse proxy, qué headers debes conocer y cómo configurarlo correctamente para servidores asíncronos como Uvicorn y Granian.
¿Qué es un Reverse Proxy?
Un proxy directo (forward proxy) actúa en nombre del cliente: el cliente le pide al proxy que busque un recurso en su nombre. Un reverse proxy, en cambio, actúa en nombre del servidor: el cliente cree que habla directamente con el servidor, pero en realidad habla con el proxy.
Cliente (navegador)
│
▼
Reverse Proxy ◄──── TLS, rate limiting, caché, balanceo de carga
(Nginx / Caddy)
│
▼
Servidor ASGI
(Uvicorn / Granian)
¿Por qué usarlo?
- Terminación TLS: el proxy gestiona el certificado SSL/TLS. El servidor ASGI recibe tráfico HTTP plano en la red interna.
- Balanceo de carga: distribuye peticiones entre varias instancias del servidor.
- Caché y compresión: sirve recursos estáticos sin tocar Python.
- Seguridad: oculta la infraestructura interna y aplica reglas de firewall, rate limiting y WAF.
- WebSocket y HTTP/2: el proxy puede negociar protocolos avanzados con el cliente.
Los headers que debes conocer
Cuando el reverse proxy reenvía una petición a tu aplicación, la petición ya no proviene directamente del cliente original. Esto rompe varios supuestos que el código asume por defecto. Para corregirlo, el proxy añade —o debe añadir— una serie de headers de reenvío.
X-Forwarded-For
Contiene la dirección IP real del cliente (y opcionalmente la cadena de proxies intermedios):
X-Forwarded-For: 203.0.113.42, 10.0.0.1
El primer valor es la IP del cliente final. Tu aplicación debe leer este header para conocer la IP real, pero solo si confías en el proxy. De lo contrario, cualquier cliente puede falsificarlo.
X-Forwarded-Proto
Indica el protocolo que usó el cliente para conectarse al proxy (http o https):
X-Forwarded-Proto: https
Sin este header, tu aplicación generará URLs con http:// aunque el cliente se conectó por HTTPS, rompiendo redirects y cookies Secure.
X-Forwarded-Host
El Host original enviado por el cliente:
X-Forwarded-Host: mi-app.ejemplo.com
Útil cuando el proxy está configurado en un dominio diferente al que escucha el servidor interno.
X-Real-IP
Alternativa simplificada a X-Forwarded-For usada por Nginx por defecto. Contiene únicamente la IP del cliente:
X-Real-IP: 203.0.113.42
Forwarded (RFC 7239)
El estándar moderno que unifica los headers anteriores:
Forwarded: for=203.0.113.42;proto=https;host=mi-app.ejemplo.com
Aunque RFC 7239 es el estándar, X-Forwarded-* sigue siendo más común en la práctica.
Headers para WebSockets
El protocolo WebSocket se inicia con un upgrade HTTP. El proxy debe reenviar:
Connection: Upgrade
Upgrade: websocket
Sin ellos, la conexión WebSocket fallará silenciosamente.
Configuración con Nginx
Instalación básica
sudo apt install nginx
Configuración para Uvicorn / Granian
# /etc/nginx/sites-available/mi-app
upstream asgi_server {
# Uvicorn escuchando en socket Unix (más eficiente que TCP)
server unix:/run/mi-app/uvicorn.sock;
# O en TCP:
# server 127.0.0.1:8000;
}
server {
listen 80;
server_name mi-app.ejemplo.com;
# Redirigir todo el tráfico HTTP a HTTPS
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
server_name mi-app.ejemplo.com;
ssl_certificate /etc/letsencrypt/live/mi-app.ejemplo.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/mi-app.ejemplo.com/privkey.pem;
# --- Headers de reenvío ---
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;
# --- Soporte para WebSockets ---
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
# --- Timeouts razonables para apps async ---
proxy_read_timeout 300s;
proxy_send_timeout 300s;
proxy_connect_timeout 10s;
# --- Buffers (deshabilitar para streaming / SSE) ---
proxy_buffering off;
location / {
proxy_pass http://asgi_server;
}
# Archivos estáticos servidos directamente por Nginx
location /static/ {
alias /var/www/mi-app/static/;
expires 30d;
add_header Cache-Control "public, immutable";
}
}
Configuración con Caddy
Caddy es una alternativa moderna que gestiona TLS automáticamente vía Let's Encrypt:
# /etc/caddy/Caddyfile
mi-app.ejemplo.com {
reverse_proxy unix//run/mi-app/uvicorn.sock {
# Headers de reenvío (Caddy los añade automáticamente,
# pero puedes sobreescribirlos)
header_up X-Real-IP {remote_host}
header_up X-Forwarded-For {remote_host}
header_up X-Forwarded-Proto {scheme}
# Soporte WebSocket (automático en Caddy)
transport http {
read_buffer 4096
write_buffer 4096
}
}
# Archivos estáticos
handle /static/* {
root * /var/www/mi-app
file_server
}
}
Configuración del servidor ASGI
Uvicorn
Lanza Uvicorn indicando que está detrás de un proxy. El flag --proxy-headers hace que Uvicorn lea X-Forwarded-For y X-Forwarded-Proto y los exponga correctamente en el scope ASGI:
uvicorn app.main:app \
--host 0.0.0.0 \
--port 8000 \
--proxy-headers \
--forwarded-allow-ips="127.0.0.1" \
--workers 4
--proxy-headers: activa la lectura de headers de reenvío.--forwarded-allow-ips: lista de IPs de proxies en los que confiar. Usa*solo en entornos completamente controlados.
Con socket Unix (más eficiente):
uvicorn app.main:app \
--uds /run/mi-app/uvicorn.sock \
--proxy-headers \
--forwarded-allow-ips="*"
Granian
Granian es un servidor ASGI/RSGI escrito en Rust, con soporte nativo para HTTP/2 y HTTP/3. Su configuración es similar:
granian --interface asgi \
--host 127.0.0.1 \
--port 8000 \
--workers 4 \
--threading-mode workers \
app.main:app
Granian y proxies: a diferencia de Uvicorn, Granian no interpreta automáticamente los headers
X-Forwarded-*. No existe un flag equivalente a--proxy-headersni a--forwarded-allow-ips. Esto significa que la responsabilidad de confiar en el proxy y de extraer la IP real del cliente recae completamente en la aplicación o en el middleware ASGI. Si ejecutas Granian directamente expuesto a Internet, esos headers no estarán presentes y no es necesario gestionarlos. Si lo pones detrás de Nginx o Caddy, debes leerlos explícitamente en tu código (ver la sección siguiente).
Para restringir el host permitido a nivel de middleware con Starlette:
from starlette.applications import Starlette
from starlette.middleware.trustedhost import TrustedHostMiddleware
app = Starlette(...)
# Restringir el host permitido
app.add_middleware(TrustedHostMiddleware, allowed_hosts=["mi-app.ejemplo.com"])
Leer la IP real del cliente en Python
Una vez configurados los headers, acceder a la IP real del cliente es directo:
from starlette.requests import Request
async def mi_endpoint(request: Request):
# Con ProxyHeadersMiddleware activo, client.host ya contiene la IP real
ip_cliente = request.client.host
# O manualmente desde los headers
ip_forwarded = request.headers.get("x-forwarded-for", "").split(",")[0].strip()
return {"ip": ip_cliente, "forwarded": ip_forwarded}
Advertencia de seguridad: nunca confíes en
X-Forwarded-Forsi el tráfico puede llegar al servidor ASGI sin pasar por el proxy. Un cliente malicioso puede inyectar cabeceras falsas. Restringe siempre con--forwarded-allow-ipso su equivalente.
Streaming, Server-Sent Events y WebSockets
Las aplicaciones asíncronas modernas usan con frecuencia respuestas de larga duración. Hay ajustes clave:
Server-Sent Events (SSE)
SSE requiere que el proxy no bufferice la respuesta:
location /eventos {
proxy_pass http://asgi_server;
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 3600s; # 1 hora
add_header X-Accel-Buffering no;
}
WebSockets
El upgrade debe estar explícitamente permitido (ya incluido en la configuración de Nginx anterior). En Caddy, el soporte es automático.
Respuestas de larga duración (streaming JSON, NDJSON)
location /stream {
proxy_pass http://asgi_server;
proxy_buffering off;
proxy_read_timeout 600s;
chunked_transfer_encoding on;
}
Edge, reverse proxy y servidor ASGI
En 2026, la arquitectura real de producción rara vez es de dos capas. Lo más habitual es un stack de tres niveles:
Edge (Cloudflare / CDN / AWS CloudFront)
│ termina TLS global, protección DDoS, caché geográfica
▼
Reverse Proxy (Nginx / Caddy)
│ routing interno, balanceo de carga, headers de reenvío
▼
Servidor ASGI (Granian / Uvicorn)
│ lógica de la aplicación Python
¿Cuándo prescindir del reverse proxy?
Con servidores modernos como Granian —que soporta HTTP/2, HTTP/3 y TLS nativo— es técnicamente posible exponer el servidor directamente al edge o incluso a Internet en escenarios simples:
Edge (Cloudflare en modo proxy)
│
▼
Granian (HTTPS nativo, HTTP/3)
Esto simplifica el stack, elimina un punto de fallo y reduce la latencia. Es una opción válida para servicios de tamaño pequeño o mediano donde el balanceo de carga no es necesario.
Sin embargo, en cuanto necesitas múltiples instancias, routing complejo por path, caché de activos estáticos avanzada o integración con sistemas legacy, el reverse proxy sigue siendo indispensable.
Conclusión
Un reverse proxy sigue siendo una pieza fundamental en arquitecturas de producción, especialmente cuando se requieren capacidades como balanceo de carga, terminación TLS centralizada o routing avanzado. Sin embargo, con servidores modernos como Granian, es posible simplificar el stack en escenarios pequeños o medianos, eliminando el proxy sin perder funcionalidad crítica.
Si decides usarlo, los puntos clave son:
- Siempre reenvía
X-Forwarded-For,X-Forwarded-ProtoyHost. - Confía en el proxy explícitamente en el servidor ASGI con
--forwarded-allow-ipso el middleware equivalente. - Deshabilita el buffering para SSE y streaming.
- Pasa los headers de Upgrade para WebSockets.
- Usa socket Unix cuando el proxy y el servidor corren en la misma máquina: es más rápido que TCP loopback.
Con esta base, ya sea con Nginx o Caddy delante de tu servidor ASGI, o con Granian expuesto directamente al edge, tienes las herramientas para tomar una decisión informada y construir un stack seguro y listo para producción.