Blog
Python ASGI Uvicorn Granian Nginx DevOps

Qué es un Reverse Proxy y cómo configurarlo para aplicaciones Python asíncronas

Aprende qué es un reverse proxy, por qué es indispensable para aplicaciones ASGI con Uvicorn o Granian, y cómo gestionar correctamente los headers HTTP en producción.

Raul Mauricio Uñate Castro Raul Mauricio Uñate Castro · 23 de junio de 2026
Qué es un Reverse Proxy y cómo configurarlo para aplicaciones Python asíncronas

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-headers ni 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-For si 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-ips o 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:

  1. Siempre reenvía X-Forwarded-For, X-Forwarded-Proto y Host.
  2. Confía en el proxy explícitamente en el servidor ASGI con --forwarded-allow-ips o el middleware equivalente.
  3. Deshabilita el buffering para SSE y streaming.
  4. Pasa los headers de Upgrade para WebSockets.
  5. 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.