Running Odoo in Docker for local development is straightforward. Running it in production without the right configuration is how you end up with data loss, silent crashes, and 3 AM outages. This guide gives you a complete, battle-tested docker-compose.yml for production Odoo — with every setting explained.
Why a Minimal docker-compose.yml Is Not Enough
Most tutorials show you the smallest compose file that works. In production, "works on my machine" is not enough. You need:
- Healthchecks — so Docker knows when a container is truly ready, not just started
- Restart policies — automatic recovery when processes crash
- Resource limits — prevent one container from starving the entire host
- Named volumes — persistent storage that survives container replacement
- Logging configuration — structured logs with rotation to prevent disk fill
- Network isolation — PostgreSQL should never be reachable from the internet
Prerequisites
- Docker Engine 24+ and Docker Compose v2
- A Linux server with at least 2 GB RAM (4 GB recommended)
- A domain name pointed at your server IP
- An Nginx or Caddy reverse proxy handling TLS termination (see our Nginx reverse proxy guide)
The Production docker-compose.yml
Create a directory for your project and add this file:
mkdir -p /opt/odoo && cd /opt/odoo
# /opt/odoo/docker-compose.yml
version: "3.9"
services:
db:
image: postgres:15-alpine
restart: unless-stopped
environment:
POSTGRES_DB: odoo
POSTGRES_USER: odoo
POSTGRES_PASSWORD_FILE: /run/secrets/pg_password
secrets:
- pg_password
volumes:
- pg_data:/var/lib/postgresql/data
networks:
- internal
healthcheck:
test: ["CMD-SHELL", "pg_isready -U odoo"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
logging:
driver: "json-file"
options:
max-size: "50m"
max-file: "5"
deploy:
resources:
limits:
cpus: "1.0"
memory: 1G
reservations:
memory: 512M
odoo:
image: odoo:17
restart: unless-stopped
depends_on:
db:
condition: service_healthy
environment:
HOST: db
PORT: 5432
USER: odoo
PASSWORD_FILE: /run/secrets/pg_password
secrets:
- pg_password
volumes:
- odoo_data:/var/lib/odoo
- ./config/odoo.conf:/etc/odoo/odoo.conf:ro
- ./addons:/mnt/extra-addons:ro
networks:
- internal
- proxy
ports:
- "127.0.0.1:8069:8069"
- "127.0.0.1:8072:8072"
healthcheck:
test: ["CMD-SHELL", "curl -fs http://localhost:8069/web/health || exit 1"]
interval: 30s
timeout: 10s
retries: 3
start_period: 60s
logging:
driver: "json-file"
options:
max-size: "100m"
max-file: "10"
deploy:
resources:
limits:
cpus: "2.0"
memory: 2G
reservations:
memory: 1G
volumes:
pg_data:
driver: local
odoo_data:
driver: local
networks:
internal:
driver: bridge
internal: true
proxy:
driver: bridge
secrets:
pg_password:
file: ./secrets/pg_password.txt
Companion odoo.conf
Place this at ./config/odoo.conf:
[options]
addons_path = /usr/lib/python3/dist-packages/odoo/addons,/mnt/extra-addons
data_dir = /var/lib/odoo
db_host = db
db_port = 5432
db_name = odoo
db_user = odoo
; Workers — set to (CPU cores * 2) + 1 for production
workers = 4
max_cron_threads = 1
; Memory limits (bytes)
limit_memory_hard = 2684354560
limit_memory_soft = 2147483648
limit_time_cpu = 600
limit_time_real = 1200
proxy_mode = True
logfile = /var/log/odoo/odoo.log
log_level = warn
See the worker configuration guide for how to calculate the right workers value for your hardware.
Setting Up Secrets
Never put passwords in environment variables that get logged. Use Docker secrets:
mkdir -p ./secrets
openssl rand -base64 32 > ./secrets/pg_password.txt
chmod 600 ./secrets/pg_password.txt
Starting and Initializing
# Start everything
docker compose up -d
# Initialize a new database (first time only)
docker compose exec odoo odoo -d odoo --init=base --stop-after-init
# Watch logs
docker compose logs -f odoo
Healthcheck Details
The depends_on: condition: service_healthy ensures Odoo only starts after PostgreSQL passes its healthcheck. Without this, Odoo frequently crashes on startup because it tries to connect before the database accepts connections.
The Odoo healthcheck hits /web/health — a lightweight endpoint added in Odoo 16 that returns 200 OK without touching the database. For older versions, replace with:
test: ["CMD-SHELL", "curl -fs http://localhost:8069/web/database/selector || exit 1"]
Resource Limits Explained
The deploy.resources block sets both a limit (hard ceiling) and a reservation (guaranteed minimum). Key values:
limit_memory_hardin odoo.conf (2.5 GB) should be slightly below the Docker memory limit (2 GB for the container). Set them to match or the OOM killer will terminate the container before Odoo self-limits.- Set PostgreSQL memory to roughly half your RAM on dedicated database servers. On shared hosts, keep it lower.
Log Rotation
The json-file logging driver with max-size and max-file prevents unbounded log growth. With 100 MB max-size and 10 files, you keep up to 1 GB of Odoo logs — enough for debugging without filling your disk.
To ship logs to a central system, swap the driver:
logging:
driver: "syslog"
options:
syslog-address: "tcp://logs.yourhost.com:514"
How DeployMonkey Handles This For You
Every instance on DeployMonkey runs this kind of hardened Docker configuration by default. You don't write or maintain compose files — we handle healthchecks, restart policies, resource limits tuned to your plan, and log management. When your Odoo crashes, we detect it and restart it automatically. When logs fill up, rotation kicks in without any action from you.
On the Starter plan ($15/month), you get a managed single-container setup. On Professional ($29/month), resource limits are tuned for higher traffic. And if you want to bring your own server, our BYOS plan ($150/month) deploys this exact stack on your infrastructure.
Start your free instance and skip the compose file maintenance entirely.
Frequently Asked Questions
Should I use host networking instead of bridge networks?
No. Bridge networks with explicit port bindings give you the best isolation. Host networking removes the network namespace entirely, which is a security regression. The slight performance overhead of bridge networking is irrelevant for Odoo.
How do I add a second Odoo instance on the same host?
Use a different project directory with different port mappings (e.g., 127.0.0.1:8070:8069) and a separate compose stack. Use a shared internal network if both instances share a PostgreSQL server — just reference the external network with external: true.
What version of the Odoo Docker image should I use?
Always pin to a major version tag like odoo:17 rather than odoo:latest. In production, consider pinning to a specific digest (odoo:17@sha256:...) for fully reproducible deployments. See our Docker update guide for how to safely upgrade versions.
Is the /web/health endpoint available on all Odoo versions?
It was added in Odoo 16. For Odoo 14 and 15, use /web/database/selector as the healthcheck URL, or simply check that the port is open with nc -z localhost 8069.
Do I need the 8072 port?
Port 8072 is the longpolling port used for real-time features (messaging, live chat, bus notifications). If you use any of these features — and most Odoo installations do — you must expose it and configure your reverse proxy to forward /longpolling/ requests to it.