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 PostgreSQLRead this before changing Nginx or DNS
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
PlanningOdoo 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:8069How 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:80692. Create the DNS record
DNS changeThe 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.comHow 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 directly3. Confirm Odoo is only listening locally
Safe checkBefore 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 checkNginx 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/managerHow 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 only5. Create the Nginx reverse proxy config
Change requiredNginx 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.comHow 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 requiredThis 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 requiredAfter 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 nginxHow 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
CautionBefore 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/managerHow 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 response9. Issue HTTPS certificate with Certbot
Change requiredHTTPS 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-runHow 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 Odoo10. Test HTTPS and redirect behavior
Safe checkAfter 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 changes11. Test database manager protection publicly
Safe checkThe 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/createHow 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 checkBefore 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 publiclyFinal 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 →