Mastering PHP Application Health Checks in Docker and Kubernetes: Essential Strategies for Reliable Performance

Hire a PHP developer for your project — click here.

by admin
php-application-health-checks-docker-kubernetes

Why Health Checks Matter More Than We Admit

There’s a particular kind of silence that only exists in production.

It’s 2 AM. The office is dark, the Slack channels are still, the dashboards look fine at a glance. And somewhere inside a Kubernetes cluster, one lonely PHP-FPM process is stuck, holding a request hostage. CPU is low, container is still “running”, liveness probe is technically green. But the business logic? Dead inside.

Most of us only really feel the importance of health checks the first time something like that happens.

Friends, fellow PHP devs, colleagues—this piece is about that silent layer of truth: PHP application health checks in Docker and Kubernetes. Not the fancy observability stack. Just the simple decision of what we choose to tell the orchestrator about our app’s state.

Because if Docker and Kubernetes are the muscles, health checks are the nervous system. They tell the rest of the platform: “This PHP app is alive. Or not.”

Let’s walk through it like we’re sitting in front of a terminal together, coffee nearby, cluster on the other screen, trying to make things behave.


Docker Health Checks: Teaching The Container To Say “I’m Okay”

Docker gives us the HEALTHCHECK instruction in the Dockerfile. It’s deceptively simple: specify a command that runs inside the container; if it exits with 0, container is healthy; non-zero, unhealthy.

For PHP apps, we tend to land on two main strategies:

  • Call an HTTP endpoint (curl a /health route).
  • Run a small internal script that pokes at PHP-FPM or business dependencies.

A very common pattern:

HEALTHCHECK --interval=30s --timeout=3s \
  CMD curl --fail http://localhost/health.php || exit 1

That || exit 1 part is the tiny logic that determines whether Docker marks this container as sick or healthy. If curl fails, we exit non-zero and the Docker daemon flips the health state.

So far, so documentable.

But here’s the thing Docker doesn’t tell you: you are now deciding the definition of “up” for this service.

If /health.php only checks “PHP responds with 200”, your container is “healthy” even if:

  • The database is down.
  • Redis is broken.
  • A critical queue worker is not running.
  • PHP-FPM is wedged but Nginx still responds with some plain text.

You can go deeper with the health script itself—multiple checks in one command:

HEALTHCHECK --interval=30s --timeout=5s CMD \
  php /app/docker/healthcheck.php || exit 1

Inside healthcheck.php, you might:

  • Ping the DB.
  • Check a cache connection.
  • Verify a queue or job runner.
  • Maybe call an internal /health route.

Docker doesn’t care how you check, only the exit code.

There are also PHP-FPM specific tools that help you keep the health logic close to the engine. Projects like php-fpm-healthcheck and similar scripts can talk to the FPM status page, parse metrics, and return a clear success/failure result. They’re built precisely with container environments like Docker and Kubernetes in mind, focusing on FPM reliability.

When you place this inside your Docker image and call it from HEALTHCHECK, you’re asking something like: “Is PHP-FPM itself alive, and is it in a sane state?” instead of “Did this random script execute.”

And that leads to the real question: what exactly does “healthy” mean for your PHP app right now?


The Hidden Problem: Docker Healthchecks Aren’t Kubernetes Healthchecks

Here’s where many teams get quietly burned.

You have a Docker image with a beautiful HEALTHCHECK. You deploy it to Kubernetes. You assume Kubernetes will use that health status.

It won’t.

Kubernetes has its own world: liveness, readiness, and startup probes. These are configured in the Pod spec, not in the Dockerfile. That Reddit thread you’ve probably seen repeats it bluntly: Docker healthchecks ≠ Kubernetes healthchecks. Kubernetes doesn’t read the Docker HEALTHCHECK instruction; it runs its own probes via the kubelet.

That means:

  • Docker health checks are mainly useful in pure Docker / Docker Compose environments.
  • In Kubernetes, you must define probes per container; the Docker HEALTHCHECK is ignored.

If your team lives between Docker Compose locally and Kubernetes in production, this is worth saying out loud. Your health model essentially forks at deployment time unless you unify the logic.


Kubernetes Probes: Liveness, Readiness, Startup

Kubernetes is more nuanced about health. It doesn’t just ask: “Are you alive?” It asks three different questions.

  • Liveness probe: “Should I kill and restart this container?”
  • Readiness probe: “Should I send traffic to this Pod?”
  • Startup probe: “Is the app still starting up; should I give it time before checking other probes?”

Each probe can use:

  • httpGet – call an HTTP endpoint.
  • exec – run a command inside the container.
  • tcpSocket – open a TCP port.

For a PHP app with Nginx + PHP-FPM, the most common pattern is HTTP endpoints:

livenessProbe:
  httpGet:
    path: /live
    port: 80
  initialDelaySeconds: 10
  periodSeconds: 15
  timeoutSeconds: 3

