Skip to main content
Production Guide

Docker Compose for Odoo: Production-Ready Setup Guide

Everything needed to run Odoo 18 with Docker Compose — from a minimal 3-minute quickstart to a production-hardened setup with Nginx, SSL, backups, and multi-worker scaling. Every docker-compose.yml in this guide is a complete, working file.

18 min read
Updated February 2026
Odoo 18 · Docker Compose v2 · PostgreSQL 16

If you want a native Ubuntu installation instead, see our Ubuntu install guide. If you want to skip configuration entirely, OEC.sh deploys Odoo on any cloud in under 5 minutes.

Quick Start — Odoo Docker Compose in 3 Minutes

This gets Odoo running on your machine with nothing more than Docker installed. No Nginx, no SSL, no customization — just a working Odoo instance for testing or development.

Create a directory and add two files:

mkdir odoo-docker && cd odoo-docker
docker-compose.ymlyaml
services:
  odoo:
    image: odoo:18.0
    container_name: odoo
    depends_on:
      - db
    ports:
      - "8069:8069"
    volumes:
      - odoo-data:/var/lib/odoo
    environment:
      - HOST=db
      - USER=odoo
      - PASSWORD=odoo

  db:
    image: postgres:16
    container_name: odoo-db
    environment:
      - POSTGRES_DB=postgres
      - POSTGRES_USER=odoo
      - POSTGRES_PASSWORD=odoo
      - PGDATA=/var/lib/postgresql/data/pgdata
    volumes:
      - db-data:/var/lib/postgresql/data/pgdata

volumes:
  odoo-data:
  db-data:

Start it:

docker compose up -d

Open http://localhost:8069 in your browser. You will see the database creation screen. That is it — Odoo is running.

What this gives you: A functional Odoo instance with persistent data (volumes survive container restarts). Good enough for local development and testing.

What this does NOT give you: SSL, reverse proxy, backups, resource limits, health checks, worker processes, or any production hardening. Read on for all of that.

Production Docker Compose Setup

This is the real configuration. It includes:

  • Odoo with resource limits and health checks
  • PostgreSQL 16 with tuned settings and health checks
  • Named volumes for data persistence
  • An isolated Docker network
  • Environment variables externalized to .env
  • Restart policies for reliability
.envbash
# Odoo
ODOO_VERSION=18.0
ODOO_PORT=8069
ODOO_GEVENT_PORT=8072
ODOO_ADMIN_PASSWD=change-this-strong-master-password
ODOO_DB_HOST=db
ODOO_DB_USER=odoo
ODOO_DB_PASSWORD=generate-a-strong-password-here
ODOO_DB_NAME=production
ODOO_WORKERS=4
ODOO_MAX_CRON_THREADS=2
ODOO_LIST_DB=False
ODOO_PROXY_MODE=True

# PostgreSQL
POSTGRES_VERSION=16
POSTGRES_USER=odoo
POSTGRES_PASSWORD=generate-a-strong-password-here
POSTGRES_DB=postgres
docker-compose.ymlyaml
services:
  odoo:
    image: odoo:${ODOO_VERSION}
    container_name: odoo
    depends_on:
      db:
        condition: service_healthy
    ports:
      - "127.0.0.1:${ODOO_PORT}:8069"
      - "127.0.0.1:${ODOO_GEVENT_PORT}:8072"
    volumes:
      - odoo-data:/var/lib/odoo
      - ./config/odoo.conf:/etc/odoo/odoo.conf:ro
      - ./custom-addons:/mnt/extra-addons:ro
    environment:
      - HOST=${ODOO_DB_HOST}
      - USER=${ODOO_DB_USER}
      - PASSWORD=${ODOO_DB_PASSWORD}
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8069/web/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 60s
    deploy:
      resources:
        limits:
          memory: 2G
          cpus: "2.0"
        reservations:
          memory: 512M
          cpus: "0.5"
    networks:
      - odoo-net

  db:
    image: postgres:${POSTGRES_VERSION}
    container_name: odoo-db
    environment:
      - POSTGRES_USER=${POSTGRES_USER}
      - POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
      - POSTGRES_DB=${POSTGRES_DB}
      - PGDATA=/var/lib/postgresql/data/pgdata
    volumes:
      - db-data:/var/lib/postgresql/data/pgdata
    restart: unless-stopped
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER}"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 30s
    deploy:
      resources:
        limits:
          memory: 1G
          cpus: "1.0"
        reservations:
          memory: 256M
          cpus: "0.25"
    networks:
      - odoo-net
    # PostgreSQL tuning for Odoo
    command:
      - "postgres"
      - "-c"
      - "shared_buffers=256MB"
      - "-c"
      - "effective_cache_size=768MB"
      - "-c"
      - "work_mem=16MB"
      - "-c"
      - "maintenance_work_mem=128MB"
      - "-c"
      - "max_connections=100"

