php-fpm: preinstall wp-cli, add WP-independent healthcheck
This commit is contained in:
23
README.md
23
README.md
@@ -40,12 +40,25 @@ The CI workflow is configured to build with the repository root as the Docker bu
|
|||||||
|
|
||||||
- Or enable the entrypoint-based fixup in php-fpm by setting `CHOWN_ON_START=1` for the `php-fpm` service (the entrypoint is guarded — it only runs when this env is explicitly enabled).
|
- Or enable the entrypoint-based fixup in php-fpm by setting `CHOWN_ON_START=1` for the `php-fpm` service (the entrypoint is guarded — it only runs when this env is explicitly enabled).
|
||||||
|
|
||||||
## Logging & debugging
|
## Logging & debugging
|
||||||
|
|
||||||
- PHP-FPM streams master/worker logs plus PHP fatals to stderr, so `docker compose logs php-fpm` (or your platform equivalent) will always contain the messages you need for incident response.
|
- PHP-FPM streams master/worker logs plus PHP fatals to stderr, so `docker compose logs php-fpm` (or your platform equivalent) will always contain the messages you need for incident response.
|
||||||
- The auto-prepend bootstrap additionally installs shutdown/exception hooks that write uncaught throwables and fatal errors to stderr even if WordPress or a plugin tampers with `ini_set()`.
|
- The auto-prepend bootstrap additionally installs shutdown/exception hooks that write uncaught throwables and fatal errors to stderr even if WordPress or a plugin tampers with `ini_set()`.
|
||||||
- When you need full stack traces in the browser, set `FORCE_DEBUG_ERRORS=1` on the `php-fpm` service. The bootstrap enables verbose output and emits a warning in the container logs reminding you to turn it back off (leaving it on in production leaks stack traces to clients).
|
- When you need full stack traces in the browser, set `FORCE_DEBUG_ERRORS=1` on the `php-fpm` service. The bootstrap enables verbose output and emits a warning in the container logs reminding you to turn it back off (leaving it on in production leaks stack traces to clients).
|
||||||
- Remove or unset `FORCE_DEBUG_ERRORS` after troubleshooting so production responses stay clean.
|
- Remove or unset `FORCE_DEBUG_ERRORS` after troubleshooting so production responses stay clean.
|
||||||
|
|
||||||
|
## Health checks
|
||||||
|
|
||||||
|
- The nginx config exposes `/healthz`, which bypasses WordPress entirely and executes `/usr/local/share/auvem/health/healthcheck.php` inside PHP-FPM. The script verifies the `/var/www/html` mount and optionally performs a MySQL connection test when `WORDPRESS_DB_*` variables are present.
|
||||||
|
- HTTP 200 means the platform (nginx ↔ php-fpm ↔ database) is healthy. Any filesystem or database failures return HTTP 503 with a short JSON payload detailing which probe failed. This lets you point uptime monitors at `/healthz` while still separately watching the public homepage for regressions caused by client code.
|
||||||
|
- Example local probe: `curl -fsS http://localhost:8080/healthz | jq`.
|
||||||
|
|
||||||
|
## WP-CLI baked in
|
||||||
|
|
||||||
|
- The PHP-FPM image now ships with the official `wp` binary, so you can run administrative remediation tasks without installing extra tooling on target hosts.
|
||||||
|
- Typical usage: `docker compose exec php-fpm wp --allow-root user update admin --user_pass='NewPassword123!' --skip-email --path=/var/www/html`.
|
||||||
|
- The binary runs as root inside the container, so remember the `--allow-root` flag (or create an app-level user if you prefer tighter isolation).
|
||||||
|
- Automation hooks (e.g., your client portal) can wrap `wp` commands for password resets, cron runs, plugin inspections, etc., provided they bind-mount the site into `/var/www/html`.
|
||||||
|
|
||||||
## Local Testing & Development
|
## Local Testing & Development
|
||||||
|
|
||||||
|
|||||||
@@ -42,6 +42,16 @@ server {
|
|||||||
location = /favicon.ico { access_log off; log_not_found off; }
|
location = /favicon.ico { access_log off; log_not_found off; }
|
||||||
location = /robots.txt { access_log off; log_not_found off; }
|
location = /robots.txt { access_log off; log_not_found off; }
|
||||||
|
|
||||||
|
# Fleet readiness probe that bypasses WordPress and executes a bundled PHP script.
|
||||||
|
location = /healthz {
|
||||||
|
access_log off;
|
||||||
|
include fastcgi_params;
|
||||||
|
fastcgi_param SCRIPT_FILENAME /usr/local/share/auvem/health/healthcheck.php;
|
||||||
|
fastcgi_param SCRIPT_NAME /healthz;
|
||||||
|
fastcgi_param PATH_INFO "";
|
||||||
|
fastcgi_pass php-fpm:9000;
|
||||||
|
}
|
||||||
|
|
||||||
# Main WordPress front-controller.
|
# Main WordPress front-controller.
|
||||||
# All non-file requests fall through to index.php.
|
# All non-file requests fall through to index.php.
|
||||||
location / {
|
location / {
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ RUN set -eux; \
|
|||||||
# Install runtime dependencies
|
# Install runtime dependencies
|
||||||
apk add --no-cache \
|
apk add --no-cache \
|
||||||
bash \
|
bash \
|
||||||
|
curl \
|
||||||
freetype \
|
freetype \
|
||||||
libjpeg-turbo \
|
libjpeg-turbo \
|
||||||
libpng \
|
libpng \
|
||||||
@@ -67,6 +68,10 @@ RUN set -eux; \
|
|||||||
# Use production php.ini
|
# Use production php.ini
|
||||||
cp "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini"; \
|
cp "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini"; \
|
||||||
\
|
\
|
||||||
|
# Install WP-CLI for remedial administration
|
||||||
|
curl -fSL https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar -o /usr/local/bin/wp; \
|
||||||
|
chmod +x /usr/local/bin/wp; \
|
||||||
|
\
|
||||||
# Clean up build dependencies
|
# Clean up build dependencies
|
||||||
apk del .build-deps; \
|
apk del .build-deps; \
|
||||||
rm -rf /var/cache/apk/* /tmp/*
|
rm -rf /var/cache/apk/* /tmp/*
|
||||||
@@ -85,6 +90,10 @@ COPY php-fpm/conf.d/ /usr/local/etc/php/conf.d/
|
|||||||
# Copy the force-debug script (enablement is handled via conf.d/99-force-debug.ini)
|
# Copy the force-debug script (enablement is handled via conf.d/99-force-debug.ini)
|
||||||
COPY --chown=app:app shared/php-fpm/force-debug.php /usr/local/etc/php/force-debug.php
|
COPY --chown=app:app shared/php-fpm/force-debug.php /usr/local/etc/php/force-debug.php
|
||||||
|
|
||||||
|
# Copy shared healthcheck assets
|
||||||
|
RUN mkdir -p /usr/local/share/auvem/health
|
||||||
|
COPY shared/php-fpm/healthcheck.php /usr/local/share/auvem/health/healthcheck.php
|
||||||
|
|
||||||
# Copy pool configuration from this directory
|
# Copy pool configuration from this directory
|
||||||
COPY --chown=app:app php-fpm/${BASE_VERSION}/www.conf /usr/local/etc/php-fpm.d/www.conf
|
COPY --chown=app:app php-fpm/${BASE_VERSION}/www.conf /usr/local/etc/php-fpm.d/www.conf
|
||||||
# Copy the global php-fpm configuration so logging defaults are predictable
|
# Copy the global php-fpm configuration so logging defaults are predictable
|
||||||
|
|||||||
94
shared/php-fpm/healthcheck.php
Normal file
94
shared/php-fpm/healthcheck.php
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
<?php
|
||||||
|
// healthcheck.php
|
||||||
|
// Lightweight readiness probe independent of WordPress code.
|
||||||
|
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
header('Cache-Control: no-store, no-cache, must-revalidate');
|
||||||
|
|
||||||
|
$response = [
|
||||||
|
'status' => 'ok',
|
||||||
|
'php_version' => PHP_VERSION,
|
||||||
|
'checks' => [],
|
||||||
|
];
|
||||||
|
$httpStatus = 200;
|
||||||
|
|
||||||
|
// Filesystem/webroot probe
|
||||||
|
$webroot = '/var/www/html';
|
||||||
|
$fsStatus = [
|
||||||
|
'name' => 'filesystem',
|
||||||
|
'status' => 'ok',
|
||||||
|
'detail' => 'Webroot mounted',
|
||||||
|
];
|
||||||
|
if (!is_dir($webroot)) {
|
||||||
|
$fsStatus['status'] = 'error';
|
||||||
|
$fsStatus['detail'] = 'Missing /var/www/html mount';
|
||||||
|
$httpStatus = 503;
|
||||||
|
} elseif (!is_readable($webroot)) {
|
||||||
|
$fsStatus['status'] = 'error';
|
||||||
|
$fsStatus['detail'] = 'Webroot not readable';
|
||||||
|
$httpStatus = 503;
|
||||||
|
}
|
||||||
|
$response['checks'][] = $fsStatus;
|
||||||
|
|
||||||
|
// Database probe (optional if env vars missing)
|
||||||
|
$requiredEnv = ['WORDPRESS_DB_HOST', 'WORDPRESS_DB_USER', 'WORDPRESS_DB_PASSWORD', 'WORDPRESS_DB_NAME'];
|
||||||
|
$dbConfig = [];
|
||||||
|
$missingEnv = [];
|
||||||
|
foreach ($requiredEnv as $var) {
|
||||||
|
$value = getenv($var);
|
||||||
|
if ($value === false || $value === '') {
|
||||||
|
$missingEnv[] = $var;
|
||||||
|
} else {
|
||||||
|
$dbConfig[$var] = $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$dbStatus = [
|
||||||
|
'name' => 'database',
|
||||||
|
'status' => 'skipped',
|
||||||
|
'detail' => 'Database env vars missing',
|
||||||
|
];
|
||||||
|
|
||||||
|
if (empty($missingEnv)) {
|
||||||
|
[$host, $port] = (function (string $rawHost): array {
|
||||||
|
if (preg_match('/^\[(.+)\](?::(\d+))?$/', $rawHost, $matches)) {
|
||||||
|
return [$matches[1], isset($matches[2]) ? (int) $matches[2] : 3306];
|
||||||
|
}
|
||||||
|
if (substr_count($rawHost, ':') === 1 && strpos($rawHost, '::') === false) {
|
||||||
|
[$h, $p] = explode(':', $rawHost, 2);
|
||||||
|
return [$h, (int) $p];
|
||||||
|
}
|
||||||
|
return [$rawHost, 3306];
|
||||||
|
})($dbConfig['WORDPRESS_DB_HOST']);
|
||||||
|
|
||||||
|
$mysqli = @mysqli_init();
|
||||||
|
if ($mysqli !== false) {
|
||||||
|
@mysqli_options($mysqli, MYSQLI_OPT_CONNECT_TIMEOUT, 3);
|
||||||
|
$connected = @$mysqli->real_connect(
|
||||||
|
$host,
|
||||||
|
$dbConfig['WORDPRESS_DB_USER'],
|
||||||
|
$dbConfig['WORDPRESS_DB_PASSWORD'],
|
||||||
|
$dbConfig['WORDPRESS_DB_NAME'],
|
||||||
|
$port
|
||||||
|
);
|
||||||
|
if ($connected) {
|
||||||
|
$dbStatus['status'] = 'ok';
|
||||||
|
$dbStatus['detail'] = 'DB connection OK';
|
||||||
|
$mysqli->close();
|
||||||
|
} else {
|
||||||
|
$dbStatus['status'] = 'error';
|
||||||
|
$dbStatus['detail'] = sprintf('DB connect failed: %s', $mysqli->connect_error ?? 'unknown error');
|
||||||
|
$httpStatus = 503;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$dbStatus['status'] = 'error';
|
||||||
|
$dbStatus['detail'] = 'Unable to init mysqli';
|
||||||
|
$httpStatus = 503;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$dbStatus['detail'] = 'Missing env: ' . implode(', ', $missingEnv);
|
||||||
|
}
|
||||||
|
$response['checks'][] = $dbStatus;
|
||||||
|
|
||||||
|
http_response_code($httpStatus);
|
||||||
|
echo json_encode($response, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n";
|
||||||
Reference in New Issue
Block a user