Ir al contenido

Configurando Odoo Detrás de un Proxy Inverso: La Guía Completa de proxy_mode

Configurando Odoo Detrás de un Proxy Inverso: La Guía Completa de proxy_mode
Benjamin Akboka Apengu
agosto 6, 2026 · 11 min leído


Hacer que Odoo funcione detrás de nginx o Cloudflare es un trabajo de diez minutos. Hacer que funcione correctamente es otro asunto, y la brecha entre los dos es inusualmente silenciosa: Odoo servirá páginas, los usuarios iniciarán sesión, nada aparecerá en los registros, y un puñado de cosas estarán sutilmente mal durante meses.

Si estás aquí porque tu sitio funciona pero las URL están mal en algún lugar (correos electrónicos que enlazan a http://, un mapa del sitio que no va a https, hreflang que nunca se renderiza, la IP incorrecta en cada línea de registro), esta es casi siempre la misma causa única.

La versión corta

Configurar proxy_mode = True en odoo.conf no hace nada por sí solo. Odoo solo lo respeta cuando la solicitud entrante ya lleva un encabezado X-Forwarded-Host. Si te pierdes ese encabezado, la configuración se ignora por completo, incluyendo X-Forwarded-Proto, que es el que la mayoría de las guías te dicen que configures.

Necesitas que los tres sean verdaderos:

  1. proxy_mode = True en odoo.conf
  2. Odoo reiniciado, porque es una opción de archivo de configuración, no una configuración en tiempo de ejecución
  3. Tu proxy envía tanto X-Forwarded-Host como X-Forwarded-Proto

Si te pierdes cualquiera de ellos, Odoo construye cada URL absoluta desde la dirección a la que se accedió internamente (http://localhost:8069) en lugar de la dirección que tus usuarios escribieron.

Por qué se requiere el encabezado adicional

Esta es la verificación que Odoo realiza en cada solicitud, en Application.__call__ (odoo/http.py, Odoo 19.0):

if odoo.tools.config['proxy_mode'] and environ.get("HTTP_X_FORWARDED_HOST"):
...
ProxyFix(fake_app)(environ, fake_start_response)

Dos condiciones unidas por y. proxy_mode es tu bandera de configuración; HTTP_X_FORWARDED_HOST es la clave del entorno WSGI para el encabezado X-Forwarded-Host. Si el encabezado está ausente, el middleware nunca se ejecuta, y ningún encabezado reenviado es confiable en absoluto: ni el proto, ni el for, ni el host.

Esto es deliberado, y es el diseño correcto. Los encabezados reenviados son cadenas proporcionadas por el cliente: cualquiera que pueda acceder a Odoo directamente puede afirmar ser cualquier host, desde cualquier IP, a través de cualquier esquema. La ayuda de configuración de Odoo lo dice claramente:

--proxy-mode ... “¡Activa los envoltorios WSGI de proxy inverso (reescritura de encabezados)! Solo habilita esto cuando estés detrás de un proxy web de confianza!” Fuente: odoo/tools/config.py

La presencia de X-Forwarded-Host se utiliza como la señal de que un proxy está genuinamente al frente. No es un límite de seguridad por sí solo (eso debe venir de tu firewall), pero significa que la bandera no puede dejarse activada en un archivo de configuración y comenzar a confiar silenciosamente en encabezados de internet abierto.

La consecuencia práctica es lo que importa: un proxy parcialmente configurado se comporta exactamente como uno no configurado. No hay un estado de medio funcionamiento y no hay advertencia.

En qué confía realmente Odoo

Cuando se abre la puerta, Odoo aplica el ProxyFix de Werkzeug con parámetros fijos:

ProxyFix = functools.partial(ProxyFix_, x_for=1, x_proto=1, x_host=1)

Esa única línea determina todo lo que tu configuración de proxy debe satisfacer:

Encabezado

Reescrituras

¿Confiable?

X-Forwarded-For

REMOTE_ADDR, la IP del cliente que Odoo registra y limita por tasa

1 salto

X-Forwarded-Proto

wsgi.url_scheme, http vs https en cada URL generada

1 salto

X-Forwarded-Host

HTTP_HOST, SERVER_NAME, SERVER_PORT

1 salto

X-Forwarded-Port

n/a

ignorado

X-Forwarded-Prefix

n/a

ignorado

Dos de estos merecen atención.

X-Forwarded-Port no se lee. Odoo toma el puerto del componente de puerto de X-Forwarded-Host en su lugar. Si estás sirviendo en un puerto no estándar, pertenece a ese encabezado (X-Forwarded-Host: erp.example.com:8443), no en X-Forwarded-Port, que Odoo descarta.

X-Forwarded-Prefix tampoco se lee, lo que significa que SCRIPT_NAME nunca se reescribe. Servir Odoo desde una subruta de un sitio más grande (example.com/erp/) no es compatible con este mecanismo, por muy bien que tu proxy reescriba la ruta. Odoo seguirá generando URLs relativas a la raíz. Dale a Odoo su propio nombre de host o subdominio.

El 1 es la parte que duele: IPs reales de clientes

x_for=1 no significa “tomar la primera entrada.” Werkzeug lo resuelve como values[-trusted]: el valor más a la derecha en el encabezado, contando hacia atrás una posición.

Con un solo proxy eso es correcto. Con dos, no lo es silenciosamente.

Una pila muy común es Cloudflare frente a nginx. Cloudflare llega con la IP del visitante en X-Forwarded-For. nginx, configurado de la manera en que casi todos los tutoriales lo escriben, luego agrega su propia vista de la conexión:

proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; # añade: dos saltos ahora

Odoo recibe X-Forwarded-For: 203.0.113.9, 172.68.x.x, toma el valor más a la derecha, y registra la IP de borde de Cloudflare como el cliente. Cada línea de registro, cada registro de inicio de sesión, cada límite de tasa y cada rastro de auditoría ahora apunta a tu CDN en lugar de a un usuario. Nada se rompe; los datos son simplemente incorrectos, y permanecen incorrectos retroactivamente.

Detrás de Cloudflare, entrega a Odoo el visitante directamente:

# Cloudflare pone la verdadera IP del cliente en su propio encabezado
proxy_set_header X-Forwarded-For $http_cf_connecting_ip;

La versión más robusta es el real_ip módulo de nginx con los rangos de IP publicados por Cloudflare en set_real_ip_fromreal_ip_header CF-Connecting-IP, y luego proxy_set_header X-Forwarded-For $remote_addr;. De esa manera $remote_addr ya es el visitante en todas partes en tu configuración de nginx, incluyendo tus propios registros de acceso. Cloudflare publica esos rangos y cambian; obténlos en lugar de copiarlos.

La regla general: Odoo confía exactamente en un salto, así que asegúrate de que el último salto esté diciendo la verdad.

Lo que realmente rompe un proxy mal configurado

Ninguno de estos se anuncia por sí mismo. Esta es la lista para verificar, porque estos síntomas suelen ser reportados como cuatro problemas no relacionados.

Las URL absolutas se construyen a partir del host incorrecto. Cualquier cosa que Odoo genere fuera de la barra de direcciones del navegador (enlaces de restablecimiento de contraseña, invitaciones al portal, enlaces para compartir, og:url, enlaces de informes enviados por correo) se ensambla a partir de HTTP_HOST y el esquema de URL. Host incorrecto, esquema incorrecto, enlace incorrecto. Los usuarios notan esto primero, y típicamente como “el enlace de restablecimiento está roto.”

robots.txt y sitemap.xml emiten http://. El controlador del mapa del sitio construye <loc> valores de url_root. Si wsgi.url_scheme es http, cada URL que envíes a los motores de búsqueda es el esquema incorrecto.

El bloque hreflang del sitio web desaparece por completo. En un sitio multilingüe, este es genuinamente invisible sin ver el código fuente. Odoo controla las etiquetas hreflang en website._is_canonical_url(), que compara:

current_url = request.httprequest.url_root[:-1] + request.httprequest.environ['REQUEST_URI']
canonical_url = self.env['ir.http']._url_localized(..., canonical_domain=self.get_base_url())
return current_url == canonical_url

url_root proviene del entorno WSGI, así que es http:// detrás de un proxy no fijo. get_base_url() devuelve tu dominio configurado, que es https://. Nunca pueden ser iguales, la verificación devuelve False en cada página, y el bloque hreflang se omite en todo el sitio, en un sitio cuyas traducciones están de otro modo perfectamente configuradas.

Ten en cuenta que este no se puede solucionar desde la base de datos. Configurar web.base.url no ayuda, porque nada en los parámetros del sistema puede cambiar url_root. Proviene del entorno WSGI, que es exactamente lo que ProxyFix existe para corregir. Se pierde tiempo regularmente aquí, porque web.base.url es la configuración que suena como si debería ser responsable. (El diagnóstico completo es una publicación propia; enlaza la guía de hreflang aquí al publicar.)

El filtrado de host de múltiples bases de datos selecciona la base de datos incorrecta, o ninguna. db_filter() resuelve %h y %d del entorno:

host = request.httprequest.environ.get('HTTP_HOST', '')

HTTP_HOST es uno de los valores ProxyFix reescribe. Sin él, cada solicitud parece llegar a cualquier dirección interna que tu proxy utilizó, así que un dbfilter basado en el nombre de host coincide con la misma base de datos para cada dominio, o no coincide con nada, y presenta un selector de base de datos que pensaste que habías desactivado. En un host multi-inquilino, esta es la diferencia entre enrutar por dominio y no enrutar en absoluto.

Una peculiaridad de caché que vale la pena conocer. El sitemap se almacena como un ir.attachment basado en un hash de url_root. Corregir el esquema por lo tanto genera un nuevo sitemap en caché inmediatamente en lugar de hacerte esperar el TTL, pero el http:// adjunto se queda en la base de datos. Elimínalo, o seguirás encontrándolo.

Cloudflare: usa Full (estricto), no Flexible

Cloudflare’s Flexible El modo SSL encripta el navegador→Cloudflare y luego se comunica con tu origen a través de HTTP sin cifrado. Se presenta como la opción de cero configuración, y interactúa mal con un origen correctamente asegurado.

El fallo es un bucle de redirección. Tu origen, sensatamente, redirige HTTP a HTTPS. Cloudflare, en modo Flexible, solo se conecta a través de HTTP. Así que: Cloudflare solicita a través de HTTP → el origen devuelve 301 → https:// → Cloudflare solicita de nuevo a través de HTTP → bucle, hasta que el navegador se rinde con ERR_TOO_MANY_REDIRECTS. La reacción habitual es eliminar la redirección HTTPS del origen, lo que resuelve el bucle al hacer que el origen esté permanentemente sin cifrar.

El segundo problema es el que no puedes ver: en modo Flexible, el tráfico entre Cloudflare y tu servidor cruza la internet pública en texto claro, incluidas las cookies de sesión, mientras el navegador muestra un candado.

Usa Completo (estricto) con un certificado válido en el origen. Let’s Encrypt es gratuito, o los certificados CA de origen de Cloudflare se emiten por 15 años y son confiables por Cloudflare específicamente para este propósito.

Websockets y el segundo puerto

Odoo ejecuta su bus en un segundo proceso, en --gevent-port, 8072 por defecto (odoo/tools/config.py). Las solicitudes a /websocket deben llegar a ese puerto, no al 8069, y deben tener permiso para actualizar la conexión.

Si te equivocas en esto, el síntoma es extrañamente específico: la aplicación funciona, pero las funciones en vivo dejan de funcionar silenciosamente: los mensajes de chat y Discuss no llegan, las notificaciones no aparecen, y la consola del navegador se llena de conexiones websocket fallidas. Las operaciones de larga duración que informan progreso parecen colgarse.

Una configuración nginx funcional

# Requerido para actualizaciones de websocket
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;;
}


upstream odoo { server 127.0.0.1:8069; }
upstream odoo_bus { server 127.0.0.1:8072; }


server {
escuchar 443 ssl;
http2 encendido;
server_name erp.example.com;

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

# Los cuatro encabezados. X-Forwarded-Host es el que abre la puerta.
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; # ver la nota de Cloudflare arriba
proxy_set_header X-Real-IP $remote_addr;

# Importaciones largas, grandes informes, generación de PDF
proxy_read_timeout 720s;
proxy_connect_timeout 720s;
proxy_send_timeout 720s;

client_max_body_size 100m;

location / {
proxy_pass http://odoo;
proxy_redirect off;
}

location /websocket {
proxy_pass http://odoo_bus;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
}

# Almacenar en caché durante mucho tiempo los paquetes de activos hasheados de Odoo
location ~* /web/(static|assets)/ {
proxy_pass http://odoo;
proxy_cache_valid 200 302 60m;
expires 864000;
}
}


server {
listen 80;
server_name erp.example.com;
return 301 https://$host$request_uri;
}

Y en odoo.conf:

proxy_mode = True

Luego reinicia Odoo. Se lee solo al inicio.

Cierra la ruta directa. proxy_mode le dice a Odoo que confíe en los encabezados reenviados; solo tu firewall impide que alguien los envíe directamente. Vincula Odoo a loopback (http_interface = 127.0.0.1) o bloquea 8069 y 8072 en el firewall. De lo contrario, cualquiera que pueda alcanzar el origen puede falsificar tanto su IP como su nombre de host.

Verificándolo, en tres comandos

No verifiques esto cargando el sitio en un navegador: el navegador oculta precisamente lo que está mal. Verifica las URL que genera Odoo.

1. La prueba de una línea. El sitemap se construye a partir de url_root, por lo que informa el esquema que Odoo cree que está sirviendo:

curl -s -L https://erp.example.com/sitemap.xml | grep -o '<loc>[^<]*</loc>' | head -3

¿Todavía http://? url_root sigue siendo incorrecto, y nada más vale la pena verificar hasta que eso se solucione.

2. robots.txt, que lleva una directiva de sitemap absoluta:

curl -s https://erp.example.com/robots.txt | grep -i sitemap

3. En un sitio multilingüe, confirma las devoluciones de hreflang, usando curl en lugar de devtools, así que estás leyendo lo que recibe un rastreador en lugar de un DOM renderizado:

curl -s -A "Googlebot" https://erp.example.com/ | grep -o '<link[^>]*hreflang[^>]*>'

La salida vacía en un sitio con más de un idioma activo significa que la comparación canónica sigue fallando.

Vuelve a ejecutar la primera verificación con una cadena de consulta (?utm_source=test). La falla original depende del esquema, y es posible una solución parcial que funcione en URLs limpias.

La parte que no está en el archivo de configuración

Todo lo anterior es un trabajo único que se mantiene correcto hasta que algo cambia: un nuevo certificado, un CDN al frente, una segunda base de datos, un idioma añadido, una migración que restablece un archivo de configuración. Cada uno de esos puede reabrir silenciosamente la misma brecha, y ninguno aparecerá como un error. El modo de falla de la capa de proxy es que sigue funcionando mientras obtiene los detalles incorrectos en silencio, por lo que estos problemas suelen ser encontrados meses después por alguien que audita otra cosa.

Esa es la diferencia real entre Odoo funcionando y Odoo funcionando correctamente. Rara vez es dramático. Es un encabezado que nadie configuró, y se mantiene así hasta que alguien lee la fuente.


Ejecutalo en tu propio servidor, sin tener este problema

Si prefieres no mantener la capa de proxy, las renovaciones de certificados, el enrutamiento de websocket y el encabezado tú mismo: Skysize BYOS se ejecuta en tu servidor, en tu país, bajo tu control, y nosotros manejamos el lado de Odoo, incluyendo todo lo anterior. Construido por exingenieros de Odoo.

Conecta tu servidor. O, si prefieres no ejecutar la máquina tampoco, consulta hosting Odoo gestionado.

Configurando Odoo Detrás de un Proxy Inverso: La Guía Completa de proxy_mode
Benjamin Akboka Apengu
Escribe sobre la infraestructura de Odoo en Skysize, un proveedor de alojamiento Odoo gestionado con sede en Bélgica que atiende a empresas y agencias en todo el mundo.