volumes:
  odoo-data:
    driver: local
  db-data:
    driver: local

networks:
  odoo-net:
    driver: bridge

Create the config directory and add the configuration file:

mkdir -p config custom-addons
config/odoo.confini
[options]
addons_path = /mnt/extra-addons,/usr/lib/python3/dist-packages/odoo/addons
data_dir = /var/lib/odoo

admin_passwd = change-this-strong-master-password
db_host = db
db_port = 5432
db_user = odoo
db_password = generate-a-strong-password-here
db_name = production
db_maxconn = 64
list_db = False

proxy_mode = True
workers = 4
max_cron_threads = 2
gevent_port = 8072

limit_memory_hard = 2684354560
limit_memory_soft = 2147483648
limit_time_cpu = 600
limit_time_real = 1200
limit_request = 8192

log_level = warn

Start the production stack:

docker compose up -d

Check that both services are healthy:

docker compose ps
docker compose logs -f odoo --tail 50

Key Design Decisions

  • Ports are bound to 127.0.0.1 so Odoo is not exposed to the internet directly — only Nginx (added in the next section) reaches it.
  • PostgreSQL uses service_healthy as a dependency condition, so Odoo waits until PostgreSQL is actually accepting connections before starting.
  • Resource limits prevent a runaway query or memory leak from taking down the entire server.
  • The odoo.conf file is mounted read-only (:ro) to prevent accidental modification from inside the container.

Environment Variables Reference

A complete reference for every environment variable used in the Odoo and PostgreSQL Docker images.

Odoo Container Variables

VariableDescriptionDefaultExample
HOSTPostgreSQL hostnamedbdb, postgres, 10.0.0.5
PORTPostgreSQL port54325432
USERPostgreSQL usernameodooodoo
PASSWORDPostgreSQL passwordodoostrong-password-here

Important: The Odoo Docker image only supports these four environment variables natively. All other Odoo settings (workers, admin password, addons path, etc.) must go in the odoo.conf file. This is why the production setup mounts a configuration file rather than trying to pass everything through environment variables.

PostgreSQL Container Variables

VariableDescriptionDefaultExample
POSTGRES_USERSuperuser usernamepostgresodoo
POSTGRES_PASSWORDSuperuser password(none)strong-password-here
POSTGRES_DBDefault database nameSame as POSTGRES_USERpostgres
PGDATAData directory path/var/lib/postgresql/data/var/lib/postgresql/data/pgdata
POSTGRES_INITDB_ARGSAdditional initdb arguments(none)--encoding=UTF8 --locale=en_US.utf8
POSTGRES_HOST_AUTH_METHODAuthentication methodscram-sha-256scram-sha-256, md5

Note about PGDATA: Always set PGDATA to a subdirectory of the volume mount (e.g., /var/lib/postgresql/data/pgdata), not the mount point itself. This avoids a known Docker issue where the volume's metadata files can conflict with PostgreSQL's data directory initialization.

Adding Nginx Reverse Proxy

For production, you need a reverse proxy for SSL termination, static file caching, and websocket support. Add Nginx as a service in the same Compose file.

Add this service to the services: section of your docker-compose.yml:

docker-compose.yml (add to services)yaml
  nginx:
    image: nginx:alpine
    container_name: odoo-nginx
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx/odoo.conf:/etc/nginx/conf.d/default.conf:ro
      - ./certbot/conf:/etc/letsencrypt:ro
      - ./certbot/www:/var/www/certbot:ro
    depends_on:
      - odoo
    restart: unless-stopped
    networks:
      - odoo-net

The Odoo port bindings from the production setup (127.0.0.1:8069:8069 and 127.0.0.1:8072:8072) are now only for health checks and internal access. Nginx handles all external traffic.

Create the Nginx configuration file:

mkdir -p nginx
nginx/odoo.confnginx
upstream odoo {
    server odoo:8069;
}

upstream odoo-chat {
    server odoo:8072;
}

