Skip to content

Docker Health Monitoring

Runtime optimizations and health monitoring for production/staging deployments using Docker ENTRYPOINT pattern and Spatie Health package.

Docker ENTRYPOINT pattern provides:

  • Runtime Laravel optimization at container startup
  • Health monitoring via Spatie Health package
  • Portable Docker images across environments
  • Consistent initialization across container restarts

Executes at container startup (runtime) instead of Docker build time:

  • Configures PHP limits from environment variables (PHP_MEMORY_LIMIT, PHP_POST_MAX_SIZE, PHP_UPLOAD_MAX_FILESIZE, PHP_MAX_EXECUTION_TIME, PHP_MAX_INPUT_TIME)
  • Tunes the PHP-FPM pool from environment variables (PHP_FPM_PM_MAX_CHILDREN, PHP_FPM_PM_START_SERVERS, PHP_FPM_PM_MIN_SPARE_SERVERS, PHP_FPM_PM_MAX_SPARE_SERVERS, PHP_FPM_REQUEST_SLOWLOG_TIMEOUT, PHP_FPM_REQUEST_TERMINATE_TIMEOUT, PHP_FPM_SLOWLOG, PHP_FPM_ACCESS_FORMAT)
  • php artisan filament:optimize - Optimizes FilamentPHP assets
  • php artisan optimize - Caches Laravel routes, views, config
  • Config cache uses actual runtime environment variables
  • Portable Docker images work across dev/staging/production
  • OptimizedAppCheck passes with proper caching state
  • Consistent initialization across container restarts

Shared entrypoint script: backend/docker/common/docker-entrypoint.sh

Used by:

  • php-fpm container
  • queue-worker container
  • scheduler container
Check Purpose Configuration
DatabaseCheck Verify database connectivity Default
CacheCheck Verify cache connectivity Default
UsedDiskSpaceCheck Monitor disk usage Warn: 70%, Fail: 90%
OptimizedAppCheck Verify Laravel optimization (prod/staging) Checks config/route cache
DebugModeCheck Ensure debug mode off (prod/staging) Fails if APP_DEBUG=true
EnvironmentCheck Verify correct environment (prod/staging) Checks APP_ENV value
AiModelConfiguredCheck Verify an AI model is configured app/Providers/AppServiceProvider.php

Application errors and performance are monitored via:

  • Sentry (sentry/sentry-laravel) — Error tracking and performance tracing
  • Nightwatch — APM, ingested through a dedicated agent container (ECR mirror volare/nightwatch-agent of laravelphp/nightwatch-agent; locally the nightwatch-agent service in compose.yaml)
  • Container health checks: HEALTHCHECK is defined in the php-fpm and queue-worker images; the scheduler’s is set in the compose template; nginx and frontend have none
  • Grafana Cloud alerts on servers, containers and 5xx rates — see Monitoring and Alerts
php-fpm: # Laravel application (PHP-FPM) — api + ops-runner
web: # nginx in front of php-fpm ("backend" image) — api + ops-runner
queue-worker: # Processes all queues — ops-runner
scheduler: # Runs schedule:work — ops-runner
nightwatch: # Nightwatch APM agent — api + ops-runner
analytics-mcp: # Analytics MCP gateway — ops-runner, production only

Each of php-fpm, queue-worker, backend (nginx) and frontend is its own ECR image (see Servers and Containers). The scheduler is not a separate image: it runs the php-fpm image with php artisan schedule:work.

Dedicated container running Laravel scheduler in production/staging:

Terminal window
php artisan schedule:work

