Health checks are middleware, not a controller

A health endpoint written as a controller action goes through routing, filters and model binding — which is exactly the machinery a health check should not depend on.

services.AddHealthChecks()
    .AddMySql(connectionString, name: "mysql", tags: new[] { "ready" })
    .AddRedis(redisConnection, name: "redis", tags: new[] { "ready" });

app.UseHealthChecks("/live", new HealthCheckOptions {
    Predicate = _ => false        // liveness: nothing checked
});
app.UseHealthChecks("/ready", new HealthCheckOptions {
    Predicate = c => c.Tags.Contains("ready")
});

The tag-based split into liveness and readiness is the part worth copying regardless of platform: a liveness check that fails because the database blipped restarts every container simultaneously and hammers the recovering database. Each check has its own timeout, which is what a hand-rolled PHP health endpoint usually lacks. The equivalent in PHP is a route registered before any middleware, which takes deliberate arranging.