# Redirect HTTP to HTTPS
server {
    listen 80;
    server_name odoo.yourcompany.com;

    # Let's Encrypt challenge
    location /.well-known/acme-challenge/ {
        root /var/www/certbot;
    }

    location / {
        return 301 https://$host$request_uri;
    }
}

server {
    listen 443 ssl http2;
    server_name odoo.yourcompany.com;

    # SSL certificates
    ssl_certificate /etc/letsencrypt/live/odoo.yourcompany.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/odoo.yourcompany.com/privkey.pem;

    # SSL hardening
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
    ssl_prefer_server_ciphers off;
    ssl_session_timeout 1d;
    ssl_session_cache shared:SSL:10m;

    # Security headers
    add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;
    add_header X-Content-Type-Options nosniff;
    add_header X-Frame-Options SAMEORIGIN;

    # Request size (for attachments)
    client_max_body_size 200m;

    # Proxy buffer tuning
    proxy_buffers 16 64k;
    proxy_buffer_size 128k;
    proxy_read_timeout 900s;
    proxy_connect_timeout 900s;
    proxy_send_timeout 900s;

    # Gzip
    gzip on;
    gzip_types text/css text/plain text/xml application/xml application/json application/javascript;
    gzip_min_length 1000;

    # Websocket (live chat, bus notifications)
    location /websocket {
        proxy_pass http://odoo-chat;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $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;
    }

    # Static files with caching
    location ~* /web/static/ {
        proxy_pass http://odoo;
        proxy_cache_valid 200 90m;
        proxy_buffering on;
        expires 7d;
        add_header Cache-Control "public, no-transform";
    }

    # All other traffic
    location / {
        proxy_pass http://odoo;
        proxy_set_header Host $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;
    }
}

Adding Let's Encrypt SSL

There are two main approaches for automatic SSL with Docker Compose: a Certbot sidecar container or Traefik as the reverse proxy. Here is both.

Option A: Certbot Sidecar (works with Nginx)

Add the certbot service to docker-compose.yml:

docker-compose.yml (add to services)yaml
  certbot:
    image: certbot/certbot
    container_name: odoo-certbot
    volumes:
      - ./certbot/conf:/etc/letsencrypt
      - ./certbot/www:/var/www/certbot
    entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 12h & wait $${!}; done'"
    networks:
      - odoo-net

For the initial certificate, temporarily modify the Nginx config to serve HTTP only (comment out the SSL block), then run:

docker compose up -d nginx

docker compose run --rm certbot certonly \
  --webroot \
  --webroot-path=/var/www/certbot \
  -d odoo.yourcompany.com \
  --email your@email.com \
  --agree-tos \
  --no-eff-email

Restore the full Nginx config with the SSL block and restart:

docker compose restart nginx

The certbot container automatically renews certificates every 12 hours (only actually renews when within 30 days of expiration).

Option B: Traefik (replaces Nginx entirely)

Traefik handles both reverse proxying and SSL automatically. Replace the Nginx service with:

docker-compose.yml (Traefik service)yaml
  traefik:
    image: traefik:v3.0
    container_name: odoo-traefik
    command:
      - "--providers.docker=true"
      - "--providers.docker.exposedbydefault=false"
      - "--entrypoints.web.address=:80"
      - "--entrypoints.websecure.address=:443"
      - "--certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web"
      - "--certificatesresolvers.letsencrypt.acme.email=your@email.com"
      - "--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json"
      - "--entrypoints.web.http.redirections.entrypoint.to=websecure"
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - traefik-certs:/letsencrypt
    restart: unless-stopped
    networks:
      - odoo-net

Then add labels to the Odoo service instead of port mappings:

Odoo service labels for Traefikyaml
  odoo:
    # ... (rest of the odoo config) ...
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.odoo.rule=Host(`odoo.yourcompany.com`)"
      - "traefik.http.routers.odoo.entrypoints=websecure"
      - "traefik.http.routers.odoo.tls.certresolver=letsencrypt"
      - "traefik.http.services.odoo.loadbalancer.server.port=8069"

Add the Traefik volume:

volumes:
  traefik-certs:

Traefik is simpler if you do not need custom Nginx configuration (static file caching, buffer tuning, etc.). For most Odoo deployments, the Nginx approach gives you more control.

Development vs Production Configuration

Docker Compose supports override files. Create a docker-compose.override.yml that is automatically merged when you run docker compose up:

