← Back to guides

Odoo deployment

How to host Odoo on a subdomain with HTTPS

Applies to Odoo 15 to 20, self-hosted (Community or Enterprise).

This guide shows how to expose a self-hosted Odoo instance safely through a dedicated subdomain using DNS, Nginx, HTTPS, proxy headers, websocket routing, and database manager protection.

Target architecture

Odoo should stay private on local ports. Public traffic should reach Nginx first, and Nginx should proxy trusted requests to Odoo.

Browser
  → https://odoo.example.com
  → Nginx on 80/443
  → 127.0.0.1:8069 Odoo
  → 127.0.0.1:8072 Odoo websocket
  → private PostgreSQL

Read this before changing Nginx or DNS

This guide assumes the server has already been and Odoo is already running locally. Do not expose Odoo directly on just to make it reachable. Keep Odoo local, put in front, and issue for the subdomain.
Before changing production , keep an active open, confirm you have , and make sure you have a or .

Planning

Design decisions to make before changing DNS, Nginx, HTTPS, or public access.

DNS change

Domain records that affect where the subdomain points. DNS may take time to propagate.

Safe check

Read-only commands or manual checks that should not change the server state.

Caution

Checks that may reveal secrets or require careful interpretation before taking action.

Change required

Actions that can affect DNS, HTTPS, Nginx, access, or production availability. Plan before applying.

Deployment checklist

1. Choose a dedicated Odoo subdomain

Planning

Odoo should be exposed through a clean HTTPS subdomain instead of directly through port 8069. This keeps the public entry point controlled by Nginx.

Show command and interpretation

Command or manual check

Recommended example:

odoo.example.com

Avoid exposing Odoo directly through:
http://your-server-ip:8069

How to interpret the result

The public URL should be a normal HTTPS domain.

Good:
- https://odoo.example.com

Avoid:
- http://your-server-ip:8069
- http://your-server-ip:8072
- public Docker bindings such as 0.0.0.0:8069

2. Create the DNS record

DNS change

The subdomain must point to the public IP address of the server that will host Odoo before Certbot can issue an HTTPS certificate.

Show command and interpretation

Command or manual check

# In your DNS provider, create:

Type: A
Name: odoo
Value: YOUR_SERVER_PUBLIC_IP
TTL: Auto or default

# Then verify from your computer or server:
dig +short odoo.example.com

# Optional: compare with public DNS resolvers
dig @8.8.8.8 +short odoo.example.com
dig @1.1.1.1 +short odoo.example.com

# If the public DNS resolvers return the right IP
# but the local server still returns nothing or an old value:
sudo resolvectl flush-caches
sudo systemctl restart systemd-resolved

# Then test again:
dig +short odoo.example.com

How to interpret the result

The command should return the public IP address of the server that will host Odoo.

Good result:
- YOUR_SERVER_PUBLIC_IP

If it returns nothing, the old IP, or a different IP:
- wait for DNS propagation
- confirm the DNS zone is the correct one
- confirm there is no conflicting CNAME or A record
- confirm you are editing the DNS provider actually used by the domain
- compare local DNS with public resolvers such as 8.8.8.8 and 1.1.1.1

If public resolvers return the correct IP but the server still returns nothing, the server may have a stale local DNS cache. Flushing systemd-resolved can fix this on Ubuntu-based servers.

If Odoo is hosted on an office/on-premise server instead of a VPS:
- the public IP must route to that server or firewall
- ports 80 and 443 must be forwarded to the reverse proxy
- the server should still avoid exposing Odoo 8069, Odoo 8072, or PostgreSQL 5432 directly

3. Confirm Odoo is only listening locally

Safe check

Before exposing Odoo through Nginx, confirm Odoo and PostgreSQL are not already exposed directly to the internet.

Show command and interpretation

Command or manual check

sudo ss -tulpn | grep -E ":(8069|8072|5432)"

docker ps --format "table {{.Names}}\t{{.Ports}}"

How to interpret the result

Safer examples:
- 127.0.0.1:8069
- 127.0.0.1:8072
- PostgreSQL internal Docker network only
- PostgreSQL on 127.0.0.1 only

Risky examples:
- 0.0.0.0:8069
- 0.0.0.0:8072
- 0.0.0.0:5432
- 0.0.0.0:8069->8069/tcp in Docker

