As a follow-up to Dockerfile HEALTCHECK
let’s see what Docker Compose healtchecks are for. Think of it as Dockerfile healtchecks with the flexibility to say “actually, in this environment let’s do things differently.”
when dockerfile healthchecks fall short
Picture this: your Dockerfile has a nice healthcheck that hits http://localhost:3000/health. Works great in prod where your app runs on port 3000. But in your local dev environment, you’re mapping that to port 8080, and in staging, it’s behind a reverse proxy with a completely different health endpoint.
Or maybe your image needs to work in multiple contexts — sometimes as a web server, sometimes as a background worker, sometimes as a one-shot migration runner. One size definitely doesn’t fit all.
That’s where Docker Compose healthchecks shine: environment-specific health checking without rebuilding images.
scratch the surface with some examples
Here’s the fun part — Docker Compose healthchecks completely override Dockerfile ones. Not merge, not extend — full replacement. Think of it as the compose file saying “I know better than your image.”
Basic example to start with:
services:
web:
image: my-app:latest
ports:
- "8080:3000"
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
interval: 20s
timeout: 5s
retries: 3
start_period: 10s
Note(s): Do not get confused as healthcheck
still uses port 3000 (internal container port) even though we’re mapping to 8080 externally. The healthcheck runs inside the container, so it sees the internal ports.
Database Dependency:
services:
postgres:
image: postgres:15
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
interval: 10s
timeout: 5s
retries: 5
web:
image: my-app:latest
depends_on:
postgres:
condition: service_healthy
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
interval: 30s
timeout: 10s
retries: 3
Note(s): The depends_on
with condition: service_healthy
is gold. Your web service won’t start until PostgreSQL is actually ready, not just “container started.”
Different Environments Pattern:
# docker-compose.yml (base)
services:
app:
image: my-app:latest
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
interval: 30s
# docker-compose.override.yml (local dev)
services:
app:
image: my-app:latest
build:
context: .
dockerfile: Dockerfile
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/health?detailed=true"]
interval: 10s # More frequent in dev for faster feedback
“Disable This Annoying Healthcheck” Pattern:
services:
app:
image: some-third-party-image-with-broken-healthcheck
healthcheck:
disable: true
gotchas
- Ports: Healthchecks run inside the container. If you’re mapping ports (like 8080:3000), your healthcheck should still use the internal port (3000), not the external one (8080).
- Env variables trap: Healthcheck commands don’t expand environment variables the same way shell commands do. Use
CMD-SHELL
if you need variable expansion.# This won't work as expected healthcheck: test: ["CMD", "curl", "-f", "http://localhost:${PORT}/health"] # instead do this: healthcheck: test: ["CMD-SHELL", "curl -f http://localhost:${PORT}/health || exit 1"]
- Alpine: Your compose healthcheck is useless if the image doesn’t have the tools it needs. That minimal Alpine image? Still doesn’t come with
curl
preinstalled. - Timing nightmare: Don’t set
start_period
too low. Your app might take 60 seconds to start, but you setstart_period: 10s
because “it should be fast.” Now your containers are stuck in an endless restart loop.
beyond docker compose
Once you’re comfortable with compose healthchecks, the next level is Kubernetes probes (liveness and readiness mostly). Same concepts, different syntax, way more flexibility. But that’s a whole other rabbit hole.
bottom line
Dockerfile healthchecks are great as defaults. Docker Compose healthchecks are essential for making those defaults work in different environments.
Your production environment, staging setup, and local dev stack probably all need slightly different health checking strategies. Embrace the override, stop trying to make one healthcheck rule them all.
And hey, if you want to monitor your containers from the outside too (because even the best healthchecks can miss things), justanotheruptime.com is still out there. Because sometimes you need to monitor the monitors that monitor your monitoring.