docker-compose.override.yml (development)yaml
services:
  odoo:
    image: odoo:${ODOO_VERSION}
    ports:
      - "8069:8069"
      - "8072:8072"
    volumes:
      - odoo-data:/var/lib/odoo
      - ./config/odoo-dev.conf:/etc/odoo/odoo.conf:ro
      - ./custom-addons:/mnt/extra-addons
      - ./odoo-src:/usr/lib/python3/dist-packages/odoo
    environment:
      - HOST=db
      - USER=odoo
      - PASSWORD=odoo
    command: ["odoo", "--dev=reload,qweb,xml"]
    deploy:
      resources:
        limits:
          memory: 4G

  db:
    ports:
      - "5432:5432"
config/odoo-dev.confini
[options]
addons_path = /mnt/extra-addons,/usr/lib/python3/dist-packages/odoo/addons
data_dir = /var/lib/odoo
admin_passwd = admin
db_host = db
db_port = 5432
db_user = odoo
db_password = odoo
list_db = True
workers = 0
log_level = debug

Key differences from production:

SettingDevelopmentProduction
workers0 (single process, easier debugging)4+ (multi-process)
list_dbTrue (access database manager)False (hidden)
log_leveldebugwarn
--dev=reloadYes (auto-reload on code changes)No
Odoo source mountYes (edit code live)No (use image code)
PostgreSQL portExposed (for pgAdmin, DBeaver)Not exposed
Resource limitsRelaxedStrict

To use only the production config (ignoring the override file):

docker compose -f docker-compose.yml up -d

Custom Addons and OCA Modules

Odoo's power comes from its module ecosystem. Here is how to add custom and OCA (Odoo Community Association) modules to your Docker setup.

Mount a Local Addons Directory

The production docker-compose.yml above already mounts ./custom-addons to /mnt/extra-addons. Drop any module directories in there:

Directory structuretext
custom-addons/
├── my_custom_module/
│   ├── __manifest__.py
│   ├── __init__.py
│   ├── models/
│   └── views/
├── oca_web_responsive/
│   ├── __manifest__.py
│   └── ...
└── another_module/

Configure addons_path

In odoo.conf, list every directory that contains modules:

addons_path = /mnt/extra-addons,/usr/lib/python3/dist-packages/odoo/addons

Order matters: Odoo searches directories left to right. If you have a module with the same name in /mnt/extra-addons and in the base addons, the one found first wins.

Installing OCA Modules with pip

Some OCA modules have Python dependencies. You have two approaches:

Approach 1: Custom Dockerfile

Dockerfiledockerfile
FROM odoo:18.0

USER root

# Install additional Python packages
COPY requirements-extra.txt /tmp/
RUN pip3 install --no-cache-dir -r /tmp/requirements-extra.txt

# Copy custom addons
COPY custom-addons /mnt/extra-addons

USER odoo

Update docker-compose.yml to build from this Dockerfile:

  odoo:
    build:
      context: .
      dockerfile: Dockerfile
    # Remove the 'image:' line when using build

Approach 2: Install at Runtime (development only)

docker compose exec odoo pip3 install phonenumbers python-stdnum

This does not survive container recreation. Use Approach 1 for production.

Git Submodules for OCA Repos

A clean pattern for managing OCA module dependencies:

git submodule add https://github.com/OCA/web.git oca/web
git submodule add https://github.com/OCA/server-tools.git oca/server-tools

Then mount the specific module directories you need:

    volumes:
      - ./oca/web/web_responsive:/mnt/extra-addons/web_responsive:ro
      - ./oca/server-tools/base_technical_user:/mnt/extra-addons/base_technical_user:ro

Multi-Worker Setup

By default, Odoo runs in single-process mode — one thread handles everything. For any deployment with more than a handful of users, you need workers.

How Odoo Workers Work

When workers > 0 in odoo.conf, Odoo spawns:

  • N worker processes — handle HTTP requests (JSON-RPC, web pages, API calls)
  • Cron workers — handle scheduled actions (set by max_cron_threads)
  • 1 gevent process — handles longpolling/websocket on port 8072 (live chat, real-time notifications)

Worker Formula

workers = (CPU cores * 2) + 1

For a 2-core server: workers = 5. For a 4-core server: workers = 9. Each worker uses 150–300 MB of RAM.

Worker Configuration in odoo.conf

odoo.conf (worker settings)ini
workers = 5
max_cron_threads = 2
gevent_port = 8072

