Guide: health checks and readiness probes for PHP apps in Docker Swarm — practical techniques that work
By Kevin Marcondes · Published on · 9 min read
Practical guide to health checks and readiness probes for PHP applications running on Docker Swarm with database and cache dependencies. You’ll learn how to write a deep PHP healthcheck, orchestrate dependency waits in the entrypoint, and implement real readiness via a proxy. Examples include stack.yml, scripts and timing guidelines to reduce 502 errors and restart loops.
Health vs readiness in Swarm: a practical view
A health check verifies whether the container process is running and responsive. Readiness indicates whether an instance can receive traffic without causing dependency errors. In Docker Swarm, the Dockerfile HEALTHCHECK does not remove tasks from the mesh load balancing by itself, so it doesn't control traffic.
A robust approach for PHP apps that depend on a database and a cache combines: a deep PHP HEALTHCHECK, an entrypoint that waits for dependencies, and an external proxy (Traefik/HAProxy) probing a /ready endpoint. Use rolling updates with start-first and small delays to warm new replicas.
If you use Docker Compose for local development, see the complementary guide health checks in Docker Compose for PHP to understand what can be reused and what changes in Swarm.
- Health check: container state (exit code).
- Readiness: able to receive traffic without dependency errors.
- Swarm does not use HEALTHCHECK to filter mesh traffic.
- Use a proxy with an HTTP readiness check.
- Use update_config for controlled rolling updates.
How to write a PHP healthcheck that validates DB and cache?
An effective healthcheck tests critical dependencies. For PHP-FPM, run a PHP CLI script that performs a SELECT 1 on Postgres (or MySQL) and a PING against Redis. If any step fails, exit with status 1. Configure short timeouts and an appropriate start_period to avoid false negatives during startup.
The example below expects environment variables like DB_DSN, DB_USER, DB_PASS and REDIS_URL. It uses the Redis extension when available and falls back to a simple TCP check.
<?php
declare(strict_types=1);
function fail(string $msg): void {
fwrite(STDERR, $msg.PHP_EOL);
exit(1);
}
$errors = [];
$dbDsn = getenv('DB_DSN') ?: '';
$dbUser = getenv('DB_USER') ?: '';
$dbPass = getenv('DB_PASS') ?: '';
if ($dbDsn) {
try {
$pdo = new PDO($dbDsn, $dbUser, $dbPass, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_TIMEOUT => 2,
]);
$pdo->query('SELECT 1');
} catch (Throwable $e) {
$errors[] = 'db';
}
}
$redisUrl = getenv('REDIS_URL') ?: '';
if ($redisUrl) {
$parts = parse_url($redisUrl);
$host = $parts['host'] ?? '127.0.0.1';
$port = (int)($parts['port'] ?? 6379);
$pass = $parts['pass'] ?? null;
try {
if (class_exists('Redis')) {
$r = new Redis();
$r->connect($host, $port, 1.5);
if ($pass) { $r->auth($pass); }
$r->ping();
} else {
$fp = @fsockopen($host, $port, $errno, $errstr, 1.5);
if (!$fp) { throw new RuntimeException('redis tcp failed'); }
fclose($fp);
}
} catch (Throwable $e) {
$errors[] = 'redis';
}
}
if ($errors) {
fail('Unhealthy: '.implode(',', $errors));
}
echo "OK\n";
Configuring the HEALTHCHECK and the Swarm stack
Define the HEALTHCHECK in the Dockerfile and reference it in the stack service. Choose retries, interval and start_period values that match your application's boot and warm-up time. A start_period that is too short causes false negatives. In Swarm, combine HEALTHCHECK with deploy.update_config for safer updates.
The example below uses 3 replicas, start-first update order and automatic rollback if the monitor detects failures. Remember that HEALTHCHECK alone does not control mesh traffic delivery.
version: "3.9"
services:
app:
image: registry.example.com/myapp:latest
env_file:
- app.env
healthcheck:
test: ["CMD", "php", "/app/.health/deep-healthcheck.php"]
interval: 10s
timeout: 3s
start_period: 20s
retries: 3
deploy:
replicas: 3
update_config:
parallelism: 1
order: start-first
delay: 10s
failure_action: rollback
monitor: 15s
restart_policy:
condition: any
max_attempts: 3
window: 30s
networks: ["frontend", "backend"]
web:
image: nginx:alpine
ports: ["80:80"]
networks: ["frontend", "backend"]
networks:
frontend:
driver: overlay
backend:
driver: overlay
How to implement real readiness in Swarm?
Real readiness in Swarm depends on your load balancer/proxy. Expose an HTTP /ready endpoint in the application and configure the proxy to remove instances that fail the check. The internal HEALTHCHECK remains useful for diagnostics but doesn't control mesh traffic.
Combine proxy readiness checks with start-first updates and short delays so new tasks can warm caches and connections before receiving traffic. See practical examples for HAProxy with health checks and gradual routing with canary routing with Traefik.
| Mechanism | Guarantees readiness? | Where to configure | Notes |
|---|---|---|---|
| Docker HEALTHCHECK | No, in Swarm it does not filter traffic | Dockerfile/stack | Good for diagnostics and internal gates; the mesh ignores the status |
| Proxy health check (HAProxy/Traefik) | Yes — removes bad replicas from the pool | Proxy configuration | Probes HTTP /ready; can rely on the app to include DB/cache checks |
| Rolling update start-first + delay | Partially | deploy.update_config | Warms instances before switching; does not fully validate readiness |
Orchestrating dependencies with a "wait-for" entrypoint
Because depends_on does not ensure readiness in Swarm, use an entrypoint that waits for the database and cache before starting PHP-FPM. Do not wait indefinitely: enforce timeouts and exit with code 1 so the orchestrator can retry or fail the task.
The script below uses nc to check TCP connectivity. Adapt the hosts, ports and environment variables to your environment (e.g. Postgres 5432, Redis 6379).
#!/usr/bin/env sh
set -e
wait_for_host() {
host="$1"; port="$2"; name="$3"; timeout="${4:-30}"
echo "Waiting for $name at $host:$port..."
for i in $(seq 1 "$timeout"); do
if nc -z "$host" "$port" >/dev/null 2>&1; then
echo "$name is up"; return 0
fi
sleep 1
done
echo "Timeout waiting for $name"; return 1
}
[ -n "$DB_HOST" ] && wait_for_host "$DB_HOST" "${DB_PORT:-5432}" "database"
[ -n "$REDIS_HOST" ] && wait_for_host "$REDIS_HOST" "${REDIS_PORT:-6379}" "redis"
exec php-fpm -F
Strategies by dependency: what to check for each
Each dependency deserves a specific, lightweight check. Avoid expensive operations in production health checks. Prefer small, idempotent operations and aggregate signals into a /ready endpoint that returns 200 only when everything is stable.
Expose a minimal internal readiness endpoint for PHP-FPM. For DB and cache, validate connectivity and minimal operations only.
- PHP-FPM: lightweight HTTP endpoint (e.g. /ready) performing a minimal application operation.
- PostgreSQL/MySQL: SELECT 1 with a short timeout; avoid running migrations in the healthcheck.
- Redis: PING and a short set/get of an ephemeral key with a low TTL.
- Queues (e.g. RabbitMQ): establish a connection and declare a passive queue without consuming messages.
- External services: HEAD/GET with short timeouts and implement a circuit breaker at the application level.
How to confirm it worked and monitor over time?
Check container health and service state after deploying. Use docker service ps to follow the rolling update. Inspect healthcheck status and validate the /ready endpoint through the proxy.
Monitor healthcheck failures and readiness error rates. Integrate logs and alerts. For application metrics and errors, see PHP-FPM metrics for Prometheus and PHP observability with Sentry.
- Confirm health status on tasks.
- Validate /ready via the proxy before routing traffic.
- Collect logs and metrics from PHP-FPM and the proxy.
# Ver tasks e atualizações
docker service ps --no-trunc my_app
# Inspecionar uma task específica
ID=$(docker service ps -q my_app | head -n1)
docker inspect $ID | jq '.[0].Status.ContainerStatus.ContainerID'
# Testar readiness via proxy (ajuste host)
curl -fsS http://app.example.com/ready || echo "not ready"
Common mistakes that break healthchecks in production
A few common patterns cause instability and restart loops. Review these points before promoting to production or after infrastructure changes.
Avoid expensive checks, tight timing, and hidden dependencies. Test under realistic latency and load. This guide focuses on Swarm; for Compose, consult the linked Compose material.
- HEALTHCHECK without an appropriate start_period and interval.
- A healthcheck script that performs heavy I/O or acquires DB locks.
- Long timeouts that delay detection of genuine failures.
- Relying on the Swarm mesh for traffic readiness.
- Expecting depends_on to be a readiness gate in Swarm.
- A /ready endpoint that accidentally runs migrations.
Frequently asked questions
Does Docker Swarm use HEALTHCHECK to remove replicas from load balancing?
No. Swarm keeps tasks in the RUNNING pool even if the HEALTHCHECK fails. For traffic readiness, use proxy checks (for example, probe /ready on HAProxy or Traefik) or an external load balancer that removes unhealthy instances.
Should I validate the database and Redis in the PHP healthcheck?
Yes, when they are critical dependencies. Validate with lightweight operations like SELECT 1 and PING. Use short timeouts and avoid migrations or expensive tasks in healthchecks to prevent instability.
What should I use to control order and warming during a Swarm update?
Use update_config with order: start-first, plus an appropriate delay and monitor. This warms the new task before stopping the old one. Still, combine it with readiness checks on the proxy so traffic is only sent when /ready returns 200.
Can I use depends_on to guarantee the database is ready in Swarm?
No. depends_on is not a readiness gate in Swarm. Use an entrypoint that waits for database and cache ports with a timeout and controlled failure, plus healthchecks and proxy readiness.
What interval and timeout should I use for healthchecks?
It depends on your boot time and expected latency. As a starting point, use intervals of 10–30s, timeouts of 2–5s and a start_period long enough for warming. Tune these values with tests under load and monitor for false positives.
Want reliable health checks on Swarm?
If you need help reviewing PHP HEALTHCHECKs, proxy readiness or service update settings, get in touch and we'll design a safe strategy for your stack.
Discuss Swarm health checksSee also
- Practical guide: active‑passive failover between two VPS with Keepalived, HAProxy & lsyncd for synchronized uploads
- Guide: technical checklist to prepare a Debian VPS with Docker Compose for PHP apps in production
- Practical guide to VPS sizing and PHP‑FPM tuning for Magento/OpenMage stores with seasonal peaks: from sizing to load testing
- Practical guide: rebuild PostgreSQL indexes in production using CREATE INDEX CONCURRENTLY and partial indexes (safe step‑by‑step)