All posts
Developer10 min read

Dockerizing your Odoo development environment: live addons, mail intercept, and isolated databases

A Docker Compose setup for local Odoo development that mounts your addons live, intercepts outbound email, and keeps project databases isolated without the overhead of multiple Odoo installations.

Running Odoo locally without Docker means managing Python virtual environments, PostgreSQL installations, and wkhtmltopdf versions per project. It works until you need to context-switch between a client on Odoo 16 and a client on Odoo 18. Then it becomes a maintenance problem.

Docker Compose eliminates that. One config file per project, one command to start everything, databases isolated by project name, and addons mounted from your working directory so changes are visible without rebuilding.

This is the setup that works in practice, not the toy version.

The Docker Compose file#

yaml
# docker-compose.yml
version: "3.8"

services:
  db:
    image: postgres:15
    environment:
      POSTGRES_DB: postgres
      POSTGRES_USER: odoo
      POSTGRES_PASSWORD: odoo
    volumes:
      - pgdata:/var/lib/postgresql/data
    ports:
      - "5433:5432"

  odoo:
    image: odoo:18.0
    depends_on:
      - db
      - mailhog
    environment:
      HOST: db
      USER: odoo
      PASSWORD: odoo
    volumes:
      - ./addons:/mnt/extra-addons
      - ./config/odoo.conf:/etc/odoo/odoo.conf:ro
      - odoo_data:/var/lib/odoo
    ports:
      - "8069:8069"
      - "8072:8072"

  mailhog:
    image: mailhog/mailhog
    ports:
      - "8025:8025"
      - "1025:1025"

volumes:
  pgdata:
  odoo_data:

The mailhog service intercepts all outbound SMTP. Any email Odoo would send during development - password resets, sales confirmations, invoice emails - arrives in the Mailhog web UI at localhost:8025 instead of going to a real address. This is mandatory for development environments.

The odoo.conf file#

ini
[options]
addons_path = /mnt/extra-addons,/usr/lib/python3/dist-packages/odoo/addons
db_host = db
db_port = 5432
db_user = odoo
db_password = odoo
smtp_server = mailhog
smtp_port = 1025
smtp_ssl = False
dev_mode = reload
log_level = info

The critical setting here is dev_mode = reload. With this set, Odoo watches your addon Python files for changes and reloads automatically - no container restart required for Python changes. XML view changes still require a module update (-u module_name).

Note that dev_mode = all enables the developer mode UI as well as auto-reload. Use reload for background services where you do not want the UI debug toolbar.

Mounting addons live#

The ./addons:/mnt/extra-addons volume mount is what makes development practical. Your local addons/ directory maps into the container. Any Python file you edit is visible to Odoo immediately (via the reload watcher). Any XML file you edit is visible after a -u.

Structure your addons directory as:

addons/
  my_sale_custom/
    __manifest__.py
    models/
    views/
    ...
  my_stock_custom/
    ...

Do not put non-module directories inside addons/. Odoo scans every directory looking for __manifest__.py and logs warnings for anything it does not recognize.

Running Odoo commands#

Initialize a database:

bash
docker compose run --rm odoo odoo   --init my_sale_custom   --database my_project   --stop-after-init

Update a module:

bash
docker compose run --rm odoo odoo   --update my_sale_custom   --database my_project   --stop-after-init

Open a shell in the running container:

bash
docker compose exec odoo bash

Run the test suite:

bash
docker compose run --rm odoo odoo   --update my_sale_custom   --database my_project_test   --stop-after-init   --test-enable   --log-level=test

Use a separate database for tests (my_project_test) so your development data is not overwritten.

Multiple projects in isolation#

The pgdata volume is shared across projects if you use the same docker-compose.yml. For project isolation, either:

Option A: Named volumes per project. Change pgdata to a project-specific name in the Compose file, e.g. pgdata_client_a. This keeps databases from different projects on different volumes.

Option B: Separate Compose stacks. Put each project in its own directory with its own docker-compose.yml. This is the cleanest separation - different ports, different volumes, completely independent. Use different host port mappings (5434:5432, 8070:8069) for each project so they can run simultaneously.

Adding OCA modules#

OCA modules go in your addons/ directory. The simplest setup:

bash
# Clone the OCA module you need
git clone https://github.com/OCA/server-tools.git /tmp/oca-server-tools
# Copy only the modules you need into addons/
cp -r /tmp/oca-server-tools/base_setup_partner addons/

For projects with many OCA dependencies, use git-aggregator with a repos.yml file to declare all your addon sources declaratively. See the separate post on git-aggregator for that setup.

The wkhtmltopdf problem#

PDF reports require wkhtmltopdf. The official odoo:18.0 image includes a compatible version. If you are using a custom Dockerfile based on the official image, verify with:

bash
docker compose exec odoo wkhtmltopdf --version

If you are building your own image without starting from odoo:18.0, install the Odoo-patched wkhtmltopdf from https://github.com/wkhtmltopdf/packaging/releases - not the version from the system package manager, which is usually too old.

What this setup does not solve#

Live XML reload without a module update. There is no watcher for XML files in Odoo - changes require -u module_name. The reload mode only watches Python. The workflow is: edit Python, see changes immediately; edit XML/views, run the update command.

If you spend a lot of time iterating on views, write a shell alias:

bash
alias oodoo-update='docker compose run --rm odoo odoo   --update $1 --database dev --stop-after-init'

Then odoo-update my_module takes about 10 seconds and gets you fresh views without a full restart.

Try ERPeek on your own Odoo module - ask questions, scaffold tests, and explore your codebase in plain language.

Get started free