Skip to content

Odoo Docker Compose: Production-Ready Setup

DeployMonkey Team · March 11, 2026 8 min read

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_hard in 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.