If Odoo is already public, fix the binding before continuing.

4. Confirm local Odoo responds

Safe check

Nginx should proxy to a working local Odoo service. Test Odoo locally before debugging DNS or HTTPS.

Show command and interpretation

Command or manual check

curl -I http://127.0.0.1:8069/

curl -I http://127.0.0.1:8069/web/database/manager

How to interpret the result

The local Odoo homepage may return 200, 303, or another normal Odoo response.

The database manager route may exist locally. That is not automatically dangerous if Odoo is local-only.

The important goal is:
- local Odoo works
- public direct port 8069 is not exposed
- public access will go through Nginx only

5. Create the Nginx reverse proxy config

Change required

Nginx will become the public HTTPS entry point and forward traffic to local Odoo services.

Show command and interpretation

Command or manual check

sudo nano /etc/nginx/sites-available/odoo.example.com

How to interpret the result

Create a dedicated Nginx site for the Odoo subdomain.

Do not put Odoo inside the main website or application Nginx config unless you intentionally want them coupled.

Use a separate server block so Odoo can be managed, secured, tested, and disabled independently from the main application.

6. Example Nginx config for Odoo

Change required

This config follows the structure of Odoo's official Nginx sample: normal traffic to 8069, websocket traffic to 8072, forwarded headers for proxy mode, and sensitive database manager routes blocked publicly.

Show command and interpretation

Command or manual check

# Upgrade header mapping for websockets (outside the server block).
# Declare it once per Nginx server: if another site already has this map,
# nginx -t reports a duplicate variable; keep a single copy.
map $http_upgrade $connection_upgrade {
    default upgrade;
    ''      close;
}

server {
    listen 80;
    listen [::]:80;

    server_name odoo.example.com;

    client_max_body_size 100M;

    proxy_read_timeout 720s;
    proxy_send_timeout 720s;
    # Nginx cannot wait longer than about 75s to connect, so keep this short.
    proxy_connect_timeout 60s;

    # Certbot will later add the HTTPS configuration and the HTTP redirect.
    location / {
        proxy_pass http://127.0.0.1:8069;

        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-Host $http_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_redirect off;
    }

    # Odoo 16 and later: websocket route, served on gevent_port (default 8072)
    # when workers > 0. With workers = 0, point it to 8069 instead.
    # Odoo 15 and earlier: rename this location to /longpolling (port set by
    # longpolling_port, default 8072).
    location /websocket {
        proxy_pass http://127.0.0.1:8072;

        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $connection_upgrade;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-Host $http_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;
    }

    # Extra protection for sensitive Odoo database manager routes
    location ~* ^/web/database/(manager|selector|create|drop|backup|restore|duplicate) {
        return 403;
    }

    # Optional: also block the XML-RPC "db" service (create, drop, dump, restore, list).
    # This can break external tools that call db.list or db.server_version.
    # location ~ ^/xmlrpc(/2)?/db$ {
    #     return 403;
    # }
}

How to interpret the result

Important points:
- public traffic goes to Nginx
- Nginx proxies Odoo to 127.0.0.1:8069
- websocket traffic goes to 127.0.0.1:8072
- sensitive /web/database routes are blocked publicly
- proxy headers, including X-Forwarded-Host, are set so Odoo understands HTTPS, the public host name, and client IPs
- client_max_body_size allows normal Odoo uploads, imports, and attachments without hitting Nginx's small default upload limit

Websocket / longpolling by Odoo version:
- Odoo 16 and later: the route is /websocket and the port setting in odoo.conf is gevent_port (default 8072)
- Odoo 15 and earlier: the route is /longpolling and the port setting is longpolling_port (default 8072)
- the separate 8072 port is only used when workers > 0 (multi-process mode)
- with workers = 0, Odoo serves these routes on 8069, so proxy them to 8069 or Nginx will return 502 for live updates
- symptom of a wrong route or port: chat, notifications, and live updates stop working, and the browser console shows websocket errors

Blocking /web/database is not the real control:
- the same database operations are available through the "db" service of Odoo's external API: /xmlrpc/2/db (and the older /xmlrpc/db), and /jsonrpc with service "db"
- with list_db = False, Odoo refuses the database management calls (create, duplicate, drop, dump, restore) on those endpoints; with list_db = True, they only require the master password
- the real control is a strong master password plus list_db = False
- the commented xmlrpc block adds an optional layer; /jsonrpc cannot be filtered by path without blocking the whole JSON-RPC API

