Skip to content
1 change: 1 addition & 0 deletions autoload.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
require_once(__DIR__ . '/deployer/requirements/task/check_env.php');
require_once(__DIR__ . '/deployer/requirements/task/check_eol.php');
require_once(__DIR__ . '/deployer/requirements/task/list.php');
require_once(__DIR__ . '/deployer/requirements/task/health.php');

/*
* dev
Expand Down
4 changes: 4 additions & 0 deletions deployer/requirements/config/set.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
set('requirements_check_env_enabled', true);
set('requirements_check_eol_enabled', true);
set('requirements_check_database_grants_enabled', true);
set('requirements_check_health_enabled', true);

// Locales
set('requirements_locales', ['de_DE.utf8', 'en_US.utf8']);
Expand Down Expand Up @@ -108,6 +109,9 @@
set('requirements_eol_warn_months', 6);
set('requirements_eol_api_timeout', 5);

// Health check
set('requirements_health_url', 'http://localhost');

// User / permissions
set('requirements_user_group', 'www-data');
set('requirements_deploy_path_permissions', '2770');
Expand Down
30 changes: 30 additions & 0 deletions deployer/requirements/functions.php
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,36 @@ function checkEolForProduct(string $label, string $product, string $cycle, int $
evaluateEolStatus("$label $cycle", $match, $warnMonths);
}

/**
* Check if a service is active via systemctl, with pgrep fallback.
*
* Returns the matched service/process name or null if none found.
*/
function isServiceActive(string ...$names): ?string
{
$hasSystemctl = test('command -v systemctl > /dev/null 2>&1');

foreach ($names as $name) {
try {
if ($hasSystemctl) {
$status = trim(run("systemctl is-active $name 2>/dev/null || true"));

if ($status === 'active') {
return $name;
}
}

if (test("pgrep -x $name > /dev/null 2>&1")) {
return $name;
}
} catch (RunException) {
continue;
}
}

return null;
}

/**
* @return array<string, string>
*/
Expand Down
88 changes: 88 additions & 0 deletions deployer/requirements/task/health.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
<?php

declare(strict_types=1);

namespace Deployer;

use Deployer\Exception\RunException;

task('requirements:health', function (): void {
set('requirements_rows', []);

if (!get('requirements_check_health_enabled')) {
return;
}

// 1. PHP-FPM
try {
$phpVersion = trim(run('php -r "echo PHP_MAJOR_VERSION.\'.\'.PHP_MINOR_VERSION;" 2>/dev/null'));

if (!preg_match('/^\d+\.\d+$/', $phpVersion)) {
throw new RunException('php', 1, "Unexpected PHP version output: $phpVersion", '');
}

$fpmService = isServiceActive("php$phpVersion-fpm", 'php-fpm');

if ($fpmService !== null) {
addRequirementRow('PHP-FPM', REQUIREMENT_OK, "Active ($fpmService)");
} else {
addRequirementRow('PHP-FPM', REQUIREMENT_FAIL, 'Process not found');
}
} catch (RunException) {
addRequirementRow('PHP-FPM', REQUIREMENT_SKIP, 'Could not determine PHP version');
}

// 2. Webserver
$webserver = isServiceActive('nginx', 'apache2', 'httpd');

if ($webserver !== null) {
addRequirementRow('Webserver', REQUIREMENT_OK, "Active ($webserver)");
} else {
addRequirementRow('Webserver', REQUIREMENT_FAIL, 'No nginx, apache2 or httpd process found');
}

// 3. Database server
$db = detectDatabaseProduct();
$dbLabel = $db !== null ? $db['label'] : 'Database';
$adminCmd = ($db !== null && $db['product'] === 'mariadb') ? 'mariadb-admin' : 'mysqladmin';
$dbChecked = false;

try {
run("$adminCmd ping --silent 2>/dev/null");
addRequirementRow('Database server', REQUIREMENT_OK, "$dbLabel responding");
$dbChecked = true;
} catch (RunException) {
// Admin tool not available — fall through to process check
}

if (!$dbChecked) {
$dbProcess = isServiceActive('mysqld', 'mariadbd');

if ($dbProcess !== null) {
addRequirementRow('Database server', REQUIREMENT_OK, "$dbLabel process running ($dbProcess)");
} else {
addRequirementRow('Database server', REQUIREMENT_FAIL, 'No mysqld or mariadbd process found');
}
}

// 4. HTTP response
$url = get('requirements_health_url');

try {
$httpCode = (int) trim(run(
sprintf("curl -s -o /dev/null -w '%%{http_code}' --max-time 5 %s 2>/dev/null", escapeshellarg($url))
));

if ($httpCode >= 200 && $httpCode < 500) {
addRequirementRow('HTTP response', REQUIREMENT_OK, "HTTP $httpCode from $url");
} elseif ($httpCode === 0) {
addRequirementRow('HTTP response', REQUIREMENT_FAIL, "No response from $url (connection refused or timeout)");
} else {
addRequirementRow('HTTP response', REQUIREMENT_FAIL, "HTTP $httpCode from $url");
}
} catch (RunException) {
addRequirementRow('HTTP response', REQUIREMENT_SKIP, 'curl not available');
}

renderRequirementsTable();
})->desc('Check service health');
23 changes: 23 additions & 0 deletions docs/REQUIREMENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,25 @@ Checks installed PHP and database (MariaDB/MySQL) versions against the [endoflif

The warning threshold is configurable (default: 6 months before EOL).

## Health check

A standalone task that verifies critical services are running on the target host. This is useful as a quick smoke test before or after deployment.

```bash
$ dep requirements:health [host]
```

The task checks four categories:

| Check | Method | OK | FAIL |
|-------|--------|----|------|
| PHP-FPM | systemctl / pgrep for `php<version>-fpm` | Process active | Process not found |
| Webserver | systemctl / pgrep for nginx, apache2, httpd | Process active | No process found |
| Database server | `mysqladmin ping` / `mariadb-admin ping` with process fallback | Responding or process running | No process found |
| HTTP response | `curl` against configured URL | HTTP 2xx–4xx | HTTP 5xx, timeout, or connection refused |

Service detection uses `systemctl is-active` with a `pgrep` fallback for systems without systemd.

## Configuration

All settings use the `requirements_` prefix and can be overridden in the consuming project:
Expand Down Expand Up @@ -162,6 +181,10 @@ set('requirements_eol_api_timeout', 5); // API timeout in seconds

// Database grants check
set('requirements_check_database_grants_enabled', true);

// Health check
set('requirements_check_health_enabled', true);
set('requirements_health_url', 'https://example.com');
```

## Extending with custom checks
Expand Down