Skip to content

How to Deploy Odoo with Custom Modules in Docker

DeployMonkey Team · March 11, 2026 8 min read

Getting a custom Odoo module into a Docker container is one of those tasks that looks trivial but has several tripping points — wrong addons_path, modules not found, upgrades not applying. This guide walks through the correct approach from a simple volume mount to a production Git-based deployment pipeline.

The Three Approaches

There are three ways to get custom modules into an Odoo Docker container:

  1. Volume mount — mount a host directory into the container at runtime
  2. Custom Docker image — COPY modules into a derived image at build time
  3. Git clone into volume — clone directly into the addons volume, pull to update

For development, use volume mounts. For production with controlled releases, build a custom image. For small teams deploying frequently, Git-into-volume is a pragmatic middle ground.

Approach 1: Volume Mount (Development)

Assuming your custom modules live at ./addons on the host:

# docker-compose.yml
services:
  odoo:
image: odoo:17
volumes:
  - odoo_data:/var/lib/odoo
  - ./config/odoo.conf:/etc/odoo/odoo.conf:ro
  - ./addons:/mnt/extra-addons:ro   # <-- your custom modules

The :ro flag mounts read-only — Odoo never needs to write to addons, and this prevents accidental modification from inside the container.

Configuring addons_path

This is where most people get stuck. The addons_path in odoo.conf must include both the built-in addons directory AND your custom path:

[options]
addons_path = /usr/lib/python3/dist-packages/odoo/addons,/mnt/extra-addons

The exact path to built-in addons varies by Odoo version:

  • Odoo 14–17: /usr/lib/python3/dist-packages/odoo/addons
  • Community Edition: also check /usr/lib/python3/dist-packages/odoo/addons
  • Enterprise: add /mnt/enterprise as a third path

To find the correct path for your image:

docker compose exec odoo python3 -c "import odoo; print(odoo.__file__)"

Installing a Custom Module

# Install (first time)
docker compose exec odoo odoo -d your_db --init=your_module_name --stop-after-init

# Upgrade after code changes
docker compose exec odoo odoo -d your_db -u your_module_name --stop-after-init

After running these, restart the main container to pick up any changes to Python files that were already loaded:

docker compose restart odoo

Approach 2: Custom Docker Image (Production)

For reproducible production deployments, bake your modules into the image:

# Dockerfile
FROM odoo:17

# Copy custom modules
COPY ./addons /mnt/extra-addons

# Optional: install Python dependencies for your modules
COPY ./requirements.txt /tmp/requirements.txt
RUN pip3 install --no-cache-dir -r /tmp/requirements.txt

USER odoo
# Build and tag
docker build -t my-company/odoo:17-v1.2.3 .

# Push to your registry
docker push my-company/odoo:17-v1.2.3

Update docker-compose.yml to use your image:

services:
  odoo:
image: my-company/odoo:17-v1.2.3   # pin to exact version

This approach gives you immutable, versioned deployments. Roll back is as simple as changing the image tag.

Approach 3: Git Clone Into Volume

For teams doing frequent deploys without a full CI pipeline:

# One-time: clone your addons repo into the Docker volume
git clone https://github.com/yourorg/odoo-addons.git /opt/odoo/addons

# Mount the clone in docker-compose.yml
volumes:
  - /opt/odoo/addons:/mnt/extra-addons:ro

To deploy an update:

#!/bin/bash
# deploy.sh
cd /opt/odoo/addons
git pull origin main

docker compose exec odoo odoo -d production \
  -u your_module_name \
  --stop-after-init --no-http

docker compose restart odoo
echo "Deploy complete"

Handling Python Dependencies in Custom Modules

If your modules require Python packages not in the base Odoo image:

# Option A: Install at container startup with a custom entrypoint
# entrypoint.sh
#!/bin/bash
pip3 install -r /mnt/extra-addons/requirements.txt --quiet
exec /entrypoint.sh "$@"
# Option B: Build a derived image (recommended for production)
FROM odoo:17
COPY requirements.txt /tmp/
RUN pip3 install --no-cache-dir -r /tmp/requirements.txt
USER odoo

Debugging Module Installation Failures

Common errors and fixes:

ErrorCauseFix
Module not foundWrong addons_path or module directory missing __manifest__.pyVerify path and that __manifest__.py exists in module root
ImportError on startupMissing Python dependencyInstall package in image or entrypoint
odoo.exceptions.AccessError on installRunning as wrong userEnsure container runs as odoo user, not root
Module installed but UI not updatedBrowser cacheHard refresh (Ctrl+Shift+R) or clear assets in Settings

Setting Up a CI/CD Pipeline

For teams using GitHub Actions:

# .github/workflows/deploy.yml
name: Deploy Odoo Modules
on:
  push:
branches: [main]

jobs:
  deploy:
runs-on: ubuntu-latest
steps:
  - uses: actions/checkout@v4

  - name: Build custom image
    run: |
      docker build -t ${{ secrets.REGISTRY }}/odoo:${{ github.sha }} .
      docker push ${{ secrets.REGISTRY }}/odoo:${{ github.sha }}

  - name: Deploy to server
    uses: appleboy/ssh-action@v1
    with:
      host: ${{ secrets.SERVER_HOST }}
      username: deploy
      key: ${{ secrets.SSH_KEY }}
      script: |
        cd /opt/odoo
        sed -i "s|image: .*|image: ${{ secrets.REGISTRY }}/odoo:${{ github.sha }}|" docker-compose.yml
        docker compose pull odoo
        docker compose stop odoo
        docker compose run --rm odoo odoo -d production -u your_module --stop-after-init --no-http
        docker compose up -d odoo

How DeployMonkey Handles Custom Modules

DeployMonkey's Git integration connects directly to your repository. Push to your configured branch, and we automatically pull the new code, run -u for changed modules, restart the container, and verify the healthcheck passes — all without SSH access to a server.

On the Professional plan ($29/month) and above, you get Git-based deployments with automatic module updates. This eliminates the entire SSH + docker exec workflow and gives you a proper deployment audit trail.

Try it free — bring your existing modules on day one.

Frequently Asked Questions

Can I mount multiple addons directories?

Yes. Add them as separate volume mounts and list all paths in addons_path, comma-separated. This is useful when you have both community modules and your own proprietary modules.

Do I need to restart the container after every code change?

For Python changes: yes, you need to restart (or reload worker processes) because Python imports are cached. For XML views and QWeb templates, you can reload without restarting if you use --dev=xml in development mode. For production, always restart.

What is the difference between --init and -u?

--init (or -i) installs a module for the first time, creating its database tables. -u upgrades an already-installed module, applying schema changes and reloading data. Using -u on an uninstalled module does nothing; you must use --init first.

How do I check that my module is visible to Odoo before installing?

Run docker compose exec odoo odoo shell -d your_db and then in the Python shell: env['ir.module.module'].search([('name', '=', 'your_module')]). If it returns nothing, Odoo cannot find the module — check your addons_path.