Kokil Thapa - Professional Web Developer in Nepal
Freelancer Web Developer in Nepal with 15+ Years of Experience

Kokil Thapa is an experienced full-stack web developer focused on building fast, secure, and scalable web applications. He helps businesses and individuals create SEO-friendly, user-focused digital platforms designed for long-term growth.

Docker Healthchecks Explained

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.

Docker Health State ModelContainerStatus: runningHealth ProbeCMD every intervalHealth Statestarting / healthyExit 0 → healthy count++ | Exit 1 → unhealthy count++healthyProbe passedstartingWithin start_periodunhealthyRetries exceeded
Docker healthchecks explained: running status and health status are independent signals orchestrators can act on.

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_on with condition: service_healthy waits 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 inspect exposes 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:

  1. interval — time between probe runs after the first check (default 30s).
  2. timeout — max seconds a single probe may run before Docker marks it failed.
  3. start-period — grace window after container start; failures do not count toward unhealthy.
  4. 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.

Healthcheck Timing ParametersContainer timeline →start_periodFailures ignoredinterval probes repeatEvery 30s typicaltimeoutMax probe durationretriesFailures → unhealthyExample: start_period=90s, interval=20s,timeout=3s, retries=4 → ~70s after grace to mark unhealthy
Tune start_period for slow Laravel boots; shorten interval only when fast failure detection justifies extra probe load.

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.

StatusMeaningTypical causeAction
noneNo HEALTHCHECK definedBase image without probeAdd HEALTHCHECK or Compose block
startingWithin start_period graceApp still booting, DB migratingWait; extend start_period if normal
healthyLast probe exited 0Service readyRoute traffic, run dependents
unhealthyRetries exceededDeadlock, bad config, OOMInspect 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.

Healthcheck Probe TypesCMD execcurl, pg_isreadyBest accuracyNeeds tools in imageHTTP GETSwarm / some proxiesChecks status codeNo body validationTCP socketPort open testWeakest signalMisses app errorsRecommendation: CMD exec against app health routeAvoid TCP-only checks for PHP, Node, or Java HTTP apps
Docker healthchecks explained by probe type: exec commands validate application logic; TCP alone only confirms a listener.

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.

Compose Startup With Health Gates1. Start dbmysqladmin ping2. db healthyGate opens3. Start appMigrations run4. app healthyTraffic allowedWithout health gate: app starts first → DB errors → manual restartBooking apps like Adventure Third Pole Trek depend on ordered startup
Health-gated Compose startup prevents race conditions in Laravel booking stacks such as Adventure Third Pole Trek.

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 HEALTHCHECK in the Dockerfile or Compose so health state reflects application readiness, not just a running PID.
  • Set start_period from measured boot time—Laravel migrations and cache warming need generous grace.
  • Use depends_on: condition: service_healthy in 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.Log when 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

A recurring command Docker runs inside a running container. Docker records the exit code and maintains a separate health state from the container lifecycle state, so a container can be running yet unhealthy.

Docker marks a container as started the moment PID 1 exists, not when the app is ready. On production Laravel stacks, PHP-FPM often starts before MySQL accepts connections, causing connection errors until manual restart. Healthchecks turn that guesswork into an automated gate. The same pattern applies to Redis, queue workers, and reverse proxies fronting multiple services. Without probes, load balancers, Compose startup order, and restart policies all guess from a weak signal.

Add a HEALTHCHECK instruction to your image definition. Docker runs it automatically for every container started from that image unless overridden at runtime. A Laravel-friendly example uses curl against an internal HTTP route: HEALTHCHECK with interval, timeout, start_period, and retries flags, then CMD curl -fsS http://127.0.0.1/health. Install probe tools like curl in the image if needed—Alpine-based PHP images often lack them. For databases, use native clients such as pg_isready or mysqladmin ping instead of generic TCP checks.

Return exit code 0 for healthy and 1 for unhealthy. Any non-zero exit counts as failure. Docker does not treat other codes specially.

