Deployment¶
Worker Process¶
The runner is a standalone Python process that connects to your Odoo databases and continuously pulls and executes jobs. It is the recommended way to run jobs in production.
Runner (recommended)¶
Use one of the two bundled entry points. Both start the multi-database
QueueJobRunner via QueueJobRunner.from_environ_or_config(), so runner settings
are read from the environment (see Configuration).
# Standalone launcher script
python job_worker_runner.py -c /etc/odoo/odoo.conf
# Or invoke the runner as a module
python -m odoo.addons.job_worker.cli -c /etc/odoo/odoo.conf
The runner:
- Discovers databases with
job_workerinstalled - Spawns a
QueueWorkerthread per database - Monitors thread health and restarts crashed workers
- Quarantines databases with repeated failures
- Uses PostgreSQL advisory locks to prevent duplicate runners
- Writes a heartbeat file for container health checks (see below)
Single-Database Worker (advanced)¶
If you need to run a single unsupervised worker for one database — for example in a
constrained container — you can instantiate QueueWorker directly:
# run_worker.py
import odoo
from odoo.tools import config
from odoo.addons.job_worker.cli.worker import QueueWorker
config.parse_config([
"-c", "/etc/odoo/odoo.conf",
"-d", "production",
])
odoo.service.server.load_server_wide_modules()
registry = odoo.modules.registry.Registry(config["db_name"])
worker = QueueWorker(config["db_name"])
worker.run()
No supervision or heartbeat
A bare QueueWorker is not restarted on crash and does not write the
heartbeat file the container healthcheck relies on. Prefer the runner unless you
have a specific reason not to.
Signals¶
| Signal | Behavior |
|---|---|
SIGTERM |
Graceful shutdown — finishes running jobs, then exits |
SIGINT |
Same as SIGTERM |
Docker Compose¶
Example docker-compose.yml for running the job worker alongside Odoo. This is a
generic illustration; the repository's own docker/docker-compose.yml is a
test harness, not a production template.
services:
postgres:
image: postgres:18-alpine
environment:
POSTGRES_DB: odoo
POSTGRES_USER: odoo
POSTGRES_PASSWORD: odoo
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD", "pg_isready", "-U", "odoo"]
interval: 5s
timeout: 5s
retries: 5
odoo:
image: your-odoo-image:19.0
depends_on:
postgres:
condition: service_healthy
ports:
- "8069:8069"
volumes:
- ./addons:/mnt/extra-addons
environment:
- HOST=postgres
- USER=odoo
- PASSWORD=odoo
job-worker:
image: your-odoo-image:19.0
depends_on:
postgres:
condition: service_healthy
volumes:
- ./addons:/mnt/extra-addons
environment:
- HOST=postgres
- USER=odoo
- PASSWORD=odoo
command: python /mnt/extra-addons/odoo-job-worker/job_worker_runner.py -c /etc/odoo/odoo.conf
restart: unless-stopped
volumes:
pgdata:
Scaling workers
Run multiple job-worker containers to increase throughput. Each worker acquires
jobs independently via FOR UPDATE SKIP LOCKED, so there is no double-execution
risk.
Health checks¶
The QueueJobRunner has no HTTP endpoint, so an orchestrator cannot probe it with
an HTTP healthcheck. Instead the runner writes a small heartbeat file on every
healthy iteration of its supervisor loop, and the bundled
job_worker_healthcheck.py script reports whether that file is fresh.
A stale heartbeat means one of:
- the runner process is gone (the file is never refreshed),
- the supervisor loop has stopped iterating (hung / deadlocked), or
- the worker fleet is degraded — the runner deliberately withholds the heartbeat while any database is quarantined (its worker has crashed past the failure threshold), so a persistently broken database surfaces as unhealthy rather than flapping after each rediscovery.
The script imports only the standard library (it does not bootstrap Odoo), so
it is fast enough to run as a container HEALTHCHECK:
job-worker:
image: your-odoo-image:19.0
command: python /mnt/extra-addons/odoo-job-worker/job_worker_runner.py -c /etc/odoo/odoo.conf
healthcheck:
test: ["CMD-SHELL", "python /mnt/extra-addons/odoo-job-worker/job_worker_healthcheck.py || exit 1"]
interval: 30s
timeout: 10s
start_period: 60s
retries: 3
restart: unless-stopped
Both the writer and the checker resolve the same settings from the environment:
| Variable | Default | Purpose |
|---|---|---|
JOB_WORKER_HEARTBEAT_FILE |
/tmp/job_worker_heartbeat |
Path of the heartbeat file |
JOB_WORKER_HEARTBEAT_MAX_AGE |
60 |
Seconds before the heartbeat is considered stale |
Runner-only
The heartbeat is emitted by the multi-database QueueJobRunner. A bare
single-database QueueWorker (see Single-Database Worker)
does not write a heartbeat file.
systemd Service¶
Create a systemd unit for the worker:
# /etc/systemd/system/odoo-job-worker.service
[Unit]
Description=Odoo Job Worker
After=postgresql.service
Requires=postgresql.service
[Service]
Type=simple
User=odoo
Group=odoo
ExecStart=/usr/bin/python3 /opt/odoo/addons/odoo-job-worker/job_worker_runner.py -c /etc/odoo/odoo.conf
Restart=on-failure
RestartSec=5
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
Enable and start:
sudo systemctl daemon-reload
sudo systemctl enable odoo-job-worker
sudo systemctl start odoo-job-worker
Monitor logs:
Production Checklist¶
- [ ] Worker process is managed by a process supervisor (systemd, Docker, etc.)
- [ ]
restartpolicy is configured for automatic recovery - [ ] Channel concurrency limits are set appropriately via
queue.limitrecords - [ ] Job retention period is configured (
job_worker.done_job_retention_days) - [ ] PostgreSQL connection pool is sized for worker concurrency
- [ ]
job_worker_monitoris installed for dashboard and alerts - [ ] Log aggregation is set up for worker process output