A batteries-included template for running Odoo 18 in Docker, with
everything you typically reach for in real projects already wired up:
PostgreSQL, Nginx, pgAdmin / Adminer, message brokers (RabbitMQ + Kafka),
a full monitoring stack (Prometheus + Grafana + exporters), per-service
compose files, a documented Makefile, helper bash scripts, and curated
custom addons.
Use this as a starting point for new Odoo 18 projects, or as a reference when wiring an existing project to brokers / observability.
- Stack overview
- Quick start
- Repository layout
- Docker services
- Custom addons
- Odoo 18
- Makefile guide
- Helper scripts (
scripts/) - Backups
- Monitoring (Prometheus + Grafana)
- Message brokers (RabbitMQ + Kafka)
- Redis
- Requirements (
requirements/) - Testing (
tests/) - Python tooling (
pyproject.toml) - Environment variables
- Deeper docs (
docs/) - Troubleshooting
Host ──► nginx ─► odoo 18 ──► postgres 15
│
├─► pgAdmin / Adminer
├─► Redis 7 (redis/)
├─► RabbitMQ 3.13 (message_brokers/rabbitmq)
├─► Kafka 3.7 KRaft (message_brokers/kafka)
└─► Prometheus + Grafana + exporters
(monitoring/)
Every service has its own compose file under
docker_compose/<service>/. They join a shared external Docker network
(backend) so they can resolve each other by container name.
git clone <this-repo> odoo-stack
cd odoo-stack
# 1. Bootstrap: copy .env.example -> .env, create the backend network and folders
make init
# 2. Edit .env and set strong values (passwords, ports, etc.)
$EDITOR .env
# 3. Bring up the core stack: postgres + odoo + nginx + pgadmin + adminer
make up
# Optional extras:
make up-redis # Redis + redis-commander + redis_exporter
make up-brokers # RabbitMQ + Kafka
make up-monitoring # Prometheus + Grafana + exporters
# Or everything at once:
make up-full
# Day to day:
make ps # which containers are up
make logs-odoo # tail Odoo logs
make health # status of every stack container
make down # stop the core stackThen open:
| URL | Service | Credentials (default) |
|---|---|---|
| http://localhost:5433 | Odoo (via nginx) | created on first run, web manager |
| http://localhost:5050 | pgAdmin | admin@admin.com / admin |
| http://localhost:8080 | Adminer | postgres / odoo / (POSTGRES_PASSWORD) |
| http://localhost:8081 | redis-commander | admin / admin |
| http://localhost:15672 | RabbitMQ UI | odoo / odoo |
| http://localhost:8089 | kafka-ui | — |
| http://localhost:9090 | Prometheus | — |
| http://localhost:3000 | Grafana | admin / admin |
Change every password before exposing the stack outside localhost.
.
├── Makefile # workflow targets (see `make help`)
├── README.md # this file
├── .env / .env.example # environment variables
├── .gitignore / .dockerignore
├── backups/ # on-demand DB dumps (gitignored, .gitkeep)
├── logs/ # bind-mounted log directory
│ ├── nginx/
│ └── odoo/
├── docs/ # extended documentation (.md / .mdx)
│ ├── setup.md
│ ├── services.md
│ ├── odoo-18.md
│ ├── addons.md
│ ├── makefile.md
│ ├── monitoring.md
│ ├── message-brokers.md
│ ├── backup-restore.md
│ ├── development.md
│ ├── architecture.mdx
│ └── troubleshooting.mdx
├── docker_compose/
│ ├── db/ # PostgreSQL 15 + backup sidecar
│ ├── odoo/ # Odoo 18 + nginx
│ ├── nginx/ # nginx configs (dev + prod)
│ ├── pgadmin/ # pgAdmin 4
│ ├── adminer/ # Adminer
│ ├── redis/ # Redis 7 + redis-commander + redis_exporter
│ ├── message_brokers/
│ │ ├── rabbitmq/ # RabbitMQ 3.13 (Mgmt + Prom plugin)
│ │ └── kafka/ # Kafka 3.7 KRaft + kafka-ui + exporter
│ └── monitoring/ # Prometheus + Grafana + exporters
│ ├── prometheus/
│ └── grafana/
├── requirements/ # pip requirement files
│ ├── base-requirements.txt
│ ├── dev-requirements.txt
│ └── all-requirements.txt
├── scripts/ # helper bash scripts
│ ├── init.sh
│ ├── backup-db.sh
│ ├── restore-db.sh
│ ├── install-addon.sh
│ ├── upgrade-addon.sh
│ ├── shell.sh
│ ├── psql.sh
│ ├── healthcheck.sh
│ ├── clean-backups.sh
│ ├── wait-for-postgres.sh
│ └── lint-addons.sh
├── tests/ # host-side pytest suite
│ ├── conftest.py
│ ├── unit/ # layout / manifest / compose sanity
│ ├── integration/ # require RUN_INTEGRATION=1 + running stack
│ └── addons/ # pytest-odoo skeletons
├── pyproject.toml # black / isort / ruff / mypy / pytest config
└── src/
├── addons/ # custom + OCA-style addons
│ ├── base_search_fuzzy/
│ ├── eqp_backup/
│ ├── password_security/
│ ├── web_notify/
│ ├── website_menu_by_user_status/
│ └── website_require_login/
├── configs/odoo.conf # mounted to /etc/odoo/odoo.conf
└── patches/ # space for runtime patches
| Service | Container | Port (host) | Compose file |
|---|---|---|---|
| Odoo 18 | odoo |
(via nginx) | docker_compose/odoo/docker-compose.yml |
| Nginx | nginx |
5433 → 81 | bundled in the Odoo compose |
| PostgreSQL 15 | postgres |
5435 → 5432 | docker_compose/db/docker-compose.yml |
| Postgres backup | postgres_backup |
— | same compose file as postgres |
| pgAdmin 4 | pgadmin |
5050 → 80 | docker_compose/pgadmin/docker-compose.yml |
| Adminer | adminer-postgres |
8080 | docker_compose/adminer/docker-compose.yml |
| Redis 7 | redis |
6379 | docker_compose/redis/docker-compose.yml |
| redis-commander | redis_commander |
8081 | same compose as redis |
| redis_exporter | redis_exporter |
9121 (int) | same compose as redis |
| RabbitMQ 3.13 | rabbitmq |
5672, 15672, 15692 | docker_compose/message_brokers/rabbitmq/ |
| Kafka 3.7 (KRaft) | kafka |
9094 | docker_compose/message_brokers/kafka/ |
| kafka-ui | kafka_ui |
8089 → 8080 | same compose as kafka |
| kafka_exporter | kafka_exporter |
9308 (int) | same compose as kafka |
| Prometheus | prometheus |
9090 | docker_compose/monitoring/docker-compose.yml |
| Grafana | grafana |
3000 | same compose as prometheus |
| node_exporter | node_exporter |
9100 (int) | same compose as prometheus |
| postgres_exporter | postgres_exporter |
9187 (int) | same compose as prometheus |
| cAdvisor | cadvisor |
8080 (int) | same compose as prometheus |
Production variants live alongside the dev files as
docker-compose-prod.yml and are wired up through the *-prod Make targets.
See docs/services.md and
docs/architecture.mdx for more.
All addons live under src/addons/ and are mounted into the Odoo container at
/mnt/extra-addons (already in odoo.conf's addons_path).
| Addon | Version | Summary |
|---|---|---|
base_search_fuzzy |
18.0.1.0.0 | Fuzzy search via PostgreSQL pg_trgm. |
eqp_backup |
18.0.1.0 | UI for scheduling Odoo DB / filestore backups directly from inside Odoo. |
password_security |
18.0.1.0.0 | Password complexity, expiration, history, lockout for Odoo users. |
web_notify |
18.0.1.1.1 | Send toast/notification messages to specific Odoo users from Python. |
website_menu_by_user_status |
18.0.1.0.0 | Conditionally show/hide website.menu items based on user state. |
website_require_login |
18.0.1.0.0 | Lock the public website behind authentication. |
Install / upgrade / test:
make install-addon ADDON=web_notify
make upgrade-addon ADDON=web_notify
make odoo-test MODULES=base_search_fuzzy,password_securitySee docs/addons.md for the full guide (manifest layout,
how to scaffold a new addon, live-reload tips).
This stack pins the official odoo:18 image and runs in proxy_mode
behind nginx with 3 workers and 2 cron threads.
| Concern | Where |
|---|---|
| Image | docker_compose/odoo/Dockerfile (adds fonts on top of odoo:18) |
| Config | src/configs/odoo.conf → /etc/odoo/odoo.conf (ro) |
| Custom addons | src/addons/ → /mnt/extra-addons |
| Filestore | named volume odoo-data → /var/lib/odoo |
| Server logs | ./logs/odoo-server.log (bind-mounted from host) |
| Nginx access / errors | ./logs/nginx/ |
| Admin password | ODOO_ADMIN_PASSWD in .env and odoo.conf |
See docs/odoo-18.md for image internals, useful CLI
flags, and notes on what changed in Odoo 18.
Run make help for the always-up-to-date list. Naming convention:
up-<svc> bring service up in the background
up-<svc>-build rebuild image and bring up
down-<svc> stop & remove containers
restart-<svc> down then up
logs-<svc> tail container logs (last 200 + follow)
shell-<svc> exec bash inside the container
up alias for the core dev stack
up-full core + redis + brokers + monitoring
*-prod same target, prod compose
Most-used:
| Target | What it does |
|---|---|
make help |
List every target with its description. |
make init |
Create .env, network, and folders (idempotent). |
make up |
postgres + odoo + nginx + pgadmin + adminer. |
make up-full |
Above + redis + brokers + monitoring. |
make down / make down-full |
Stop core / everything. |
make restart |
Restart the core stack. |
make ps |
List containers on the backend network. |
make logs-odoo / make logs-db / ... |
Tail one service. |
make health |
One-line status for every container. |
make backup [DB=<db>] |
On-demand pg_dump into ./backups/. |
make restore-db FILE=<path> [DB=<db>] |
Drop & recreate the DB, then restore. |
make load-backup FILE=<file> |
pg_restore from inside the backup sidecar. |
make install-addon ADDON=<name> [DB=<db>] |
Install an Odoo addon. |
make upgrade-addon ADDON=<name> [DB=<db>] |
Upgrade an Odoo addon. |
make odoo-shell |
Open the Odoo Python shell. |
make odoo-psql |
Open psql against the Odoo DB. |
make odoo-test MODULES=a,b |
Run tests for the listed modules. |
make up-redis / make down-redis |
Start / stop Redis stack. |
make cli-redis |
Open redis-cli authenticated with $REDIS_PASSWORD. |
make clean-backups [DAYS=<n>] |
Purge old backup files (default 14 days). |
make lint |
ruff + pylint-odoo against src/addons. |
make format |
black + isort over src/. |
make stop-all / rm-all / prune |
Host-wide docker cleanup. |
Full reference: docs/makefile.md.
All wrappers around docker exec / pg_dump / odoo. They read .env
from the repo root.
| Script | What it does |
|---|---|
init.sh |
.env + docker network + required folders. |
backup-db.sh [db] |
pg_dump of $ODOO_DB into ./backups/. |
restore-db.sh <file> [db] |
Drop + recreate DB, load .sql or .dump. |
install-addon.sh <a> [db] |
odoo -i <addon> --stop-after-init. |
upgrade-addon.sh <a> [db] |
odoo -u <addon> --stop-after-init. |
shell.sh [db] |
odoo shell REPL inside the odoo container. |
psql.sh [db] |
psql inside the postgres container. |
healthcheck.sh |
One-line status for every stack container. |
clean-backups.sh [days] |
Delete backups older than N days (default 14). |
wait-for-postgres.sh [t] |
Block until pg_isready. Useful in CI / init paths. |
lint-addons.sh |
ruff + pylint-odoo on src/addons. |
After cloning:
chmod +x scripts/*.shTwo backup paths, intentionally:
-
Automatic sidecar —
postgres_backupcontainer next to Postgres does a dailypg_dumpintodocker_compose/db/backups/. Restore with:make load-backup FILE=backup_prod_2025-10-14.sql
-
On-demand via
./scripts/, writing to the top-level./backups/folder:make backup # dumps $ODOO_DB make backup DB=staging # different db make restore-db FILE=backups/...sql # drop+recreate then load make clean-backups DAYS=30 # retention sweep
Both backup folders are git-ignored but kept around via .gitkeep.
The filestore (odoo-data volume) is not included in pg_dump; see
docs/backup-restore.md for a full-stack snapshot
recipe.
Located in docker_compose/monitoring/.
make up-monitoring
make logs-monitoring
make down-monitoring| Container | Image | Purpose |
|---|---|---|
prometheus |
prom/prometheus:v2.54.1 |
Scraping + TSDB (15d retention) |
grafana |
grafana/grafana:11.2.0 |
Dashboards & alerting |
node_exporter |
prom/node-exporter:v1.8.2 |
Host CPU / mem / disk |
postgres_exporter |
prometheuscommunity/postgres-exporter:v0.15.0 |
Postgres metrics |
cadvisor |
gcr.io/cadvisor/cadvisor:v0.49.1 |
Per-container metrics |
Grafana is auto-provisioned with the Prometheus datasource. Drop dashboard
JSON files into docker_compose/monitoring/grafana/dashboards/ and they
appear automatically. Starter alert rules ship in
docker_compose/monitoring/prometheus/alerts.yml.
Full guide: docs/monitoring.md.
Located in docker_compose/message_brokers/. Independent compose files —
run only what you need.
make up-rabbitmq # AMQP 5672, UI 15672, /metrics 15692Pre-declares a topic exchange odoo.events and a queue
odoo.events.default bound to # (see definitions.json).
make up-kafka # PLAINTEXT kafka:9092 (in-net), localhost:9094 (host)Includes kafka-ui at http://localhost:8089 and a Prometheus exporter wired into the monitoring stack.
Full guide: docs/message-brokers.md.
Located in docker_compose/redis/. Single-node Redis 7 with a web UI and a
Prometheus exporter — useful as a cache, queue backend, session store, or
generic pub/sub for custom Odoo modules.
make up-redis # redis:6379 + redis-commander:8081 + redis_exporter
make logs-redis
make cli-redis # interactive redis-cli (uses $REDIS_PASSWORD)
make down-redis| Container | Image | Port | Purpose |
|---|---|---|---|
redis |
redis:7.4-alpine |
6379 | Cache / queue / pub-sub |
redis_commander |
rediscommander/redis-commander:latest |
8081 | Web UI (basic-auth protected) |
redis_exporter |
oliver006/redis_exporter |
9121 (int) | Prometheus metrics |
The custom redis.conf enables AOF persistence (appendfsync everysec) and
RDB snapshots, caps memory at 256mb with allkeys-lru eviction, and
disables FLUSHALL / CONFIG in shared dev environments. The password is
supplied at container start via --requirepass $REDIS_PASSWORD.
From another container on backend:
import redis
client = redis.Redis(
host="redis", port=6379,
password="redis", # $REDIS_PASSWORD
decode_responses=True,
)
client.set("hello", "world")From the host, swap host="redis" for host="localhost".
prometheus.yml already scrapes the exporter on redis_exporter:9121, so
metrics show up in Grafana as soon as both stacks are running.
Folder README: docker_compose/redis/README.md.
Pip requirement files for host-side linting / scripting. The official Odoo image already ships its own Python dependencies; these are extras the custom addons or helper scripts use.
requirements/
├── base-requirements.txt # runtime libs (psycopg2, requests, pika, kafka-python, ...)
├── dev-requirements.txt # tooling (ruff, black, isort, pylint-odoo, pytest-odoo, mkdocs, ...)
└── all-requirements.txt # -r base + -r dev
Highlights:
pika,kafka-python— clients for the bundled message brokers.prometheus-client— expose custom metrics from inside Odoo.sentry-sdk,structlog— error tracking + structured logging.openpyxl,xlsxwriter,reportlab— Excel / PDF reports.qrcode,Pillow— barcodes / images.pytest-odoo— Odoo-aware test runner.pylint-odoo,ruff,black,isort— code quality.mkdocs-material— render thedocs/folder locally.
Install everything:
python -m venv .venv
source .venv/bin/activate
pip install -r requirements/all-requirements.txtHost-side test suite, separate from the Odoo-internal addon tests under
src/addons/*/tests/. Configured through pyproject.toml
([tool.pytest.ini_options]).
tests/
├── conftest.py # shared fixtures: project_root, addons_dir, env, ...
├── unit/ # cheap checks — no Docker required
│ ├── test_project_layout.py # required files / folders / executable scripts
│ ├── test_env_example.py # .env.example declares every key the stack uses
│ ├── test_addon_manifests.py # every __manifest__.py is valid + pinned to 18.x
│ ├── test_compose_files.py # every docker-compose.yml is valid YAML
│ └── test_makefile_targets.py # Makefile declares the expected targets
├── integration/ # require RUN_INTEGRATION=1 + a running stack
│ └── test_postgres_connection.py
└── addons/ # pytest-odoo entry points (skeleton)
└── test_web_notify_sample.py
make test # pytest tests/unit
make test-integration # RUN_INTEGRATION=1 pytest tests/integration
make test-cov # pytest tests/unit --cov --cov-report=term-missing
# Odoo-side tests (boot a real Odoo registry):
make odoo-test MODULES=base_search_fuzzy,password_security| Marker | Meaning |
|---|---|
unit |
Pure Python, no DB, no Docker. |
integration |
Requires the Docker stack to be running. |
odoo |
Boots an Odoo registry via pytest-odoo. |
slow |
Long-running tests; deselect with -m 'not slow'. |
Integration tests are auto-skipped unless RUN_INTEGRATION=1 is set — that
gate lives in tests/conftest.py.
Full details: tests/README.md.
All host-side Python tools share one config file at the project root.
| Section | Tool | What it controls |
|---|---|---|
[project] |
metadata | name, version, Python ≥ 3.11, license, URLs. |
[tool.black] |
black | Line length 100, target Python 3.11, excludes static/migrations. |
[tool.isort] |
isort | Black profile, custom ODOO section for import odoo.*. |
[tool.ruff] / [tool.ruff.lint] |
ruff | E/W/F/I/B/UP/SIM/C4/RUF rule sets, Odoo-friendly ignores. |
[tool.mypy] |
mypy | Python 3.11, ignore missing imports (Odoo isn't pip-installable). |
[tool.pytest.ini_options] |
pytest | testpaths = tests, src/addons, markers, default addopts. |
[tool.coverage.*] |
coverage.py | Branch coverage on src/addons and scripts. |
[tool.pylint.*] |
pylint-odoo | Loaded via scripts/lint-addons.sh. |
Running the tools:
make format # black + isort on src/ and tests/
make lint # ruff + pylint-odoo on src/addons
make test # pytest tests/unit
make test-cov # + coverage reportEverything reads from pyproject.toml, so editor integrations (VS Code's
Python extension, PyCharm's inspections, etc.) pick up the same settings as
CI.
.env.example is the source of truth. Copy it to .env (handled by
make init) and edit. Sections:
| Section | Variables |
|---|---|
| PostgreSQL | POSTGRES_DB, POSTGRES_USER, POSTGRES_PASSWORD, ODOO_DB |
| Odoo / Nginx | ODOO_ADMIN_PASSWD, NGINX_PORT |
| pgAdmin | PGADMIN_DEFAULT_EMAIL, PGADMIN_DEFAULT_PASSWORD |
| Redis | REDIS_PORT, REDIS_PASSWORD, REDIS_UI_PORT, REDIS_UI_USER, REDIS_UI_PASSWORD |
| RabbitMQ | RABBITMQ_USER, RABBITMQ_PASSWORD, RABBITMQ_VHOST, RABBITMQ_PORT, RABBITMQ_UI_PORT, RABBITMQ_PROM_PORT |
| Kafka | KAFKA_EXTERNAL_HOST, KAFKA_EXTERNAL_PORT, KAFKA_UI_PORT |
| Monitoring | PROMETHEUS_PORT, GRAFANA_PORT, GRAFANA_USER, GRAFANA_PASSWORD |
.env is git-ignored.
| File | What |
|---|---|
docs/setup.md |
First-time bootstrap and OS requirements. |
docs/services.md |
Every container + ports + network hostnames. |
docs/odoo-18.md |
Odoo 18 image / config notes. |
docs/addons.md |
Bundled addons + how to add your own. |
docs/makefile.md |
Make target reference. |
docs/monitoring.md |
Prometheus + Grafana playbook. |
docs/message-brokers.md |
RabbitMQ + Kafka with code samples. |
docs/redis.md |
Redis 7 setup, config, and usage examples. |
docs/backup-restore.md |
Both backup paths and full restore recipes. |
docs/development.md |
Local Python tooling, tests, pre-commit. |
docs/architecture.mdx |
ASCII topology + volumes / bind-mounts. |
docs/troubleshooting.mdx |
Common failures and fixes. |
Quick cheats — full list in
docs/troubleshooting.mdx.
| Symptom | Fix |
|---|---|
network backend ... could not be found |
make network (or make init). |
| Odoo restart loop, "auth failed" | .env and odoo.conf passwords disagree. |
| Port 5433 already bound | Change NGINX_PORT in .env. |
| Restore fails: "database is being accessed" | pg_terminate_backend(...) open sessions. |
| Prometheus target down | Run make health, check prometheus.yml ports. |
| Grafana datasource missing | Recreate the volume: docker volume rm monitoring_grafana_data. |
Each bundled addon keeps the license declared in its own __manifest__.py
(LGPL-3 / AGPL-3). The rest of this template is provided as-is.