← Blog
odoonginxreverse-proxydeployment

Running Odoo Behind an Nginx Reverse Proxy (proxy_mode)

Put Odoo behind an nginx reverse proxy the right way: TLS termination, proxy_mode in odoo.conf, the X-Forwarded headers, and the websocket port most guides miss.

ODXProxy Team · Jul 6, 2026 · 7 min read

Running Odoo Behind an Nginx Reverse Proxy (proxy_mode) — ODXProxy blog cover

You almost never want to expose Odoo's built-in HTTP server directly to the internet. Running Odoo behind an nginx reverse proxy is the standard production setup: nginx terminates TLS, serves static files fast, buffers slow clients, and hands clean requests to Odoo on the loopback interface. Done right it's a fifteen-minute job. Done wrong you get redirect loops, a site stuck on http://, and live chat that never connects — all from two or three missing lines. This guide walks the whole setup, explains why each directive is there, and flags the proxy_mode and websocket details most tutorials skip.

Why put Odoo behind a proxy at all

Odoo's application server (Werkzeug under the hood) is perfectly capable of speaking HTTP, but it isn't built to be your edge. A reverse proxy in front of it buys you several things at once:

  • TLS termination — nginx holds your certificate and speaks HTTPS to the world; Odoo only ever sees plain HTTP on 127.0.0.1.
  • A single hostname and port — clients hit https://erp.example.com; nginx routes to Odoo's 8069 and its websocket port internally.
  • Static file offloading and buffering — nginx serves assets and shields Odoo workers from slow connections, so a worker isn't tied up drip-feeding bytes to a mobile client.
  • A clean place for rate limits, IP allow-lists, and logs that you don't want living in the app.

The catch is that once a proxy sits in the path, Odoo no longer sees the real scheme, host, or client IP directly — it sees 127.0.0.1 over http. Fixing that is what most of this article is about.

The two ports Odoo actually listens on

The single most common reason "it mostly works but live chat / discuss is broken" is forgetting that Odoo listens on two ports:

  • 8069 — the main HTTP port for the web client and JSON-RPC.
  • 8072 — the longpolling / websocket port (the gevent worker). Odoo 16 and later use a real websocket at /websocket for the bus that powers Discuss, live chat, and real-time notifications.

If you only proxy 8069, pages load but anything real-time silently fails. Your nginx config has to route the websocket path to 8072 separately. Keep that fact in your head; it explains the second location block below.

A complete, annotated nginx config

Here's a production-shaped virtual host for a single Odoo instance. TLS is terminated at nginx and Odoo runs on the loopback interface.

# Upstreams: Odoo's web port and its websocket (gevent) port.
upstream odoo {
    server 127.0.0.1:8069;
}
upstream odoochat {
    server 127.0.0.1:8072;
}

# Redirect all plain HTTP to HTTPS.
server {
    listen 80;
    server_name erp.example.com;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl;
    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;

    # Tell Odoo the truth about the original request.
    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;
    proxy_set_header X-Forwarded-Host $host;

    # Long ERP requests (big reports, imports) need a generous timeout.
    proxy_read_timeout 720s;
    proxy_connect_timeout 720s;
    proxy_send_timeout 720s;

    # Let nginx buffer responses off the Odoo workers.
    proxy_buffers 16 64k;
    proxy_buffer_size 128k;

    client_max_body_size 100m;   # allow large attachment / import uploads

    # Websocket / longpolling → the gevent port.
    location /websocket {
        proxy_pass http://odoochat;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
    }

    # Everything else → the main HTTP port.
    location / {
        proxy_pass http://odoo;
        proxy_redirect off;
    }

    # Cache Odoo's static assets aggressively.
    location ~* /web/static/ {
        proxy_pass http://odoo;
        proxy_cache_valid 200 90m;
        expires 864000;
    }

    gzip on;
    gzip_types text/css text/javascript application/javascript application/json;
}

