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:
- Volume mount — mount a host directory into the container at runtime
- Custom Docker image — COPY modules into a derived image at build time
- 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/enterpriseas 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:
| Error | Cause | Fix |
|---|---|---|
| Module not found | Wrong addons_path or module directory missing __manifest__.py | Verify path and that __manifest__.py exists in module root |
| ImportError on startup | Missing Python dependency | Install package in image or entrypoint |
| odoo.exceptions.AccessError on install | Running as wrong user | Ensure container runs as odoo user, not root |
| Module installed but UI not updated | Browser cache | Hard 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.