This assumes Odoo has proxy_mode = True in odoo.conf. Step 8 checks it before HTTPS is enabled.

7. Enable the Nginx site

Change required

After creating the config, enable it with a symlink and test Nginx before reloading.

Show command and interpretation

Command or manual check

sudo ln -s /etc/nginx/sites-available/odoo.example.com /etc/nginx/sites-enabled/odoo.example.com

sudo nginx -t

sudo systemctl reload nginx

How to interpret the result

Expected:
- nginx -t returns successful
- reload does not disconnect SSH
- http://odoo.example.com reaches Odoo or redirects later after HTTPS setup

If nginx -t fails, do not reload until the syntax error is fixed.

If nginx -t reports a duplicate "connection_upgrade" variable, another site on this server already defines the same map. Remove the map block from one of the two files.

8. Verify Odoo configuration for proxy mode

Caution

Before enabling HTTPS, Odoo should know it is behind a trusted reverse proxy so HTTPS, cookies, and generated URLs behave correctly from the first login.

Show command and interpretation

Command or manual check

# Go to the folder where your Odoo deployment is managed.
# Example for a Docker Compose install:
cd /opt/odoo-secure

# Review the important proxy/security settings without printing the master password.
grep -E "^(proxy_mode|list_db|dbfilter|admin_passwd)" config/odoo.conf \
  | sed -E 's/^(admin_passwd|db_password)[[:space:]]*=.*/\1 = ***hidden***/'

# If you changed odoo.conf, restart Odoo.
sudo docker compose restart odoo

# Re-test through Nginx (plain HTTP for now; HTTPS is added in the next step).
curl -I http://odoo.example.com/

# Re-test public database manager protection.
curl -I http://odoo.example.com/web/database/manager

How to interpret the result

Recommended:
- proxy_mode = True
- list_db = False
- dbfilter = ^your_database_name$
- admin_passwd is strong and not printed publicly

Why proxy_mode needs a private Odoo:
- with proxy_mode = True, Odoo trusts the X-Forwarded-* headers it receives
- this is only safe when Odoo is reachable through Nginx alone (step 3), otherwise a client could send forged headers directly

The command should be executed from the folder where your Odoo deployment is managed.

Examples:
- /opt/odoo-secure for a dedicated Docker Compose deployment
- another project folder if docker-compose.yml and config/odoo.conf are stored elsewhere
- /etc/odoo/odoo.conf for some package-based Linux installs

If you changed odoo.conf:
- restart the Odoo container or service
- re-test the homepage through Nginx
- re-test that /web/database routes are blocked or restricted publicly

For Docker Compose:
- sudo docker compose restart odoo

For a package-based Linux install, the service may be:
- sudo systemctl restart odoo
- sudo systemctl restart odoo-server

Good result through Nginx:
- http://odoo.example.com/ returns 200 OK, 303, or another normal Odoo response

Good database manager protection result:
- http://odoo.example.com/web/database/manager returns 403, 404, or another intentionally restricted response

9. Issue HTTPS certificate with Certbot

Change required

HTTPS should be enabled before real users log in to Odoo. Certbot can configure Nginx automatically.

Show command and interpretation

Command or manual check

sudo certbot --nginx -d odoo.example.com

# Confirm automatic renewal is scheduled (systemd timer, snap timer, or cron)
systemctl list-timers | grep -i certbot || ls /etc/cron.d/certbot

# Simulate a renewal without changing the real certificate
sudo certbot renew --dry-run

How to interpret the result

Certbot should:
- verify DNS points to the VPS
- issue a certificate
- update Nginx config for HTTPS
- optionally redirect HTTP to HTTPS

Renewal check:
- a certbot timer (certbot.timer or snap.certbot.renew.timer) or /etc/cron.d/certbot should exist
- certbot renew --dry-run should end with "Congratulations, all simulated renewals succeeded"
- if the dry run fails, fix it now: Let's Encrypt certificates are short-lived, so a broken renewal breaks HTTPS within weeks

If Certbot fails:
- confirm DNS resolves to the VPS
- confirm ports 80 and 443 are open
- confirm Nginx is running
- confirm the domain is not proxied incorrectly by a DNS/CDN provider
- if validation still fails, the catch-all proxy may intercept the ACME challenge; make sure /.well-known/acme-challenge/ is served locally and not proxied to Odoo

