Contents
- 1 Why Health Checks Matter More Than We Admit
- 2 Docker Health Checks: Teaching The Container To Say “I’m Okay”
- 3 The Hidden Problem: Docker Healthchecks Aren’t Kubernetes Healthchecks
- 4 Kubernetes Probes: Liveness, Readiness, Startup
- 5 Splitting The Brain: What /live And /ready Should Mean
- 6 Health Checks For PHP-FPM: Getting Closer To The Engine
- 7 Exec Vs HttpGet: Choosing Your Check Type
- 8 Healthcheck Design For Real PHP Apps: A Practical Blueprint
- 9 Performance And Behavior: When Health Checks Hurt
- 10 Security: Health Endpoints Shouldn’t Be Shouting In Public
- 11 For Teams And Careers: Why This Matters On “Find PHP”
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 (
curla/healthroute). - 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
/healthroute.
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
HEALTHCHECKis 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
200and399is 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./readyfor 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.
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
HEALTHCHECKcommands. - Kubernetes
execprobes (running the script inside the container). - Or even aggregate their result in your
/liveendpoint.
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?
-
httpGetis attractive:- Simple to configure.
- Runs fast.
- Mimics real user traffic.
- Works nicely with web servers.
-
exechas 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/liveexists (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-healththat returns200if:- 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
200once 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 tostartupProbe.
- Returns
-
/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.
- Very lightweight:
-
/ready- More honest, heavier:
- DB ping.
- Cache ping.
- Perhaps a small test query or call to a core external service.
- Returns
503if any critical dependency is down.
- More honest, heavier:
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 hastimeoutSeconds: 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
failureThresholdcarefully—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.