An Odoo container that starts before PostgreSQL is ready fails with "connection refused", exits or sits in a broken state, and you end up restarting it by hand. The fix is an Odoo Postgres Docker healthcheck: PostgreSQL reports when it actually accepts connections, and Docker Compose holds Odoo back until it does. This guide shows the complete setup with odoo:19 and postgres:16: a pg_isready healthcheck, depends_on with condition: service_healthy, restart: true, and a healthcheck for Odoo itself.
If you don't have a working Compose stack yet, start with the main Odoo Docker Compose guide and come back here.
Why plain depends_on does not make Odoo wait for the database
The short form of depends_on only controls start order:
services:
odoo:
image: odoo:19
depends_on:
- dbCompose starts the db container first, then Odoo, but it does not wait for PostgreSQL to finish initializing. On the first run PostgreSQL creates its data directory, runs init scripts and restarts itself, which can take several seconds. During that window it refuses connections.
The official Odoo image softens this a little: its entrypoint runs a small script that waits up to 30 seconds for PostgreSQL before launching Odoo. That covers a fast machine on a good day. It does not cover a slow disk, a large database recovering after a crash, or a database container that is recreated while Odoo keeps running. A real healthcheck covers all three.
Add a pg_isready healthcheck to PostgreSQL
The postgres image ships with pg_isready, a client tool that exits with status 0 when the server accepts connections. Use it as the healthcheck command:
services:
db:
image: postgres:16
environment:
POSTGRES_DB: postgres
POSTGRES_USER: odoo
POSTGRES_PASSWORD: odoo
volumes:
- db-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U odoo -d postgres"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
start_interval: 2s
restart: unless-stoppedWhat each field does:
- test: the command Docker runs inside the container.
CMD-SHELLruns it through the container's shell. Exit code 0 means healthy, 1 means unhealthy. - interval: time between checks once the container is running.
- timeout: a check that takes longer than this counts as failed.
- retries: consecutive failures needed before the container is marked
unhealthy. - start_period: grace period after start. Failures during it don't count toward
retries, but the first success marks the container healthy right away. - start_interval: how often to check during the start period, so a fast database is reported healthy within a couple of seconds instead of waiting a full
interval. It needs Docker Engine 25 or newer; drop it on older engines.
Pass the same user you set in POSTGRES_USER. Without -U, pg_isready uses the operating system user name (root), which still gets a correct answer but fills the PostgreSQL log with "role root does not exist" lines on every check.
A container's health moves through three states: starting during the start period, then healthy or unhealthy. You see it in the STATUS column of docker compose ps.
Make Odoo wait with condition: service_healthy
Now switch Odoo to the long form of depends_on:
services:
db:
image: postgres:16
environment:
POSTGRES_DB: postgres
POSTGRES_USER: odoo
POSTGRES_PASSWORD: odoo
volumes:
- db-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U odoo -d postgres"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
start_interval: 2s
restart: unless-stopped
odoo:
image: odoo:19
depends_on:
db:
condition: service_healthy
restart: true
ports:
- "8069:8069"
environment:
HOST: db
USER: odoo
PASSWORD: odoo
volumes:
- odoo-data:/var/lib/odoo
- ./addons:/mnt/extra-addons
healthcheck:
test: ["CMD-SHELL", "curl -fsS http://localhost:8069/web/health || exit 1"]
interval: 30s
timeout: 5s
retries: 3
start_period: 60s
restart: unless-stopped
volumes:
db-data:
odoo-data:condition accepts three values:
service_started: the default, same as the short form.service_healthy: wait until the dependency's healthcheck reportshealthy. This is the one you want for PostgreSQL.service_completed_successfully: wait until the dependency runs to completion with exit code 0. Useful for one-shot jobs, for example a container that restores a database dump before Odoo starts.
With service_healthy, docker compose up creates both containers, starts db, waits for it to turn healthy, and only then starts Odoo. If PostgreSQL never becomes healthy, Compose stops with an error that names the unhealthy dependency instead of leaving a half-started stack.
Restart Odoo when PostgreSQL restarts: restart: true
Odoo keeps a pool of open connections to PostgreSQL. When the database container is restarted or recreated (a new postgres:16 patch image, a changed environment variable), those connections break, and Odoo can keep serving errors until it is restarted too.
restart: true under the dependency tells Compose to restart Odoo after it restarts or updates the db service. It applies to restarts that Compose performs, such as docker compose restart db or docker compose up -d recreating db. It does not apply when the Docker daemon restarts a crashed container on its own because of its restart policy. It needs Docker Compose 2.17 or newer; check yours with docker compose version.
A related option is required: false (Compose 2.20+), which turns a missing dependency into a warning instead of an error. Don't use it for PostgreSQL: Odoo cannot run without its database.
A healthcheck for Odoo itself
Odoo 19 exposes /web/health, an unauthenticated route that returns HTTP 200 with {"status": "pass"} when the server answers requests. The official image includes curl, so the check above works without extra packages. curl -f turns any HTTP error into a non-zero exit code.
The route also accepts ?db_server_status=1, which makes Odoo open a test connection to PostgreSQL and return HTTP 500 if it fails. That is useful for an external monitor. Inside the container healthcheck the plain route is the better choice, because PostgreSQL already has its own check and you don't want a short database blip to mark Odoo unhealthy as well.
Give Odoo a longer start_period than the database. The first start on an empty database, or a start that runs module updates, takes a while.
unhealthy keeps running. Docker only reports the status; restart: unless-stopped acts when the process exits, not when a healthcheck fails. Health status drives depends_on, docker compose up --wait and your monitoring. Automatic replacement of unhealthy tasks is a Docker Swarm feature.Wait for the database in scripts and CI
When a script or CI job starts the stack, let Compose do the waiting:
docker compose up -d --wait
--wait returns once services are running and, where a healthcheck is defined, healthy. It exits non-zero if a service fails, so the next step (running tests, restoring a dump) never starts against a database that isn't ready. Add --wait-timeout 120 to cap how long it waits.
The same pattern works for one-off Odoo commands. docker compose run starts the dependencies first and honors their conditions, so this waits for a healthy database before installing a module:
docker compose run --rm odoo odoo -d mydb -i sale --stop-after-init
Debug a failing healthcheck
See the current status of every service:
docker compose ps
Read the last results, including the output of each check:
docker inspect --format '{{json .State.Health}}' $(docker compose ps -q db)
Run the check by hand inside the container to see the real error:
docker compose exec db pg_isready -U odoo -d postgres
docker compose exec odoo curl -fsS http://localhost:8069/web/health
Common causes:
- Wrong user or database in the test. The
-Uand-dvalues must exist.postgresalways exists as a database; the user must matchPOSTGRES_USER. - Timeout too short. A busy server under load can take more than a second to answer. Five seconds is a safe default.
- Odoo marked unhealthy during long upgrades. Raise
start_period, or run the upgrade as a one-offdocker compose runbefore starting the service. - Connection settings mismatch.
HOST,USERandPASSWORDon the Odoo service must match the database service name and itsPOSTGRES_USER/POSTGRES_PASSWORD. Odoo's own logs say so directly; see reading Odoo Docker logs.
Hard-coded passwords are fine on a laptop. For anything shared, move them into an env file or secrets as shown in Odoo Docker environment variables, and give PostgreSQL persistent storage as described in Odoo Docker volumes.
Skip the plumbing
Healthchecks, restart rules and monitoring are part of running Odoo yourself. If you'd rather not maintain them, Easy Instance runs Odoo 17 to 20 with monitoring and daily backups included and staging copies on demand, priced per Odoo worker (see pricing). You can try it free for 10 days with no credit card.