10. Test HTTPS and redirect behavior

Safe check

After Certbot, confirm the subdomain works over HTTPS and HTTP no longer serves insecure Odoo sessions.

Show command and interpretation

Command or manual check

curl -I http://odoo.example.com/

curl -I https://odoo.example.com/

How to interpret the result

Expected:
- HTTP should return 301 or 308 and redirect to HTTPS
- HTTPS should return 200, 303, or another normal Odoo response
- the certificate should be valid in the browser

Good examples:
- http://odoo.example.com/ returns 301 or 308 with Location: https://odoo.example.com/
- https://odoo.example.com/ returns 200 OK or a normal Odoo redirect
- the browser shows a valid HTTPS certificate

What would be bad:
- HTTP serves Odoo directly without redirecting to HTTPS
- HTTPS fails with certificate errors
- HTTPS returns 502 or 504, which usually means Nginx cannot reach Odoo locally
- HTTPS returns 403 on the main Odoo homepage, unless you intentionally restricted the whole site
- the browser shows mixed content or wrong generated URLs

Important:
- 403 is bad for the main Odoo homepage
- 403 is good for sensitive /web/database routes if you intentionally block them publicly

If HTTPS works but Odoo shows wrong URLs or mixed content, check:
- proxy_mode = True in odoo.conf (step 8)
- X-Forwarded-Proto header in Nginx
- X-Forwarded-Host / Host headers in Nginx
- Odoo was restarted after config changes

11. Test database manager protection publicly

Safe check

The Odoo database manager is sensitive. Public access should be blocked or intentionally restricted.

Show command and interpretation

Command or manual check

curl -I https://odoo.example.com/web/database/manager

curl -I https://odoo.example.com/web/database/selector

curl -I https://odoo.example.com/web/database/create

How to interpret the result

Target public result:
- 403 Forbidden
- or another intentionally blocked/restricted result

Avoid public 200 OK on database manager routes unless you fully understand and accept the risk.

Even if these routes are blocked in Nginx:
- keep admin_passwd strong
- keep list_db = False for production-style setups
- keep dbfilter configured

These routes are only the web interface. The same operations exist on the external API's db service (/xmlrpc/2/db and /jsonrpc), which is why list_db = False and a strong master password remain the real protection (see step 6).

12. Final public exposure check

Safe check

Before considering Odoo online, verify only intended public ports are reachable and Odoo/PostgreSQL remain private.

Show command and interpretation

Command or manual check

sudo ss -tulpn | grep -E ":(22|80|443|5432|8069|8072)"

sudo ufw status verbose

sudo docker ps --format "table {{.Names}}\t{{.Ports}}"

How to interpret the result

Good final state:
- 22 is public only if SSH is needed
- 80 and 443 are public for Nginx
- 8069 is bound to 127.0.0.1 only
- 8072 is bound to 127.0.0.1 only
- PostgreSQL is not public
- Docker does not publish PostgreSQL publicly
- firewall does not allow 8069, 8072, or 5432 publicly

Final verification

  • ✓https://odoo.example.com opens Odoo successfully.
  • ✓HTTP redirects to HTTPS.
  • ✓The certificate is valid in the browser.
  • ✓Odoo 8069 is not publicly exposed.
  • ✓Odoo 8072 is not publicly exposed.
  • ✓PostgreSQL 5432 is not publicly exposed.
  • ✓/web/database routes are blocked or intentionally restricted.
  • ✓proxy_mode is enabled in the active Odoo configuration.
  • ✓Nginx test passes with nginx -t.
  • ✓Certbot renewal is scheduled and certbot renew --dry-run succeeds.

Need help exposing Odoo safely?

If you want to put Odoo behind Nginx, HTTPS, and a clean subdomain without exposing port 8069, port 8072, PostgreSQL, or database manager routes publicly, I can help review or set up the deployment.

I can also help troubleshoot common deployment issues such as wrong proxy headers, broken websocket routing, Certbot failures, HTTP not redirecting to HTTPS, or Odoo generating incorrect URLs.

Contact me for Odoo HTTPS setup →

Related guide

Before hosting Odoo publicly, first make sure the underlying VPS is hardened and audited.

How to secure Odoo on a VPS →