readinessProbe:
  httpGet:
    path: /ready
    port: 80
  initialDelaySeconds: 5
  periodSeconds: 10
  timeoutSeconds: 3

Now Kubernetes starts to care about how your application responds:

  • Any status code between 200 and 399 is considered success.
  • Anything else is failure.
  • If liveness fails repeatedly, Kubernetes restarts the container.
  • If readiness fails, Kubernetes pulls this Pod out of service and stops routing traffic to it.

This is powerful. And dangerous, if the endpoints are sloppy.


Splitting The Brain: What /live And /ready Should Mean

One of the most useful strategies I’ve seen in PHP services is to split health endpoints intentionally:

  • /live (or /healthz, /livez) for liveness.
  • /ready for readiness.

We separate them because the questions are different.

Liveness should answer: “Is the process fundamentally alive and processing requests?” This check should be:

  • Light.
  • Fast.
  • Hard to fail unless something truly serious is wrong.

You might implement /live in PHP as:

  • Does PHP-FPM respond?
  • Is the basic app bootstrap working?
  • Maybe a quick self-check (like reading a simple config file, ensuring no fatal errors).

Readiness should answer: “Can this instance serve real traffic safely right now?” This check is allowed to be more strict:

  • Can we connect to the database?
  • Is the cache available?
  • Are critical services reachable?
  • Have migrations run?
  • Are we past any startup warm-up phase?

For a Laravel app, for example, a /ready route might:

  • Attempt a DB query like SELECT 1.
  • Ping Redis.
  • Check a feature flag service if core logic depends on it.

Only if all those checks pass do we return 200. If not, 503—and Kubernetes knows not to send traffic.

This separation gives you space to be honest:

  • “I’m alive, but I’m not ready.”
  • “I’m ready, send me traffic.”
  • “I’m broken, please restart me.”

And that honesty saves you from the half-broken, half-serving Pods that make on-call nights miserable.

Health Checks For PHP-FPM: Getting Closer To The Engine

Most of us run PHP-FPM behind Nginx or Apache. When something goes wrong in production, it’s often not the framework itself—it’s FPM:

  • Process pool exhausted.
  • Requests stuck.
  • Slow logs exploding.
  • Master process wedged.
See also
The Unfiltered Life of a PHP Developer: Daily Tasks, Challenges, and the Skills That Lead to Success

Several developers have shared approaches like:

  • Exposing the FPM status page on a private port and using tools that parse it and report health.
  • Using dedicated FastCGI-based health scripts that talk directly to PHP-FPM without going through the full app stack.

Some health check utilities are built exactly for this: they connect to FPM, send a request, inspect status metrics or response codes, and exit with success/failure accordingly. You can wire these into:

  • Docker HEALTHCHECK commands.
  • Kubernetes exec probes (running the script inside the container).
  • Or even aggregate their result in your /live endpoint.

Used well, FPM-level health checks allow you to detect situations where:

  • Nginx responds, but PHP-FPM cannot process requests.
  • The pool is overloaded and needs a restart.
  • Certain metrics (like max children reached) are indicators of trouble.

This is especially relevant when you’re dealing with Kubernetes liveness probes. If your liveness probe only hits a static Nginx endpoint, Kubernetes will happily keep restarting Pods that look “fine” from Nginx’s perspective while FPM is quietly yelling in the logs.

You want your liveness to reflect the health of the actual request path your users hit.


Exec Vs HttpGet: Choosing Your Check Type

There’s a subtle design decision that will haunt you later if you ignore it: should Kubernetes probe your app via HTTP, or via an internal command?

  • httpGet is attractive:

    • Simple to configure.
    • Runs fast.
    • Mimics real user traffic.
    • Works nicely with web servers.
  • exec has its strengths:

    • Can check internal system state (file existence, processes, sockets).
    • Doesn’t rely on HTTP configuration.
    • Can directly call PHP CLI scripts, FPM status tools, shell checks.

Many platform engineers recommend starting with HTTP probes for your PHP applications, for one reason: they test the same path users use. If your /health endpoint goes through the framework, the middleware (or bypasses it intentionally), the router, the FPM pool—your check sees a realistic picture.

But you might mix in exec probes for specific liveness checks:

  • Checking a PID file.
  • Verifying that /tmp/live exists (the classic Kubernetes tutorial style).
  • Running a short php /app/bin/fpm-health.php.

The important part is recognizing that probes aren’t free. Every request is extra load. Every command is extra work. You want them to be lightweight and fast while still telling the truth.


Healthcheck Design For Real PHP Apps: A Practical Blueprint

Let’s imagine you’re deploying a PHP application (say, Laravel) to Kubernetes, with Docker as the packaging runtime. How would a sane health setup look?

Dockerfile

You might keep a simple Docker health check for local dev / Docker Compose:

HEALTHCHECK --interval=30s --timeout=5s \
  CMD curl --fail http://localhost/docker-health || exit 1

And inside the app:

  • A route like /docker-health that returns 200 if:
    • PHP bootstraps.
    • DB can be pinged.
    • Basic dependencies are alive.