Purpose: Executes the scheduled commands defined in backend/routes/console.php:

  • telescope:prune - Daily cleanup (keeps 14 days, preserves exceptions)
  • model:prune - Daily health history cleanup (1-day retention)
  • currency:sync-rates - Daily exchange rate sync from the configured provider
  • offers:auto-generate - Every 15 minutes, from flight cache entries
  • last-seats:sync-prices - Every 15 minutes, keeps the persisted offer prices of Last Seats departures in step with the running campaigns
  • offers:draft-released - Daily, withdraws offers that entered the supplier release-days window
  • products:check-leading-price - Daily at 07:00 Madrid, snapshots each product’s leading (“desde”) price and alerts marketing when it drifts up
  • offers:audit-domestic-dates --notify - Daily at 06:45 Madrid, read-only audit reporting any active offer or live booking with a domestic leg off schedule or departing outside its leg’s departure window, notifying admins
  • flights:mark-stale-searching-as-failed - Every 5 minutes, flips silently-dead flight cache rows to FAILED
  • bookings:send-balance-reminders - Daily at 08:00 Madrid, chases the outstanding balance of bookings approaching departure (D-75 → D-60 stages)
  • bookings:send-pre-departure - Daily at 09:00 Madrid, sends check-in instructions to the passengers of trips leaving in 15 days
  • ProcessScheduledBalancePaymentsJob (Job) - Hourly scheduled balance payments
  • flights:protect-expiring-fares - Hourly, re-prices reservations whose Aerticket fare hold is about to lapse
  • emails:sync-resend-status - Hourly, pulls Resend delivery outcomes onto the recorded transactional email sends
  • db-backup.sh - Daily at 05:00 UTC, uploads a fresh DB dump to S3 (see Database Backup)

Deployment: The scheduler and queue worker are defined in infra-volare/terragrunt/templates/docker-compose.ops-runner.yml.tmpl, rendered into the SSM parameter /volare/<env>/autoscaling/ops-runner/docker-compose. Because onOneServer() is used, a task runs once even with two ops-runners.

GET /health

Returns JSON with all check results:

{
"finishedAt": "2025-11-14 17:30:00",
"checkResults": [
{
"name": "Database",
"status": "ok"
},
{
"name": "Cache",
"status": "ok"
}
]
}

Response Codes:

  • 200 - All checks passed
  • 503 - One or more checks failed
GET /health-dashboard

Authenticated UI showing cached health check results (requires login).

Terminal window
php artisan health:check
Terminal window
HEALTH_NOTIFICATION_EMAIL=ops@example.com
HEALTH_DB_CONNECTION=pgsql

The queue worker has a separate Laravel memory threshold, configured in megabytes:

Terminal window
QUEUE_WORKER_MEMORY=384

The image defaults to 384. An empty override also uses the default. Any other value must be a positive integer, or the container exits during startup with a configuration error. Keep this threshold below the container memory limit so the worker can shut down cleanly before Docker terminates it.

File: config/health.php

  • Result storage: Database (1-day history)
  • Notifications: Email (throttled to 1/hour)

Configure a load balancer (e.g., AWS ALB) to poll the /health endpoint:

Health Check Settings:

  • Path: /health
  • Interval: 30 seconds
  • Timeout: 5 seconds
  • Healthy threshold: 2
  • Unhealthy threshold: 2
  1. Ensure ENTRYPOINT executes before service starts
  2. Check container logs for optimization errors
  3. Verify runtime environment variables set correctly
Terminal window
# Check if optimization ran
docker logs php-fpm | grep "artisan optimize"
  1. Check disk usage:
Terminal window
df -h
  1. Clean old logs:
Terminal window
php artisan telescope:prune
  1. Prune health history: Daily scheduled task handles this
  1. Check database connectivity:
Terminal window
php artisan tinker
>>> DB::connection()->getPdo();
  1. Verify environment variables:
Terminal window
php artisan config:show database

The cache store is database (there is no Redis in staging or production), so cache failures are usually database failures — check those first.

  1. Check the cache works:
Terminal window
php artisan tinker
>>> Cache::get('test');
  1. Verify the cache configuration:
Terminal window
php artisan config:show cache
app/Providers/AppServiceProvider.php
use Spatie\Health\Facades\Health;
use Spatie\Health\Checks\Checks\DatabaseCheck;
Health::checks([
DatabaseCheck::new(),
CacheCheck::new(),
UsedDiskSpaceCheck::new()
->warnWhenUsedSpaceIsAbovePercentage(70)
->failWhenUsedSpaceIsAbovePercentage(90),
// Add custom checks here
]);
use Spatie\Health\Checks\Check;
use Spatie\Health\Checks\Result;
class QueueSizeCheck extends Check
{
public function run(): Result
{
$queueSize = Queue::size('default');
if ($queueSize > 1000) {
return Result::make()
->failed("Queue size is {$queueSize}");
}
if ($queueSize > 500) {
return Result::make()
->warning("Queue size is {$queueSize}");
}
return Result::make()
->ok("Queue size is {$queueSize}");
}
}