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_dependenciesthat 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
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 odooLine by line:
ARGbeforeFROMlets you pick the Odoo version at build time.odoo:18.0works the same way.USER root…USER odoo: the base image runs as theodoouser. 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 plainpip 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-addonsas 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.
[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 = 2Keep 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.
.git
**/__pycache__
**/*.pyc
*.dump
*.tar.gz
.envA 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:
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 odooBoth stages start from the same Odoo image, so the wheels match the final image's Python version.
Build the image with Docker Compose
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:
| Task | Command |
|---|---|
| Build the image | docker compose build |
| Build and start in one step | docker compose up -d --build |
| Refresh the base image, then build | docker compose build --pull |
| Build from scratch, ignoring cache | docker compose build --no-cache web |
| Override a build argument | docker compose build --build-arg ODOO_VERSION=18.0 |
| Push the tagged image to your registry | docker 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
./addonscopies it into the running container and restarts Odoo (sync+restart), so Python changes take effect without a rebuild. - Editing
requirements.txtrebuilds 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 onlylatest. Rolling back is then a one-line tag change. - Rebuild with
--pullregularly 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_VERSIONto 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.