This keeps your local Docker stack honest, even before Kubernetes enters the story.

Kubernetes Deployment

In your Pod spec:

livenessProbe:
  httpGet:
    path: /live
    port: 80
  initialDelaySeconds: 15
  periodSeconds: 20
  timeoutSeconds: 3
  failureThreshold: 3

readinessProbe:
  httpGet:
    path: /ready
    port: 80
  initialDelaySeconds: 10
  periodSeconds: 10
  timeoutSeconds: 3
  failureThreshold: 3

startupProbe:
  httpGet:
    path: /startup
    port: 80
  initialDelaySeconds: 0
  periodSeconds: 5
  timeoutSeconds: 3
  failureThreshold: 30

Then, in PHP:

  • /startup

    • Returns 200 once the app has:
      • Warmed caches.
      • Run migrations (if applicable).
      • Loaded configuration.
    • Until then, it might return 503, allowing Kubernetes to give you more time thanks to startupProbe.
  • /live

    • Very lightweight:
      • Simple “can I handle a request?” check.
      • Maybe one tiny PHP-FPM self-call.
      • No heavy DB queries.
    • Only returns non-200 in true fatal situations.
  • /ready

    • More honest, heavier:
      • DB ping.
      • Cache ping.
      • Perhaps a small test query or call to a core external service.
    • Returns 503 if any critical dependency is down.

Add some structured JSON output so humans can read what’s going on when curling the endpoints:

{
  "status": "ok",
  "checks": {
    "db": "ok",
    "redis": "ok",
    "queue": "ok"
  },
  "timestamp": "2026-07-22T01:23:45Z"
}

That matters when someone is on-call, alone in front of the monitor, trying to understand why readiness keeps flapping.


Performance And Behavior: When Health Checks Hurt

It’s surprisingly easy to make health checks the source of your problems.

Some common traps:

  • Heavy checks in tight intervals
    A readiness probe that runs a real DB query every 2 seconds over hundreds of Pods can hammer your database. The “health” mechanism becomes a denial-of-service tool.

  • Too strict liveness checks
    If liveness fails on transient issues (brief DB hiccups, temporary cache blips), Kubernetes restarts Pods that might have recovered a second later. Now you have cascading restarts.

  • Timeouts out of sync with reality
    If your app occasionally takes 4 seconds to respond under load, but your health check has timeoutSeconds: 3, you’ll see false negatives. Kubernetes isn’t being mean; your configuration is.

Better patterns:

  • Use longer intervals and reasonable timeouts for checks that hit slower systems (DB, external services).
  • Keep liveness probes simple and resilient; reserve heavier logic for readiness.
  • Tune failureThreshold carefully—maybe tolerate a couple of failures before restart on liveness.

The balance is subtle: enough sensitivity to catch real failures, enough tolerance to let short spikes pass.


Security: Health Endpoints Shouldn’t Be Shouting In Public

One more uncomfortable truth: health endpoints leak information.

A /health or /ready endpoint that returns a JSON with:

  • DB status,
  • cache status,
  • external service URLs,
  • error messages,

is gold for someone mapping your internal architecture.

General wisdom here:

  • Keep health endpoints internal-only:

    • Restricted to cluster network.
    • Protected by ingress rules.
    • Sometimes entirely avoided on public interfaces.
  • Be thoughtful about what you return:

    • “db: down” is fine internally.
    • Hostnames, connection strings, stack traces—less fine if exposed.

In PHP, this might mean:

  • Registering health routes outside of public API routes.
  • Using middleware or routing configuration that limits access.
  • Or having separate internal endpoints just for Kubernetes, never exposed through public ingress.

For Teams And Careers: Why This Matters On “Find PHP”

If you’re reading this on Find PHP, you probably care about more than just syntax. You care about the craft—and about work that doesn’t implode in production.

Health checks are small, but they sit at the crossroads of:

  • PHP framework design.
  • Docker image structure.
  • Kubernetes deployment strategy.
  • DevOps collaboration.
  • On-call sanity.

When you build or review a PHP project:

  • Ask: What do our health endpoints really test?
  • Confirm: Are Docker and Kubernetes using consistent logic, or did we accidentally fork behavior?
  • Check: Are we hitting PHP-FPM itself, or just the static edge of the stack?
  • Reflect: Would I trust these checks at 3 AM, when everything feels more fragile?

And when you describe yourself as a PHP developer—as a candidate, a consultant, a lead—being able to talk about health checks in Docker and Kubernetes with nuance is quietly powerful. It shows you don’t just write code; you think about how it lives, fails, and recovers in real systems.

Some of the best PHP engineers I know are the ones who, during a retrospective, say softly: “I think our health checks are lying to us.” Then sit down and fix them.

If this text nudges you to look at your /health, /ready, or /live routes with a slightly more critical eye tomorrow morning, then it has done its job—and you will be a bit more at peace the next time the cluster feels too quiet.
перейти в рейтинг

Related offers