Interval is the time between probe runs after the first check, defaulting to 30 seconds. Timeout is the maximum seconds a single probe may run before Docker marks it failed. Start_period is a grace window after container start where failures do not count toward unhealthy status. Retries is the number of consecutive failures needed before status flips to unhealthy. Tune start_period for slow Laravel boots and migrations; shorten interval only when fast failure detection justifies the extra probe load.

Compose lets you override or add healthchecks per service using a healthcheck block with test, interval, timeout, retries, and start_period. Pair this with depends_on using condition: service_healthy so your app container waits until MySQL passes mysqladmin ping before booting. That eliminates race-induced migration failures on first boot. Some upstream images ship aggressive defaults—override with disable: true only when a sidecar or external monitor replaces the probe. Match Compose file version features to the engine you actually run.

Docker exposes four health states through the API and CLI. None means no HEALTHCHECK is defined on the base image. Starting means the container is within start_period grace while the app is still booting or migrating. Healthy means the last probe exited 0 and the service is ready for traffic. Unhealthy means retries were exceeded, typically from deadlock, bad config, or OOM. Inspect health JSON with docker inspect formatted to .State.Health to see log entries with exit codes and timestamps.

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.

It makes one service wait until another passes its health probe before starting. In a typical Laravel stack, the app service depends on db with condition: service_healthy, so MySQL must respond to mysqladmin ping before the application container boots. That prevents race-induced migration failures and connection errors on first boot. This is the main payoff of defining healthchecks in Compose rather than relying on container start order alone.

Most failures are configuration errors, not Docker bugs. Probe localhost when the app binds elsewhere—curl the nginx port if it terminates TLS and proxies to PHP on a Unix socket. Set start_period too short for migrations, marking containers unhealthy mid-migration. Run heavy probes under load—a health route running full report queries every 10 seconds hurts production; keep probes O(1). Missing curl or wget in slim images causes false failures. Exposing /health publicly with sensitive dependency details is a security risk—restrict by internal network or return minimal OK text.

Both run periodic checks, but Kubernetes separates liveness, which restarts the pod, from readiness, which controls traffic routing. Docker has one health state consumed differently per runtime—Swarm maps it to task state, Traefik pulls bad backends from rotation, standalone Engine exposes it via docker inspect. Copy the probe command, not the orchestration model, when moving between platforms. Kubernetes users should define liveness and readiness probes in manifests rather than assuming Docker health maps one-to-one.

Yes. Use built-in clients like pg_isready for PostgreSQL, redis-cli ping for Redis, or mysqladmin ping for MySQL—they validate the actual service, not just an open port. Use wget -qO- if present in the image. For minimal or distroless images that cannot run shell probes, add a minimal sidecar health checker or switch to an image layer that includes one small HTTP client. A bash /dev/tcp test only verifies TCP, not HTTP semantics like a 500 response.

Inspect health JSON with docker inspect formatted to .State.Health and pipe through a JSON formatter. The output includes Log entries with exit codes and timestamps. When probes flap, that log beats guessing from application logs alone. Tune interval and timeout before blaming application code. Check whether CPU throttling from resource limits causes false failures. Verify probe tools exist in slim images and that start_period covers measured P95 boot time including Laravel migrations and cache warm-up.

Expose a tiny route that checks database connectivity without heavy logic. Keep it fast and unauthenticated internally—a SELECT 1 against MySQL returning plain text, under 50 milliseconds total runtime. Slow probes cause false negatives under load. Pair the route with a HEALTHCHECK using curl -f against http://127.0.0.1/health inside the container. Use the same probe contract in Dockerfile, Compose, and CI/CD smoke scripts so one definition spans environments. Do not publish sensitive dependency details in responses reachable from the internet.

Reverse proxies like Traefik read Docker health to pull bad backends from rotation. Compose uses depends_on with condition: service_healthy for reliable multi-container startup ordering. Swarm maps health to task state and replaces unhealthy tasks. Monitoring hooks expose health JSON via docker inspect that scripts can scrape without opening extra ports. In CI/CD, the same probe spans Dockerfile, Compose, and post-deploy smoke scripts—after symlink swap on PHP-FPM hosts, watch container health before marking deploy success. Restarts fix crashed processes but not wedged apps that respond on TCP yet return errors.

Share this article

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.

Quick Contact Options
Choose how you want to connect me: