ReadyWait: Docker Compose depends_on & Healthcheck Checker
Stop "connection refused" on docker compose up. ReadyWait flags depends_on entries that only wait for a database container to start - not to be ready - and service_healthy conditions that can never be satisfied, right in the editor, with one-click fixes.
The problem
services:
app:
build: .
depends_on:
- db # waits for the container to start, not for Postgres to accept connections
db:
image: postgres:16
Short-form depends_on is the same as condition: service_started. The db container is running within a second, but Postgres is still initializing, so app boots, tries to connect and crashes with ECONNREFUSED / connection refused / Can't connect to MySQL server. The fix is condition: service_healthy - which in turn only works when the dependency has a healthcheck. The official postgres, mysql, mariadb, mongo, redis and rabbitmq images define no HEALTHCHECK, so service_healthy without a compose healthcheck: makes docker compose up fail with "container is unhealthy" / "has no healthcheck configured".
References: depends_on, healthcheck, Control startup order.
Rules
| Rule |
Severity |
What it flags |
| RW001 |
Warning |
Short-form depends_on (or condition: service_started) on a stateful or slow service: postgres, mysql, mariadb, mongo, redis, valkey, kafka, zookeeper, rabbitmq, elasticsearch, opensearch, cassandra, clickhouse, minio, localstack, keycloak (configurable) |
| RW002 |
Warning |
condition: service_healthy targeting a service with no healthcheck: in any compose file (or a disabled one), and no HEALTHCHECK in its local Dockerfile |
| RW003 |
Information |
A healthcheck with no start_period, or a timeout longer than its interval |
| RW004 |
Information |
condition: service_completed_successfully on a service with restart: always or unless-stopped - contradictory |
Quick Fixes
- RW001 rewrites the
depends_on block to long form with condition: service_healthy for the database (other entries keep service_started), and when the target has no healthcheck, inserts one:
depends_on:
db:
condition: service_healthy
db:
image: postgres:16
healthcheck:
test: ["CMD-SHELL", "pg_isready -h 127.0.0.1 -U $${POSTGRES_USER:-postgres}"]
interval: 5s
timeout: 5s
retries: 10
start_period: 30s
- RW002 inserts the same kind of healthcheck into the target service.
Healthcheck commands are included only where the tool ships in the image:
| Image |
test |
| postgres |
pg_isready -h 127.0.0.1 -U $${POSTGRES_USER:-postgres} |
| mysql |
mysqladmin ping -h 127.0.0.1 |
| mariadb (official image) |
healthcheck.sh --connect --innodb_initialized (mysqladmin ping -h 127.0.0.1 for tags before 10.4) |
| mongo |
mongosh --quiet --eval 'db.adminCommand({ping: 1})' (falls back to mongo) |
| redis / valkey |
redis-cli ping / valkey-cli ping, reply checked (below) |
| rabbitmq |
rabbitmq-diagnostics -q ping |
| cassandra |
cqlsh -e 'describe keyspaces' |
Why these exact commands:
- Over TCP, not the socket. On first boot the postgres and mysql entrypoints run a temporary server for the init scripts that does not listen on TCP.
pg_isready over the Unix socket reports ready during that phase; -h 127.0.0.1 waits for the real server.
$$ is intentional. Compose interpolates ${...} itself; $$ passes a literal $ so the container's shell reads its own POSTGRES_USER.
- No password needed.
pg_isready and mysqladmin ping report a running server even when the login is refused, so the check works with any credentials.
- redis / valkey match the reply.
redis-cli ping exits 0 on every reply, including LOADING (dataset still loading) and NOAUTH (requirepass set). The check counts PONG or NOAUTH as ready, so it works with a password and does not pass while data is still loading.
test: ["CMD-SHELL", "redis-cli ping 2>&1 | grep -qE 'PONG|NOAUTH'"]
- mariadb's
healthcheck.sh ships only in the official image (MariaDB 11 has no mysqladmin), so other mariadb images get no generated healthcheck.
For kafka, elasticsearch, opensearch, clickhouse, minio, localstack, keycloak and zookeeper, the healthcheck depends on the image flavor and security settings, so ReadyWait converts the condition but leaves writing the healthcheck to you (RW002 then reminds you).
How it reads your project
- Scans
compose*.yml|yaml and docker-compose*.yml|yaml in the workspace.
- Per folder, the first base file (
compose.yaml, compose.yml, docker-compose.yml, docker-compose.yaml) and the first override file (compose.override.yml, compose.override.yaml, docker-compose.override.yml, docker-compose.override.yaml) are merged exactly as a plain docker compose up does, so a healthcheck or condition defined in the override counts.
- Any other compose file in the folder (
compose.prod.yaml, docker-compose.test.yml...) is checked as an overlay on the base file, like docker compose -f compose.yaml -f compose.prod.yaml. It never hides a problem in the base file, because docker compose does not load it by default.
- Follows
extends: in the same file or through file: inside the workspace: the image, build, restart policy, healthcheck (merged key by key) and depends_on are inherited. A depends_on inherited this way is reported on the extends: line.
- For services built locally, reads the Dockerfile named by
build: and honors a HEALTHCHECK in the stage that is built (build.target or the last stage).
- Status bar shows the finding count; click it (or run ReadyWait: Show depends_on & Healthcheck Report) for a Markdown table of services, healthchecks, dependencies and findings.
- Activity bar: the ReadyWait view lists every finding grouped by compose file, each row named after the exact relation (
RW001 api -> db, RW003 db: no start_period). Click a row to jump to the line; the inline Use service_healthy and Add healthcheck buttons apply the Quick Fix. The badge shows the finding count, and the title bar has Rescan and Show Report.
Settings
| Setting |
Default |
Description |
readyWait.enable |
true |
Turn diagnostics on or off |
readyWait.statefulImages |
list above |
Image names treated as slow-to-become-ready |
readyWait.disabledRules |
[] |
Rule codes to turn off, e.g. ["RW003"] |
readyWait.maxFiles |
200 |
Maximum compose files scanned |
Privacy and safety
ReadyWait is fully static: it only reads files in your workspace. No network access, no telemetry, no child processes, no docker calls. It runs in untrusted workspaces.
DCLint is a command-line Compose linter focused on style and structure (ordering, quoting, ports, container names); it has no rule for depends_on readiness or healthchecks. The Docker extension for VS Code validates the Compose schema but does not check whether a service_healthy dependency can ever become healthy. ReadyWait covers that gap inside the editor.
Limitations
- Image healthchecks are not inspected (no registry or
docker access): an image from a registry that defines its own HEALTHCHECK still gets RW002 - disable the rule or add an explicit healthcheck: to silence it.
- The YAML reader is line-based: anchors, merge keys (
<<:) and include: are not followed, and -f file combinations from scripts are not detected.
- With
requirepass, the generated redis / valkey check proves the server is up and answering, not that your password works; use redis-cli -a with grep -q PONG if you need an authenticated check.
- MariaDB's
healthcheck.sh --innodb_initialized logs in with a healthcheck user that the image creates on first start. A data volume initialized by an older image may not have it: start once with MARIADB_AUTO_UPGRADE=1 to create it.
License
MIT - included with the extension.
| |