; Memory limits per worker
limit_memory_hard = 2684354560   ; 2.5 GB — worker is killed above this
limit_memory_soft = 2147483648   ; 2 GB — worker is recycled above this

; CPU time limits
limit_time_cpu = 600             ; 10 min CPU time per request
limit_time_real = 1200           ; 20 min wall clock per request
limit_time_real_cron = -1        ; No limit for cron jobs

; Request queue
limit_request = 8192             ; Max requests before worker is recycled

Docker Compose Resource Limits with Workers

Scale the container memory to match:

    deploy:
      resources:
        limits:
          # workers * 300MB + 500MB overhead
          memory: 2G
          cpus: "2.0"

If workers are killing themselves (check logs for Worker (PID) reached memory limit), increase limit_memory_soft or reduce the number of workers.

Backup Strategy with Docker

Losing data is not an option. Here is a backup strategy that covers both the PostgreSQL database and the Odoo filestore.

Database Backup with pg_dump

# One-liner: dump the production database to a compressed file
docker compose exec -T db pg_dump -U odoo -d production -Fc > backup_$(date +%Y%m%d_%H%M%S).dump

The -Fc flag creates a custom-format dump that supports parallel restore and selective table restoration.

Filestore Backup

The filestore contains uploaded attachments, generated reports, and session data:

# Copy the Odoo data volume to a tar archive
docker run --rm -v odoo-docker_odoo-data:/data -v $(pwd):/backup alpine \
  tar czf /backup/filestore_$(date +%Y%m%d_%H%M%S).tar.gz -C /data .

Automated Backup Script

Create backup.sh:

backup.shbash
#!/bin/bash
set -euo pipefail

BACKUP_DIR="/opt/backups/odoo"
RETENTION_DAYS=30
DATE=$(date +%Y%m%d_%H%M%S)

mkdir -p "$BACKUP_DIR"

echo "[$(date)] Starting Odoo backup..."

# Database
docker compose exec -T db pg_dump -U odoo -d production -Fc \
  > "$BACKUP_DIR/db_${DATE}.dump"

# Filestore
docker run --rm \
  -v odoo-docker_odoo-data:/data \
  -v "$BACKUP_DIR":/backup \
  alpine tar czf "/backup/filestore_${DATE}.tar.gz" -C /data .

# Cleanup old backups
find "$BACKUP_DIR" -name "db_*.dump" -mtime +$RETENTION_DAYS -delete
find "$BACKUP_DIR" -name "filestore_*.tar.gz" -mtime +$RETENTION_DAYS -delete

echo "[$(date)] Backup complete: db_${DATE}.dump + filestore_${DATE}.tar.gz"
chmod +x backup.sh

Add it to cron for daily backups at 2 AM:

crontab -e
# Add this line:
0 2 * * * cd /path/to/odoo-docker && ./backup.sh >> /var/log/odoo-backup.log 2>&1

Restore from Backup

# Restore database
docker compose exec -T db pg_restore -U odoo -d production --clean --if-exists < backup_file.dump

# Restore filestore
docker run --rm \
  -v odoo-docker_odoo-data:/data \
  -v $(pwd):/backup \
  alpine sh -c "rm -rf /data/* && tar xzf /backup/filestore_backup.tar.gz -C /data"

# Restart Odoo to pick up restored data
docker compose restart odoo

Scaling Odoo with Docker Compose

When a single Odoo container is not enough — either because you have hit the CPU ceiling or you need zero-downtime deployments — you can scale horizontally.

Multiple Odoo Containers with a Load Balancer

docker-compose.yml (scaled)yaml
services:
  odoo:
    image: odoo:${ODOO_VERSION}
    # Do NOT set container_name — it prevents scaling
    depends_on:
      db:
        condition: service_healthy
    volumes:
      - odoo-data:/var/lib/odoo
      - ./config/odoo.conf:/etc/odoo/odoo.conf:ro
      - ./custom-addons:/mnt/extra-addons:ro
    environment:
      - HOST=db
      - USER=${ODOO_DB_USER}
      - PASSWORD=${ODOO_DB_PASSWORD}
    restart: unless-stopped
    deploy:
      replicas: 3
      resources:
        limits:
          memory: 2G
          cpus: "2.0"
    networks:
      - odoo-net

  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx/odoo-lb.conf:/etc/nginx/conf.d/default.conf:ro
    depends_on:
      - odoo
    restart: unless-stopped
    networks:
      - odoo-net

