Build an Odoo Compose stack that can survive the first failure.
A production design is more than docker compose up -d. Keep PostgreSQL private, persist both data stores, pin tested images, terminate HTTPS at a reverse proxy, restrict database selection, tune Odoo’s production server, monitor the host, and prove that remote backups restore.

The direct answer: use at least three roles: an HTTPS reverse proxy exposed on 80/443, Odoo on a private application network, and PostgreSQL on a private database network with no published host port. Persist /var/lib/odoo and PostgreSQL separately. Compose describes that topology; production readiness comes from configuration, secret handling, resource policy, monitoring, patching, and tested off-host recovery.
Prepare operations before YAML
- A supported, patched Linux host with restricted key-based administration and a firewall.
- DNS controlled by your organization and a valid certificate renewal path.
- A chosen Odoo 19 build, PostgreSQL version, tested custom addon set, and documented owner for updates.
- Remote encrypted backup storage with separate credentials, retention policy, alerts, and restore exercises.
- Monitoring for application health, logs, certificate expiry, CPU, memory, disk, database growth, cron work, and backup status.
Do not copy a VPS size from a generic blog. Odoo workload depends on concurrent requests, workers, imports, reports, cron jobs, integrations, website traffic, attachments, and custom code. Measure and adjust with headroom.
Separate public traffic, application, database, and recovery
| Layer | Public exposure | Persistent state | Control |
|---|---|---|---|
| Reverse proxy | 80/443 only | Certificates/configuration | TLS, headers, request limits, timeouts |
| Odoo 19 | Private proxy network | /var/lib/odoo filestore | Workers, limits, dbfilter, addons |
| PostgreSQL | Private database network only | PostgreSQL data directory | Non-superuser Odoo role, access policy |
| Backup target | Outbound or isolated backup path | Database + matching filestore | Encryption, retention, immutability, restore tests |
Only the proxy should accept internet requests. Do not publish PostgreSQL. Ideally do not publish Odoo’s 8069 port to all host interfaces either; connect the proxy through the internal network or bind Odoo to loopback if the proxy runs on the host. Avoid mounting the Docker socket into the web-facing proxy or application.
Start from official image behavior, then add controls
Docker Hub’s official Odoo example documents a web service, a separate PostgreSQL service, named volumes, custom configuration at /etc/odoo, custom addons at /mnt/extra-addons, and file-based PostgreSQL password support. The following fragment illustrates the private application/database core, not a complete production file:
services:
web:
image: odoo:19.0
depends_on:
- db
volumes:
- odoo-web-data:/var/lib/odoo
- ./config:/etc/odoo:ro
- ./addons:/mnt/extra-addons:ro
environment:
HOST: db
USER: odoo
PASSWORD_FILE: /run/secrets/postgresql_password
secrets:
- postgresql_password
db:
image: postgres:15
environment:
POSTGRES_DB: postgres
POSTGRES_USER: odoo
POSTGRES_PASSWORD_FILE: /run/secrets/postgresql_password
PGDATA: /var/lib/postgresql/data/pgdata
volumes:
- odoo-db-data:/var/lib/postgresql/data/pgdata
secrets:
- postgresql_password
volumes:
odoo-web-data:
odoo-db-data:
secrets:
postgresql_password:
file: ./secrets/odoo_pg_pass
Keep the secret file outside version control with restrictive permissions. Compose file-based secrets improve accidental exposure but are not an automatic hardware-backed vault. Add explicit internal networks, a reverse proxy, health checks, restart behavior, log rotation, and resource controls appropriate to your Compose/runtime version and tested deployment.
odoo:19.0 avoids an unintended major upgrade, but it can move as new 19.0 builds are published. For reproducible production rollout, record and deploy a tested dated tag or immutable digest, then deliberately test and promote newer current builds. Never advise latest for production.
Harden database routing and server behavior
Set proxy_mode = True only because requests arrive through a trusted reverse proxy. Enabling it on a directly exposed server can make client-address and scheme information trust attacker-controlled forwarding headers.
For a single production database, configure db_name and/or a precise dbfilter that matches only the intended hostname/database. Confirm routing first, then set list_db = False (equivalent startup option: --no-database-list) and block /web/database management routes at the proxy. Odoo strongly recommends disabling Database Manager for internet-facing systems.
Docker’s default Odoo server is multi-threaded, which Odoo describes primarily for development and demonstrations. Production uses a nonzero worker count and configured memory, request, CPU, real-time, and cron limits. Odoo publishes rules of thumb, but custom modules and reports vary dramatically; load-test, monitor, and tune rather than treating a formula as a promise.
Make HTTPS the only public path
Odoo’s deployment guide states that authentication information is transmitted in cleartext without HTTPS. Redirect HTTP to HTTPS, maintain a valid certificate, forward the documented host, scheme, address, and realtime/websocket traffic, and set proxy request-size and timeout limits based on legitimate imports and reports. Test long-running requests without opening unlimited timeouts.
Add HSTS only after HTTPS and renewal are proven; a long HSTS policy can lock users out if certificates fail. Restrict direct access to Odoo, verify generated links use the public HTTPS URL, and test websocket-dependent features. Protect metrics and administrative proxy endpoints from public access.
Assume the host and a backup job can fail
Use a non-superuser PostgreSQL application role and keep port 5432 private. Patch the host, Docker runtime, proxy, PostgreSQL image, and Odoo build. Scan and review custom addons, remove unused services, deny demo data, secure the Odoo master password, rotate credentials deliberately, and keep production separate from developer environments.
A named volume is not a backup. Schedule a PostgreSQL-aware dump or snapshot coordinated with the matching Odoo filestore. Copy encrypted backups to remote storage that compromised application credentials cannot delete. Preserve configuration, custom addon revisions, image digests, and a secrets inventory. Define retention and recovery objectives based on business needs.
Restore tests are mandatory: build an isolated stack, restore both data components, and verify attachment downloads, logins, reports, integrations, cron jobs, and representative transactions. Record time to recovery and fix undocumented steps.
Promote builds; do not mutate blindly
- Record the running image digests, Odoo configuration, database version, and addon revisions.
- Read release/build changes and dependency advisories.
- Create and verify a recoverable backup before maintenance.
- Restore or clone representative data to staging with personal-data controls.
- Deploy the candidate Odoo 19 and PostgreSQL patch images; test install/update paths and workflows.
- Roll production during a communicated window, monitor errors and queues, and retain a tested recovery path.
A major Odoo change requires a database and addon migration; changing 19.0 to a later major tag is not an upgrade plan. PostgreSQL major upgrades also need their own documented method.
Use the failing boundary to choose the next check
- 502/504: inspect proxy and Odoo logs, container health, worker exhaustion, timeouts, and network membership before increasing limits.
- Redirect loop or HTTP links: verify proxy headers and Odoo proxy mode; do not enable proxy mode without the proxy.
- Database unavailable: check PostgreSQL health, private DNS, credentials file permissions, storage space, and role access.
- Attachments missing after restore: the database and filestore are mismatched or the volume is mounted at the wrong path.
- Database selector visible: validate db routing, disable listing, and block management routes after confirming the target database resolves.
Evidence required before launch
- Only 80/443 public; PostgreSQL and 8069 are not directly internet-accessible.
- Valid HTTPS, renewal alert, correct proxy headers, realtime traffic, and safe request limits tested.
- Pinned tested Odoo 19 and PostgreSQL image references recorded; no production
latest. - Database routing restricted, listing disabled, management routes blocked, demo data absent.
- Workers and limits tested under representative load; host and container metrics alert.
- Remote database-plus-filestore backup restored successfully in isolation.
- Owner, maintenance window, incident contacts, update runbook, and recovery runbook documented.
FAQ
Is Docker Compose itself production-ready for Odoo?
Compose can describe a production topology, but a short Compose file is not production readiness. You still need HTTPS, isolation, secrets, database restrictions, workers and limits, monitoring, updates, remote backups, and tested recovery.
Should Odoo proxy mode always be enabled in Docker?
No. Enable proxy_mode only when Odoo is behind a trusted reverse proxy that sets the required forwarding headers. Do not enable it merely because Odoo runs in a container.
What must an Odoo Compose backup contain?
It must contain a recoverable PostgreSQL database and the matching Odoo filestore, plus the configuration, custom addon revisions, image references, and protected secrets information needed to rebuild the stack.
Build from the right starting point
Want a managed stack, not a mystery YAML file?
Odin offers transparent one-click and managed Odoo deployment. We document image policy, data location, proxy, backups, updates, monitoring, and responsibility boundaries before go-live.