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.
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-dockerservices:
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 -dOpen 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
# 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=postgresservices:
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: bridgeCreate the config directory and add the configuration file:
mkdir -p config custom-addons[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 = warnStart the production stack:
docker compose up -dCheck that both services are healthy:
docker compose ps
docker compose logs -f odoo --tail 50Key Design Decisions
- •Ports are bound to
127.0.0.1so Odoo is not exposed to the internet directly — only Nginx (added in the next section) reaches it. - •PostgreSQL uses
service_healthyas 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.conffile 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
| Variable | Description | Default | Example |
|---|---|---|---|
| HOST | PostgreSQL hostname | db | db, postgres, 10.0.0.5 |
| PORT | PostgreSQL port | 5432 | 5432 |
| USER | PostgreSQL username | odoo | odoo |
| PASSWORD | PostgreSQL password | odoo | strong-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
| Variable | Description | Default | Example |
|---|---|---|---|
| POSTGRES_USER | Superuser username | postgres | odoo |
| POSTGRES_PASSWORD | Superuser password | (none) | strong-password-here |
| POSTGRES_DB | Default database name | Same as POSTGRES_USER | postgres |
| PGDATA | Data directory path | /var/lib/postgresql/data | /var/lib/postgresql/data/pgdata |
| POSTGRES_INITDB_ARGS | Additional initdb arguments | (none) | --encoding=UTF8 --locale=en_US.utf8 |
| POSTGRES_HOST_AUTH_METHOD | Authentication method | scram-sha-256 | scram-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:
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-netThe 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 nginxupstream 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:
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-netFor 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-emailRestore the full Nginx config with the SSL block and restart:
docker compose restart nginxThe 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:
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-netThen add labels to the Odoo service instead of port mappings:
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:
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"[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 = debugKey differences from production:
| Setting | Development | Production |
|---|---|---|
| workers | 0 (single process, easier debugging) | 4+ (multi-process) |
| list_db | True (access database manager) | False (hidden) |
| log_level | debug | warn |
| --dev=reload | Yes (auto-reload on code changes) | No |
| Odoo source mount | Yes (edit code live) | No (use image code) |
| PostgreSQL port | Exposed (for pgAdmin, DBeaver) | Not exposed |
| Resource limits | Relaxed | Strict |
To use only the production config (ignoring the override file):
docker compose -f docker-compose.yml up -dCustom 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:
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/addonsOrder 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
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 odooUpdate docker-compose.yml to build from this Dockerfile:
odoo:
build:
context: .
dockerfile: Dockerfile
# Remove the 'image:' line when using buildApproach 2: Install at Runtime (development only)
docker compose exec odoo pip3 install phonenumbers python-stdnumThis 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-toolsThen 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:roMulti-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) + 1For 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
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 recycledDocker 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).dumpThe -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:
#!/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.shAdd 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>&1Restore 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 odooScaling 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
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-netUpdate the Nginx config to use Docker Compose's built-in DNS:
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=3Shared 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 100Common causes:
- •
odoo.confhas a syntax error — check for typos, missing=signs, or invalid option names. - •PostgreSQL is not ready — ensure the
depends_oncondition usesservice_healthy, not justservice_started. - •The
addons_pathpoints 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-addonsFor 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:
- Is the
dbcontainer running?docker compose ps db - Is the
HOSTenvironment variable set todb(the service name), notlocalhostor127.0.0.1? - Do the
POSTGRES_USER/POSTGRES_PASSWORDvalues match between thedbandodooservices? - 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 portOdoo Cannot Find Custom Modules
Symptom: Your module does not appear in the Apps list after installation.
Fix:
- Verify the volume mount is correct:
docker compose exec odoo ls /mnt/extra-addons/ - Verify
addons_pathinodoo.confincludes/mnt/extra-addons. - Update the module list: go to Apps > Update Apps List in the Odoo UI.
- 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
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.
Related Resources
Odoo Docker Guide
Complete guide to running Odoo in Docker containers.
Odoo Docker Hub Reference
Official image tags, versions, and environment variables.
Enterprise vs Community Docker
Run Enterprise or Community Edition with Docker.
Odoo Ubuntu Install Guide
Install Odoo natively on Ubuntu without Docker.
Nginx Reverse Proxy Config
Production Nginx config with SSL and websocket support.
Docker Compose Generator
Generate production-ready Docker Compose files visually.
Deploy Odoo on Any Cloud
Step-by-step guide to deploy Odoo on any cloud provider.
Server Requirements Calculator
Calculate the right server size for your Odoo workload.
OEC.sh Pricing
Free tier available. Deploy Odoo on any cloud provider.