Update the Nginx config to use Docker Compose's built-in DNS:

nginx/odoo-lb.confnginx
upstream odoo {
    # Docker Compose resolves 'odoo' to all replicas
    server odoo:8069;
}

upstream odoo-chat {
    server odoo:8072;
}

Scale up or down at runtime:

docker compose up -d --scale odoo=3

Shared Filestore Requirement

When running multiple Odoo containers, they all need access to the same filestore. Docker named volumes handle this on a single host. For multi-host setups, you need a shared filesystem (NFS, EFS on AWS, or an S3-compatible object store via Odoo's ir.attachment configuration).

When to Scale Beyond Docker Compose

Docker Compose works well for single-host deployments up to about 3–5 Odoo containers. Beyond that, consider:

  • Docker Swarm — native Docker orchestration, minimal learning curve
  • Kubernetes — full orchestration, best for large-scale or multi-tenant setups
  • A managed platform — skip the infrastructure entirely

If you are hitting the limits of what a single server can handle, it might be time to stop managing infrastructure and let OEC.sh handle it.

Docker Compose Generator Tool

Configuring all of the above — Odoo version, PostgreSQL version, workers, Nginx, SSL, volumes, environment variables — is tedious and error-prone.

We built a free tool that does it for you: the Odoo Docker Compose Generator.

Select your options in a visual interface:

  • Odoo version (16, 17, 18, 19)
  • PostgreSQL version
  • Number of workers
  • Reverse proxy (Nginx / Traefik / none)
  • SSL (Let's Encrypt / self-signed / none)
  • Custom addons path
  • Backup configuration

It generates a complete docker-compose.yml, .env, odoo.conf, and Nginx config — ready to copy, paste, and docker compose up. Download everything as a ZIP or copy individual files.

Common Docker Compose Issues

Container Restart Loop (Odoo keeps restarting)

Symptom: docker compose ps shows Odoo restarting repeatedly. Logs show the container starting then immediately stopping.

Debug:

docker compose logs odoo --tail 100

Common causes:

  • odoo.conf has a syntax error — check for typos, missing = signs, or invalid option names.
  • PostgreSQL is not ready — ensure the depends_on condition uses service_healthy, not just service_started.
  • The addons_path points to a directory that does not exist inside the container.
  • Port 8069 is already in use by another process: sudo lsof -i :8069.

Volume Permission Errors

Symptom: PermissionError: [Errno 13] Permission denied: '/var/lib/odoo/...'

Cause: The Odoo container runs as UID 101 (the odoo user inside the container). If you mount a host directory, the host directory must be writable by UID 101.

Fix:

sudo chown -R 101:101 ./custom-addons
# Or set the directory to be world-writable for development:
chmod -R 777 ./custom-addons

For named volumes (the odoo-data: volume), Docker handles permissions automatically. This issue only affects bind mounts.

Database Connection Refused

Symptom: psycopg2.OperationalError: could not connect to server: Connection refused

Fix checklist:

  1. Is the db container running? docker compose ps db
  2. Is the HOST environment variable set to db (the service name), not localhost or 127.0.0.1?
  3. Do the POSTGRES_USER / POSTGRES_PASSWORD values match between the db and odoo services?
  4. Is PostgreSQL healthy? docker compose exec db pg_isready -U odoo

Port Conflict (address already in use)

Symptom: Bind for 0.0.0.0:8069 failed: port is already allocated

Fix: Either stop the other process using the port, or change the host port mapping:

    ports:
      - "8070:8069"  # Map to a different host port

Odoo Cannot Find Custom Modules

Symptom: Your module does not appear in the Apps list after installation.

Fix:

  1. Verify the volume mount is correct: docker compose exec odoo ls /mnt/extra-addons/
  2. Verify addons_path in odoo.conf includes /mnt/extra-addons.
  3. Update the module list: go to Apps > Update Apps List in the Odoo UI.
  4. Remove the “Apps” filter in the search bar — by default, Odoo only shows “Apps” and hides technical modules.

Docker Compose Version Confusion

Symptom: docker-compose (with a hyphen) throws errors about unsupported syntax.

Fix: Use docker compose (without the hyphen). The old standalone docker-compose tool (v1) is deprecated. Modern Docker Desktop and Docker Engine include Compose as a plugin: docker compose up -d. If you see errors about version: at the top of the YAML file, just remove that line — Docker Compose v2 ignores it.

Or Skip Docker — Deploy on OEC.sh

Docker gives you control and portability. It also gives you YAML files to maintain, SSL certificates to renew, backup scripts to write, volume permissions to debug, and compose configurations to keep in sync across environments.

OEC.sh is what we built for the teams that would rather ship product:

  • Any cloud provider — AWS, Hetzner, DigitalOcean, GCP, Azure, OVH, Linode, Vultr. You pick the provider and region. Your data stays on your infrastructure. No vendor lock-in.
  • Any Odoo version — 16 through 19, Community or Enterprise.
  • Production from day one — SSL, automated backups, monitoring, log management, and security updates are included. Not configured by you — included.
  • Free tier — start without a credit card. Upgrade when you need more resources.

The Docker knowledge from this guide is not wasted — it is exactly what OEC.sh runs under the hood, tuned and automated. If you ever need to eject, your data and configuration come with you.

Ready to deploy Odoo?

Skip the Docker configuration. OEC.sh deploys Odoo on any cloud provider in under 5 minutes — with SSL, backups, and monitoring included.

  • Free tier available
  • No credit card required
  • Your cloud, your data
Try OEC.sh Free

Frequently Asked Questions

What Docker image should I use for Odoo?

Use the official odoo image from Docker Hub. Tags follow the pattern odoo:18.0 for the latest Odoo 18 point release, or odoo:18.0-20260201 for a specific build date. For production, pin to a specific date tag so your image does not change unexpectedly when Docker Hub updates the 18.0 tag. For development, the floating 18.0 tag is fine.

What are the minimum system requirements for Odoo with Docker?

2 GB of RAM, 2 CPU cores, and 20 GB of disk space for a small instance (1–10 users). Docker itself adds minimal overhead — roughly 50–100 MB of RAM for the daemon. The real resource consumer is the Odoo process and PostgreSQL. Use our Server Requirements Calculator for precise sizing based on your user count and module list.

Can I run Odoo Enterprise with Docker Compose?

Yes, but the Enterprise image is not on the public Docker Hub. You need an active Odoo Enterprise subscription and access to the private Odoo Docker registry. Replace image: odoo:18.0 with the Enterprise image URL provided by Odoo. Everything else in the docker-compose.yml stays the same. Alternatively, build a custom image with your Enterprise license and the Enterprise addons.

How do I update Odoo in Docker?

Pull the new image, stop the containers, back up your data, and start again: docker compose pull odoo && docker compose down && docker compose up -d. If the new version requires a database migration, run: docker compose run --rm odoo odoo -u all -d production --stop-after-init. Always test upgrades on a copy of your production database first.

How do I access the Odoo shell in a Docker container?

For an interactive Odoo shell (Python REPL with Odoo environment loaded): docker compose run --rm odoo odoo shell -d production. For a bash shell inside the running container: docker compose exec odoo bash.

Is Docker Compose suitable for production Odoo?

Yes, for single-server deployments. Docker Compose manages the container lifecycle, networking, and volumes reliably. Add health checks, restart policies, resource limits, and automated backups (all covered in this guide) and you have a solid production setup. For multi-server, high-availability deployments, consider Docker Swarm or Kubernetes instead.

How do I view Odoo logs in Docker?

Follow logs in real time: docker compose logs -f odoo. Last 200 lines: docker compose logs odoo --tail 200. Logs since a specific time: docker compose logs odoo --since "2026-02-26T10:00:00". If you configured a logfile in odoo.conf, logs also go to that file inside the container.

Docker Compose vs Docker Swarm vs Kubernetes — which should I use for Odoo?

Docker Compose is right for single-server deployments (the vast majority of Odoo installations). It is simple, well-documented, and sufficient for 1–200 concurrent users on properly sized hardware. Docker Swarm adds multi-host orchestration with minimal extra complexity — use it when you need failover across 2–3 servers. Kubernetes is for large-scale, multi-tenant, or enterprise deployments where you need auto-scaling, rolling updates, and infrastructure-as-code across clusters. Most teams should start with Docker Compose and only move to Swarm or Kubernetes when they have a concrete reason.

Last updated: February 2026. Tested with Odoo 18.0, Docker 27.x, Docker Compose v2, PostgreSQL 16, and Nginx 1.27.

Deploy Odoo Without the Docker Hassle

Use the Docker Compose Generator for a custom config, or skip configuration entirely and deploy on OEC.sh — SSL, backups, monitoring, and scaling handled for you.