Odoo Docker Image: Build a Custom Image with Your Addons

Build a custom Odoo Docker image on odoo:19.0 with your addons and pip packages, a multi-stage build, and auto-rebuild with Docker Compose watch.

Updated . 5 min read

The official Odoo Docker image runs stock Odoo well, but real projects add their own modules, Python libraries and configuration. This guide shows how to build a custom Odoo Docker image on top of odoo:19.0 with a short Dockerfile, bake in your custom addons and pip packages, use a multi-stage build when a package needs a compiler, and rebuild automatically while you develop with Docker Compose.

When a custom Odoo image is worth it

Mounting an ./addons folder into the stock image (as in the Odoo Docker Compose guide) is fine on a laptop. Build your own image when:

  • your modules list external_dependencies that the stock image does not ship, such as a PyPI library;
  • you need system packages (fonts, client libraries, tools);
  • you want one versioned artifact, for example mycompany/odoo:19.0-42, that runs the same code in test and production.

Project layout

odoo-image/
├── Dockerfile
├── .dockerignore
├── requirements.txt
├── odoo.conf
├── docker-compose.yml
└── addons/
    ├── my_module/
    └── another_module/

A basic Odoo Dockerfile

Dockerfile
ARG ODOO_VERSION=19.0
FROM odoo:${ODOO_VERSION}

USER root

# System packages your modules need (example)
RUN apt-get update \
 && apt-get install -y --no-install-recommends fonts-noto-cjk \
 && rm -rf /var/lib/apt/lists/*

# Python packages
COPY requirements.txt /tmp/requirements.txt
RUN pip3 install --no-cache-dir --break-system-packages -r /tmp/requirements.txt

# Custom addons and config
COPY --chown=odoo:odoo addons /opt/odoo/custom-addons
COPY --chown=odoo:odoo odoo.conf /etc/odoo/odoo.conf

USER odoo

Line by line:

  • ARG before FROM lets you pick the Odoo version at build time. odoo:18.0 works the same way.
  • USER root … USER odoo: the base image runs as the odoo user. Switch to root only for the install steps and switch back at the end. Never ship an image that runs Odoo as root.
  • --break-system-packages: the Odoo 18 and 19 images are built on Ubuntu 24.04, whose system Python refuses plain pip install (PEP 668). The flag lets pip install into the image's Python. That is acceptable in a single-purpose container. Prefer the Ubuntu package (python3-xyz) when one exists.
  • Addons go to /opt/odoo/custom-addons, not /mnt/extra-addons. The base image declares /mnt/extra-addons as a volume. When Compose recreates a container it carries anonymous volumes over from the old one, so code you baked into that path can be silently replaced by the previous version's files. A separate path avoids that.
odoo.conf
[options]
addons_path = /opt/odoo/custom-addons,/mnt/extra-addons
data_dir = /var/lib/odoo
admin_passwd = replace-with-a-long-random-string
list_db = False
proxy_mode = True
workers = 2

Keep database connection settings out of this file and pass them as environment variables at runtime. Passwords and other settings are covered in Odoo Docker environment variables.

.dockerignore
.git
**/__pycache__
**/*.pyc
*.dump
*.tar.gz
.env

A small build context makes builds faster and keeps secrets and backups out of the image.

Multi-stage build for packages that need a compiler

Some PyPI packages compile C code on install, which needs build-essential and header files. Installing those in the final image adds hundreds of megabytes. A multi-stage build compiles wheels in a throwaway stage and copies only the result:

Dockerfile (multi-stage)
ARG ODOO_VERSION=19.0

FROM odoo:${ODOO_VERSION} AS wheels
USER root
RUN apt-get update \
 && apt-get install -y --no-install-recommends build-essential python3-dev \
 && rm -rf /var/lib/apt/lists/*
COPY requirements.txt /tmp/requirements.txt
RUN pip3 wheel --no-cache-dir -r /tmp/requirements.txt -w /wheels

FROM odoo:${ODOO_VERSION}
USER root
COPY --from=wheels /wheels /tmp/wheels
RUN pip3 install --no-cache-dir --break-system-packages --no-index --find-links /tmp/wheels /tmp/wheels/*.whl \
 && rm -rf /tmp/wheels
COPY --chown=odoo:odoo addons /opt/odoo/custom-addons
COPY --chown=odoo:odoo odoo.conf /etc/odoo/odoo.conf
USER odoo

Both stages start from the same Odoo image, so the wheels match the final image's Python version.

Build the image with Docker Compose

docker-compose.yml
services:
  db:
    image: postgres:16
    environment:
      POSTGRES_DB: postgres
      POSTGRES_USER: odoo
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD in .env}
    volumes:
      - odoo-db-data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U odoo -d postgres"]
      interval: 10s
      timeout: 5s
      retries: 5

  web:
    build:
      context: .
      args:
        ODOO_VERSION: "19.0"
    image: mycompany/odoo:19.0-dev
    depends_on:
      db:
        condition: service_healthy
    ports:
      - "127.0.0.1:8069:8069"
    environment:
      HOST: db
      USER: odoo
      PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD in .env}
    volumes:
      - odoo-web-data:/var/lib/odoo
    develop:
      watch:
        - action: sync+restart
          path: ./addons
          target: /opt/odoo/custom-addons
        - action: rebuild
          path: requirements.txt

volumes:
  odoo-db-data:
  odoo-web-data:

With both build and image set, Compose builds the Dockerfile and tags the result with that name. The commands you need:

TaskCommand
Build the imagedocker compose build
Build and start in one stepdocker compose up -d --build
Refresh the base image, then builddocker compose build --pull
Build from scratch, ignoring cachedocker compose build --no-cache web
Override a build argumentdocker compose build --build-arg ODOO_VERSION=18.0
Push the tagged image to your registrydocker compose push web

BuildKit is the default builder in current Docker, so you do not need DOCKER_BUILDKIT=1. Services build in parallel by default.

Auto-rebuild while you develop

The develop.watch block above makes Compose react to file changes:

docker compose up --watch
  • Saving a file under ./addons copies it into the running container and restarts Odoo (sync+restart), so Python changes take effect without a rebuild.
  • Editing requirements.txt rebuilds the image and replaces the container (rebuild).

Watch only works for services that have a build section, not for plain image: services. A restart reloads Python code, but changes to XML views, data files or security rules still need a module update:

docker compose run --rm web odoo -d mydb -u my_module --stop-after-init
docker compose restart web

Use run rather than exec: run goes through the image's entrypoint, which adds the database connection arguments. In production, stop the web service before updating modules.

Tag, test and ship

  • Tag every build with the Odoo version and your own build number (19.0-42), never only latest. Rolling back is then a one-line tag change.
  • Rebuild with --pull regularly so you pick up the newer Odoo build and security fixes in the base image.
  • Test a new image against a copy of the production database before deploying it, and back up the database and filestore first.
  • Bumping ODOO_VERSION to a new major release needs a database migration, not just a rebuild.

Custom modules without building images

On Easy Instance you get the same result without maintaining a Dockerfile or registry: connect a GitHub repository and your custom modules deploy on every push, and PyPI packages install from the Packages tab. See adding custom modules from GitHub and installing Python packages. Plans are $30 per worker a month, and the 10-day free trial needs no credit card.

Rather not run the server yourself?

Get managed Odoo hosting with backups, SSL, monitoring and updates handled for you. Free for 10 days.