
September 10, 2026
11 min read
By Kokil Thapa | Last reviewed: September 2026
A container can listen on port 80 and still serve 502 errors for five minutes. Docker healthchecks explained properly tell you whether the process inside is actually ready to work, not merely running. That distinction matters on every stack I deploy with Laravel Sail and Docker, on GitLab CI runners, and on client VPS boxes in Kathmandu. Without a health probe, Docker marks the container as started the moment PID 1 exists. Load balancers, Compose startup order, and restart policies all guess from that weak signal.
What is a Docker healthcheck and why does it matter?
A Docker healthcheck is a recurring command Docker executes inside a running container. Docker records the exit code and updates a separate health state from the container lifecycle state. A container can be running yet unhealthy. That gap is where most silent outages hide.
Think of it as a lightweight synthetic monitor. You choose what “healthy” means: an HTTP 200, a database ping, or a custom script. Docker does not infer application readiness from open ports. In practice, I treat healthchecks as part of deployment design, alongside logging and backups. They belong in the same conversation as Linux system administration and uptime planning.
On production Laravel stacks, a PHP-FPM container often starts before MySQL accepts connections. Without health-aware startup, the app logs connection errors until someone restarts it manually. Healthchecks turn that guesswork into an automated gate. The same pattern applies to Redis, queue workers, and reverse proxies fronting multiple services.
Where healthchecks show up in real deployments
- Reverse proxies: Traefik and similar tools read Docker health to pull bad backends from rotation. See Traefik as a modern reverse proxy for Docker.
- Compose startup order:
depends_onwithcondition: service_healthywaits for databases before app containers boot. - Swarm and Kubernetes adapters: Swarm maps health to task state; Kubernetes uses its own probes but the Dockerfile habit still helps local parity.
- Monitoring hooks:
docker inspectexposes health JSON your scripts can scrape without opening extra ports.
I have seen teams skip healthchecks because “the container restarts anyway.” Restarts fix crashed processes. They do not fix a wedged app that still responds on TCP but returns errors. That is exactly what probes catch.
How do you configure Docker healthchecks in a Dockerfile?
The HEALTHCHECK instruction belongs in your image definition. Docker runs it automatically for every container started from that image unless overridden at runtime. Official syntax is documented in the Dockerfile HEALTHCHECK reference.
Basic HEALTHCHECK syntax
FROM php:8.3-fpm
HEALTHCHECK --interval=30s --timeout=5s --start-period=60s --retries=3 \
CMD curl -f http://127.0.0.1/health || exit 1 Each flag controls timing behaviour:
- interval — time between probe runs after the first check (default 30s).
- timeout — max seconds a single probe may run before Docker marks it failed.
- start-period — grace window after container start; failures do not count toward unhealthy.
- retries — consecutive failures needed before status flips to unhealthy.
Install probe tools in the image if needed. Alpine-based PHP images often lack curl. Either add it in a prior layer or use a shell built-in test against a Unix socket.
Laravel-friendly HTTP probe example
Expose a tiny route that checks database connectivity without heavy logic. Keep it fast and unauthenticated internally.
HEALTHCHECK --interval=20s --timeout=3s --start-period=90s --retries=4 \
CMD curl -fsS http://127.0.0.1/health > /dev/null || exit 1 On a legal-tech portal I maintain, the health route runs SELECT 1 against MySQL and returns plain text. Total runtime stays under 50 ms. Slow probes cause false negatives under load. Pair this with Docker Compose for local Laravel development so dev and prod share the same probe contract.
Database and cache probes
PostgreSQL images ship with pg_isready. Redis images accept redis-cli ping. Use the native client when possible—it validates the actual service, not just an open port.
HEALTHCHECK --interval=10s --timeout=5s --retries=5 \
CMD pg_isready -U app -d app_db || exit 1 For deeper local database setup patterns, read run PostgreSQL in Docker for development. Persistent data still needs volumes; healthchecks do not replace backup strategy covered in Docker volumes and persistent data.
How do you define healthchecks in Docker Compose?
Compose lets you override or add healthchecks per service. This is where multi-container apps gain reliable startup ordering. Full stack patterns appear in Docker Compose multi-container apps.
Compose healthcheck block
services:
db:
image: mysql:8.4
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1"]
interval: 10s
timeout: 5s
retries: 6
start_period: 40s
app:
build: .
depends_on:
db:
condition: service_healthy
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost/health"]
interval: 30s
timeout: 5s
retries: 3
start_period: 90s The depends_on condition is the payoff. Your app container waits until MySQL passes mysqladmin ping. No more race-induced migration failures on first boot. For profile-based overrides in dev versus staging, see Docker Compose profiles and overrides.
Disable inherited healthchecks when needed
Some upstream images ship aggressive defaults that fail in your environment. Override with disable: true or replace the test entirely.
healthcheck:
disable: true Use sparingly. Disabling without replacement removes visibility. I only disable when a sidecar or external monitor replaces the probe.
Compose v2 on Ubuntu 22/24 is what I standardise for client servers after installing Docker on Ubuntu. Match Compose file version features to the engine you actually run—do not copy a Swarm-only health config into plain Compose and expect identical behaviour.
What are the different Docker container health statuses?
Docker exposes four health states through the API and CLI. Understanding them prevents misread alerts during deploys.
| Status | Meaning | Typical cause | Action |
|---|---|---|---|
| none | No HEALTHCHECK defined | Base image without probe | Add HEALTHCHECK or Compose block |
| starting | Within start_period grace | App still booting, DB migrating | Wait; extend start_period if normal |
| healthy | Last probe exited 0 | Service ready | Route traffic, run dependents |
| unhealthy | Retries exceeded | Deadlock, bad config, OOM | Inspect logs, fix probe or app |
Inspect health JSON with:
docker inspect --format='{{json .State.Health}}' my_container | jq The output includes Log entries with exit codes and timestamps. When debugging flaky probes, that log beats guessing from application logs alone. Pipe JSON through a JSON formatter if you paste results into tickets.
Swarm services accept HEALTHCHECK from the image or define checks at deploy time via docker service create health options. Kubernetes users should not assume Docker health maps 1:1—define liveness and readiness probes in manifests. For cluster-level decisions, compare Kubernetes vs Docker Swarm before over-engineering probe layers.
What are common Docker healthcheck mistakes to avoid?
Most failures I troubleshoot are configuration errors, not Docker bugs. These patterns repeat across agency projects and solo-founder VPS setups.
Probe checks localhost but app binds elsewhere
Inside the container network namespace, 127.0.0.1 is correct for single-process apps. If nginx terminates TLS and proxies to PHP on a Unix socket, curl the nginx port—not the FPM socket path unless your script handles it.
start_period too short for migrations
Laravel php artisan migrate --force during entrypoint can exceed 30 seconds on large schemas. Short grace marks the container unhealthy mid-migration. Set start_period to your measured P95 boot time plus buffer.
Heavy probes under load
A health route that runs full report queries every 10 seconds will hurt production. Keep probes O(1). Offload deep checks to cron or APM. Resource limits interact with probe timing—see limit Docker container resources so CPU throttling does not cause false failures.
Missing curl or wget in slim images
Distroless and scratch-based images cannot run shell probes. Use a minimal sidecar health checker or switch to an image layer that includes one small HTTP client. Image size tactics from reduce Docker image size best practices must balance slim builds against observability tooling.
Health endpoint exposed publicly without thought
Do not publish /health to the internet with sensitive dependency details. Restrict by internal network, reverse-proxy ACL, or return minimal OK text. Security review belongs in testing and optimization workflows before go-live.
How do healthchecks fit into CI/CD and production ops?
Healthchecks bridge build time and runtime. Your CI pipeline builds the image; production runtime proves it stays healthy after deploy. I wire the same probe in Dockerfile, Compose, and Deployer post-deploy smoke scripts so one contract spans environments.
After symlink swap on PHP-FPM hosts, I reload FPM and watch container health for 120 seconds before marking deploy success. Failed probes trigger automatic rollback on stacks where the orchestrator supports it. Sister sites on shared EC2 using Deployer 7 follow the same pattern described in my Docker networking explained notes—network readiness and app readiness are separate checks.
Podman users can reuse identical HEALTHCHECK instructions when migrating; cgroup and systemd differences do not change probe semantics. See Podman vs Docker migration guide for runtime-specific caveats. For ongoing probe drift after upgrades, support and maintenance retainers include health JSON review in quarterly audits.
Document expected boot duration in your runbook. New developers should know that a starting state for two minutes may be normal after cache warm-up. Without that note, on-call engineers restart healthy containers and make incidents worse.
Key Takeaways
- Define
HEALTHCHECKin the Dockerfile or Compose so health state reflects application readiness, not just a running PID. - Set
start_periodfrom measured boot time—Laravel migrations and cache warming need generous grace. - Use
depends_on: condition: service_healthyin Compose to eliminate database race errors on startup. - Prefer CMD exec probes (
curl -f,pg_isready) over TCP port checks that miss HTTP 500 responses. - Inspect
.State.Health.Logwhen probes flap; tune interval and timeout before blaming application code. - Keep health routes fast, unauthenticated internally, and free of sensitive diagnostics in public responses.
People Also Ask
What exit code should a Docker healthcheck return?
Return exit code 0 for healthy and 1 for unhealthy. Any non-zero exit counts as failure. Do not use other codes expecting special handling—Docker treats them all as failed probes.
Does Docker restart unhealthy containers automatically?
Not by default on standalone Docker Engine. The restart policy handles container exit, not health state. Orchestrators like Swarm replace unhealthy tasks; Compose restarts depend on configuration. You often pair healthchecks with external monitors or proxy health routing instead.
Can you run a healthcheck without installing curl?
Yes. Use built-in clients like pg_isready, redis-cli ping, or wget -qO- if present. For minimal images, add a one-line shell script using /dev/tcp in bash—though that only verifies TCP, not HTTP semantics.
How is Docker healthcheck different from Kubernetes liveness probes?
Both run periodic checks, but Kubernetes separates liveness (restart pod) and readiness (receive traffic). Docker has one health state consumed differently per runtime. Copy the probe command, not the orchestration model, when moving between platforms.
Ship containers that prove they are ready
Docker healthchecks explained end-to-end means your images declare readiness, Compose respects startup order, and proxies stop sending traffic to broken backends. That is the difference between a demo that boots once and a booking or eCommerce stack that survives Monday-morning deploys. Start with one honest HTTP probe, measure boot time, set start_period, then add database gates in Compose. Need help hardening a Laravel or WordPress container fleet on Ubuntu? Contact us for deployment review, or browse the portfolio for production systems already running on disciplined Docker workflows.
Frequently Asked Questions
0 Comments
Leave a comment
Your email is not published. Comments appear once they have been read. Sign in to have your details filled in.

