Deploy Odoo with Docker Compose

Docker provides a reproducible, isolated environment for running Odoo. Instead of installing Python, PostgreSQL, and system dependencies directly on a host machine, containers package each service into a self-contained unit. This guide covers Docker Compose setup for both development and production, including PostgreSQL configuration, persistent volumes, and reverse proxy setup.

Why Use Docker for Odoo

Docker solves several problems that are common in traditional Odoo deployments:

  • ✓Reproducible environments: The same docker-compose.yml produces identical setups on your laptop, a staging server, and production. No more "it works on my machine" issues.
  • ✓Isolated services: Odoo, PostgreSQL, and Redis run in separate containers that don't conflict with host packages or other applications on the same server.
  • ✓Multiple instances: Spin up separate Odoo instances for different projects or customers by running additional docker-compose stacks on different ports.
  • ✓Version-controlled infrastructure: Your docker-compose.yml and Dockerfile are code. They can be committed to Git, reviewed, and rolled back like any other source file.
  • ✓Simplified backups: Named volumes store database and filestore data persistently. Backup scripts can dump the database and archive the volume contents with simple shell commands.

Prerequisites

  • →Linux server (Ubuntu 22.04+ recommended) or a local machine with Docker Desktop installed
  • →Docker Engine 24+ and Docker Compose v2 (the docker compose plugin, not the legacy docker-compose binary)
  • →At least 2 GB of RAM for a small instance (4 GB recommended for production workloads)
  • →A domain name with DNS pointing to your server (for production deployments with SSL)

Basic Development Setup

A minimal Odoo development setup with Docker Compose requires just two services: Odoo and PostgreSQL. Here is a working docker-compose.yml for development:

version: "3.8"

services:
  odoo:
    image: odoo:17
    depends_on:
      - db
    ports:
      - "8069:8069"
    volumes:
      - odoo-data:/var/lib/odoo
      - ./addons:/mnt/extra-addons
    environment:
      - HOST=db
      - USER=odoo
      - PASSWORD=odoo
    restart: unless-stopped

  db:
    image: postgres:15
    environment:
      - POSTGRES_DB=postgres
      - POSTGRES_USER=odoo
      - POSTGRES_PASSWORD=odoo
    volumes:
      - db-data:/var/lib/postgresql/data
    restart: unless-stopped

volumes:
  odoo-data:
  db-data:

Key points about this configuration:

  • →Named volumes (odoo-data and db-data) persist data across container restarts. Without them, all data is lost when containers are removed.
  • →The ./addons bind mount lets you develop custom modules on the host and see changes reflected inside the container immediately.
  • →Environment variables (HOST, USER, PASSWORD) tell Odoo how to connect to PostgreSQL.

Start the stack with docker compose up -d. Odoo will be accessible at http://localhost:8069. The first startup takes a few minutes while Odoo initializes the database and installs base modules.

Production Considerations

A production Docker setup needs additional configuration beyond the development stack. Here is a production-ready example with health checks, resource limits, and proper security:

version: "3.8"

services:
  odoo:
    image: odoo:17
    depends_on:
      db:
        condition: service_healthy
    volumes:
      - odoo-data:/var/lib/odoo
      - ./addons:/mnt/extra-addons
      - ./config:/etc/odoo
    environment:
      - HOST=db
      - USER=odoo
      - PASSWORD=${DB_PASSWORD}
    deploy:
      resources:
        limits:
          cpus: "2"
          memory: 2G
        reservations:
          cpus: "1"
          memory: 1G
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8069/web/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 60s
    restart: always
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"
    networks:
      - odoo-net

  db:
    image: postgres:15
    environment:
      - POSTGRES_DB=postgres
      - POSTGRES_USER=odoo
      - POSTGRES_PASSWORD=${DB_PASSWORD}
    volumes:
      - db-data:/var/lib/postgresql/data
    deploy:
      resources:
        limits:
          cpus: "1"
          memory: 1G
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U odoo"]
      interval: 10s
      timeout: 5s
      retries: 5
    restart: always
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"
    networks:
      - odoo-net

volumes:
  odoo-data:
  db-data:

networks:
  odoo-net:
    driver: bridge

Important production practices:

  • →Resource limits: Prevent a single container from consuming all server resources. Set CPU and memory limits based on your workload.
  • →Health checks: Docker monitors container health and restarts unhealthy containers automatically. The depends_on condition ensures Odoo waits for PostgreSQL to be ready before starting.
  • →Log rotation: Without limits, container logs grow unbounded and fill the disk. The logging configuration above limits each log file to 10 MB and keeps only the three most recent files.
  • →Environment variables: Store sensitive values like database passwords in a .env file, not in the docker-compose.yml. Never commit the .env file to version control.
  • →Network isolation: The custom bridge network keeps Odoo and PostgreSQL communicating internally while exposing only Odoo to the reverse proxy.

Nginx Reverse Proxy

Odoo should never be exposed directly on port 8069 in production. An Nginx reverse proxy handles SSL termination, static file caching, WebSocket support for live chat, and security headers.

server {
    listen 80;
    server_name odoo.example.com;
    return 301 https://$host$request_uri;
}

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

    ssl_certificate /etc/letsencrypt/live/odoo.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/odoo.example.com/privkey.pem;

    proxy_buffers 16 64k;
    proxy_buffer_size 128k;

    location / {
        proxy_pass http://127.0.0.1:8069;
        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;
    }

    location /websocket {
        proxy_pass http://127.0.0.1:8069;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
    }

    location ~* /web/static/ {
        proxy_pass http://127.0.0.1:8069;
        proxy_cache_valid 200 90m;
        proxy_buffering on;
        expires 864000;
    }

    client_max_body_size 200m;
}

The configuration above handles HTTP-to-HTTPS redirection, WebSocket proxying for Odoo's live chat module, static file caching to reduce load on the Odoo container, and a generous body size limit for file uploads.

SSL with Let's Encrypt

Use Certbot to obtain and auto-renew SSL certificates:

  1. 1.Install Certbot: sudo apt install certbot python3-certbot-nginx
  2. 2.Obtain a certificate: sudo certbot --nginx -d odoo.example.com
  3. 3.Verify auto-renewal: sudo certbot renew --dry-run

Certbot installs a systemd timer that automatically renews certificates before they expire. No manual intervention is needed after initial setup.

Backups in Docker

Backing up a Docker-based Odoo setup involves two components: the PostgreSQL database and the filestore (attachments, documents, and binary fields).

#!/bin/bash
# Database backup
docker compose exec -T db pg_dump -U odoo odoo_db > backup_$(date +%Y%m%d_%H%M%S).sql

# Filestore backup
docker run --rm -v odoo-sh-front_odoo-data:/data -v $(pwd):/backup \
  alpine tar czf /backup/filestore_$(date +%Y%m%d_%H%M%S).tar.gz /data

Automate these scripts with cron to run daily. Store backups off-site — a named volume on the same server does not protect against disk failure.

Updating Odoo in Docker

One of Docker's advantages is that data lives in volumes, not containers. Updating Odoo is straightforward:

  1. 1.Pull the new image: docker compose pull odoo
  2. 2.Recreate the container: docker compose up -d
  3. 3.If needed, run a module upgrade: docker compose exec odoo odoo -u all -d odoo_db --stop-after-init

The named volumes remain intact throughout the process. Only the container (the running process) is replaced.

Related Resources

Running Odoo in Docker gives you control over the stack. These hosting options build on that control with managed infrastructure and developer-friendly tooling.