Three parts of that config carry the whole setup, and they're worth understanding rather than pasting:

  • The X-Forwarded-* headers are how nginx tells Odoo what the original request looked like — the real client IP (X-Forwarded-For), and crucially the original scheme (X-Forwarded-Proto $scheme). Without the last one, Odoo thinks every request arrived over plain HTTP.
  • The /websocket location routes real-time traffic to 8072 with the HTTP/1.1 Upgrade handshake. Miss this and Discuss/live chat break while everything else looks fine.
  • The timeouts stop nginx from returning a 504 on legitimately long ERP operations. If you've hit gateway errors, see the companion guide on fixing Odoo 502 and 504 errors.

The line that makes or breaks it: proxy_mode

nginx is now sending the right headers, but Odoo ignores them by default for security — any client could forge an X-Forwarded-Proto header if Odoo trusted it blindly. You have to explicitly tell Odoo that it sits behind a trusted proxy. In your odoo.conf:

[options]
proxy_mode = True

With proxy_mode = True, Odoo reads the X-Forwarded-* headers and reconstructs the real scheme, host, and client IP. This is the switch that fixes the classic symptoms:

  • Redirects bouncing you back to http:// (and browsers blocking mixed content).
  • OAuth / SSO callbacks failing because Odoo built the return URL with the wrong scheme.
  • Every request in the log showing 127.0.0.1 instead of the real visitor IP.
Only enable proxy_mode when Odoo is genuinely reachable only through your proxy. If Odoo's 8069 is also exposed to the internet, a client can hit it directly and forge X-Forwarded-* headers to spoof its IP or scheme. Bind Odoo to 127.0.0.1 (or firewall the ports) so the proxy is the only path in.

To bind Odoo to the loopback interface so it can't be reached except through nginx, set these in the same odoo.conf:

[options]
proxy_mode = True
http_interface = 127.0.0.1
http_port = 8069
gevent_port = 8072

Restart Odoo, reload nginx (nginx -t && systemctl reload nginx), and load the site over HTTPS.

Verifying it actually works

Don't trust that a page rendered — check the details that proxy_mode is responsible for:

  1. Scheme: the address bar stays on https:// after logging in and navigating; no mixed-content warnings in the browser console.
  2. Real client IP: in Odoo, developer mode → Settings → Technical → Logging (or your nginx access log) shows real visitor IPs, not 127.0.0.1.
  3. Websockets: open Discuss; in the browser dev tools Network → WS tab you should see a /websocket connection in the 101 Switching Protocols state, not a failing request.

If all three hold, your reverse proxy is correctly configured. If a page hangs or returns a gateway error instead, that's a different failure mode — the 502 / 504 troubleshooting guide walks through upstream connectivity and worker timeouts.

Where an API gateway fits (a different kind of proxy)

Everything above is about the web proxy — the one that puts a browser-facing Odoo on HTTPS. It's worth being precise, because "proxy" gets overloaded: an nginx reverse proxy fronts Odoo's web server, while an API gateway fronts Odoo's programmatic surface for the apps and services that call it.

They solve different problems and often coexist. If you're exposing Odoo to external integrations, mobile apps, or a fleet of client instances, the questions become: who is allowed to call, which methods are they allowed to invoke, and how do you keep per-instance Odoo credentials out of every client? That's the layer ODXProxy handles — a single JSON-RPC endpoint in front of one or more Odoo instances, with one proxy API key for the caller and the Odoo credentials carried per request. It doesn't replace nginx; it sits on the API path, the way nginx sits on the browser path.

Odoo's API speaks JSON-RPC 2.0, not REST, and a response can return HTTP 200 while still carrying an error. If you're building against the API behind whatever proxy, start with authenticating to the Odoo API and the two-secret model.

Where to go next

Putting Odoo behind an nginx reverse proxy comes down to four things: proxy both ports (8069 and the 8072 websocket), forward the X-Forwarded-* headers, flip proxy_mode = True so Odoo trusts them, and bind Odoo to the loopback so the proxy is the only way in.