Odoo Postgres Docker Healthcheck: Wait for the Database

Set up an Odoo Postgres Docker healthcheck with pg_isready, depends_on service_healthy and restart: true so Odoo always waits for its database.

Updated . 7 min read

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:

compose.yaml (start order only)
services:
  odoo:
    image: odoo:19
    depends_on:
      - db

Compose 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:

compose.yaml (db service)
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

What each field does:

  • test: the command Docker runs inside the container. CMD-SHELL runs 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:

compose.yaml (complete)
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 reports healthy. 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 does not mean restarted. With plain Docker and Docker Compose, a container that turns 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 -U and -d values must exist. postgres always exists as a database; the user must match POSTGRES_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-off docker compose run before starting the service.
  • Connection settings mismatch. HOST, USER and PASSWORD on the Odoo service must match the database service name and its POSTGRES_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.

Rather not run the server yourself?

Get managed Odoo hosting with backups, SSL, monitoring and updates handled for you. Free for 10 days.