From e9b81034f3b48d30a44f7b74a4d75c9dc2e186e8 Mon Sep 17 00:00:00 2001 From: Walmir Silva Date: Wed, 19 Nov 2025 17:33:16 -0300 Subject: [PATCH 1/2] refactor: remove DEMO_MODE and HEALTH_CHECK_INSTALL functionality MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove DEMO_MODE and HEALTH_CHECK_INSTALL build args from Makefile - Remove template files (php/index.php, php/health.php) and related logic from Dockerfile - Remove DEMO_MODE/HEALTH_CHECK_INSTALL sections from docker-entrypoint.sh - Remove demo mode variables from .env.example and docker-compose.example.yml - Fix OPcache configuration for development (VALIDATE_TIMESTAMPS=1) - Fix Nginx configuration test to accept PID permission errors during build The DEMO_MODE feature was removed to simplify the stack. Applications should provide their own index.php and health check endpoints. OPcache now properly validates file timestamps in development mode, ensuring code changes are immediately reflected without container restart. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .env.example | 12 +- Dockerfile | 14 - Makefile | 19 +- docker-compose.example.yml | 3 - docker-entrypoint.sh | 24 -- php/health.php | 709 ------------------------------------- php/index.php | 642 --------------------------------- scripts/process-configs.sh | 19 +- 8 files changed, 25 insertions(+), 1417 deletions(-) delete mode 100644 php/health.php delete mode 100644 php/index.php diff --git a/.env.example b/.env.example index 0bb9e72..429e189 100644 --- a/.env.example +++ b/.env.example @@ -2,16 +2,6 @@ # PHP API Stack — Environment Configuration # Image: kariricode/php-api-stack # ============================================ -# Demo Mode (optional) -# Set to 'true' to install demo landing page when no application exists -DEMO_MODE=true -# Mode debug (optional) 0=inactive 1=acrive -ENABLE_XDEBUG=1 -# Health Check Installation (optional) -# Set to 'true' to install health check endpoint -# Automatically enabled when DEMO_MODE=true -HEALTH_CHECK_INSTALL=true -# ============================================ # Application # ============================================ APP_NAME=php-api-stack @@ -110,7 +100,7 @@ PHP_FPM_REQUEST_SLOWLOG_TIMEOUT=30s PHP_OPCACHE_ENABLE=1 PHP_OPCACHE_MEMORY=256 PHP_OPCACHE_MAX_FILES=20000 -PHP_OPCACHE_VALIDATE_TIMESTAMPS=0 +PHP_OPCACHE_VALIDATE_TIMESTAMPS=1 PHP_OPCACHE_REVALIDATE_FREQ=0 PHP_OPCACHE_JIT=tracing PHP_OPCACHE_JIT_BUFFER_SIZE=128M diff --git a/Dockerfile b/Dockerfile index 89f76e7..1553dbb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -425,20 +425,6 @@ COPY scripts/quick-start.sh /usr/local/bin/quick-start RUN chmod +x /usr/local/bin/docker-entrypoint /usr/local/bin/process-configs /usr/local/bin/quick-start -# ---------------------------------------------------------------------------- -# Healthcheck templates (staged, published at runtime) -# ---------------------------------------------------------------------------- -RUN install -d -m 0755 /opt/php-api-stack-templates - -COPY --chown=nginx:nginx php/index.php /opt/php-api-stack-templates/index.php -COPY --chown=nginx:nginx php/health.php /opt/php-api-stack-templates/health.php - -# Validate syntax at build time -RUN set -eux; \ - php -l /opt/php-api-stack-templates/index.php; \ - php -l /opt/php-api-stack-templates/health.php; \ - echo "✓ Healthcheck templates validated successfully" - WORKDIR /var/www/html # ============================================================================ diff --git a/Makefile b/Makefile index f0df04e..c2ce12b 100644 --- a/Makefile +++ b/Makefile @@ -54,13 +54,8 @@ ALPINE_VERSION?=3.21 COMPOSER_VERSION?=2.8.12 SYMFONY_CLI_VERSION?=5.15.1 -DEMO_MODE ?= false -HEALTH_CHECK_INSTALL ?= false - # Common build args block BUILD_ARGS := \ - --build-arg DEMO_MODE=$(DEMO_MODE) \ - --build-arg HEALTH_CHECK_INSTALL=$(HEALTH_CHECK_INSTALL) \ --build-arg VERSION=$(VERSION) \ --build-arg PHP_VERSION=$(PHP_VERSION) \ --build-arg PHP_CORE_EXTENSIONS=$(PHP_CORE_EXTENSIONS) \ @@ -85,9 +80,7 @@ PROD_BUILD_ARGS := \ --build-arg PHP_OPCACHE_ENABLE=1 \ --build-arg PHP_OPCACHE_MEMORY_CONSUMPTION=256 \ --build-arg XDEBUG_ENABLE=0 \ - --build-arg APP_DEBUG=false \ - --build-arg DEMO_MODE=false \ - --build-arg HEALTH_CHECK_INSTALL=false + --build-arg APP_DEBUG=false # Development build args @@ -98,10 +91,8 @@ XDEBUG_VERSION ?= 3.4.6 DEV_BUILD_ARGS := \ --build-arg APP_ENV=development \ --build-arg APP_DEBUG=true \ - --build-arg DEMO_MODE=true \ --build-arg SYMFONY_CLI_VERSION=$(SYMFONY_CLI_VERSION) \ --build-arg XDEBUG_VERSION=$(XDEBUG_VERSION) \ - --build-arg HEALTH_CHECK_INSTALL=true \ --build-arg XDEBUG_ENABLE=$(XDEBUG_ENABLE) .PHONY: help @@ -186,11 +177,10 @@ build-base: ## Build base image (target=base) - for debugging only .PHONY: build-test build-test: ## Build test image (production with health check) - @echo "$(GREEN)Building test image with comprehensive health check...$(NC)" + @echo "$(GREEN)Building test image...$(NC)" @docker build \ $(BUILD_ARGS) \ $(PROD_BUILD_ARGS) \ - --build-arg HEALTH_CHECK_INSTALL=true \ --target production \ -t $(FULL_IMAGE):test \ . @@ -229,11 +219,13 @@ run-dev: ## Run dev container with Xdebug @echo "$(GREEN)Starting dev container...$(NC)" @docker stop $(DEV_CONTAINER) >/dev/null 2>&1 || true @docker rm $(DEV_CONTAINER) >/dev/null 2>&1 || true + @if docker ps --format '{{.Ports}}' | grep -q '$(DEV_PORT)->'; then \ echo "$(RED)X Port $(DEV_PORT) is already in use!$(NC)"; \ echo "$(YELLOW)Try another port:$(NC) make run-dev DEV_PORT=9000"; \ exit 1; \ fi + @docker run -d \ --name $(DEV_CONTAINER) \ -p $(DEV_PORT):80 \ @@ -241,11 +233,14 @@ run-dev: ## Run dev container with Xdebug --env-file $(ENV_FILE) \ -e APP_ENV=development \ -e XDEBUG_ENABLE=1 \ + -v $(PWD):/var/www/html \ -v $(PWD)/logs:/var/log \ $(FULL_IMAGE):$(IMAGE_TAG) + @echo "$(GREEN)OK: Dev container running at http://localhost:$(DEV_PORT)$(NC)" @echo "$(CYAN)Xdebug enabled on port 9003$(NC)" + .PHONY: run-test run-test: build-test ## Run test container @echo "$(GREEN)Starting test container...$(NC)" diff --git a/docker-compose.example.yml b/docker-compose.example.yml index 4791a35..26a9093 100644 --- a/docker-compose.example.yml +++ b/docker-compose.example.yml @@ -3,9 +3,6 @@ services: image: kariricode/php-api-stack:latest container_name: php-api-stack restart: unless-stopped - environment: - - DEMO_MODE=true - - HEALTH_CHECK_INSTALL=true env_file: - .env ports: diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh index d37cd95..cbd4c72 100755 --- a/docker-entrypoint.sh +++ b/docker-entrypoint.sh @@ -180,30 +180,6 @@ if [ -f "/var/www/html/bin/console" ]; then fi fi -# ---------------------------------------------------------------------------- -# DEMO_MODE -# ---------------------------------------------------------------------------- -if [ "${DEMO_MODE}" = "true" ] && [ -f "/opt/php-api-stack-templates/index.php" ]; then - log_info "Publishing demo index.php to /var/www/html/public" - mkdir -p /var/www/html/public - cp /opt/php-api-stack-templates/index.php /var/www/html/public/index.php - chown nginx:nginx /var/www/html/public/index.php - chmod 644 /var/www/html/public/index.php -else - rm -f /var/www/html/public/index.php 2>/dev/null || true -fi - -# HEALTH_CHECK_INSTALL -if [ "${HEALTH_CHECK_INSTALL}" = "true" ] && [ -f "/opt/php-api-stack-templates/health.php" ]; then - log_info "Publishing health.php to /var/www/html/public" - mkdir -p /var/www/html/public - cp /opt/php-api-stack-templates/health.php /var/www/html/public/health.php - chown nginx:nginx /var/www/html/public/health.php - chmod 644 /var/www/html/public/health.php -else - rm -f /var/www/html/public/health.php 2>/dev/null || true -fi - # ----------------------------------------------------------------------------- # XDEBUG # ----------------------------------------------------------------------------- diff --git a/php/health.php b/php/health.php deleted file mode 100644 index 41ce9ae..0000000 --- a/php/health.php +++ /dev/null @@ -1,709 +0,0 @@ - $this->healthy, - 'status' => $this->status, - ]; - - if (!empty($this->details)) { - $result['details'] = $this->details; - } - - if ($this->error !== null) { - $result['error'] = $this->error; - } - - if ($this->duration !== null) { - $result['duration_ms'] = round($this->duration * 1000, 2); - } - - return $result; - } -} - -// ============================================================================ -// UTILITIES (DRY Principle) -// ============================================================================ - -final class ByteFormatter -{ - private const UNITS = ['B', 'KB', 'MB', 'GB']; - - public static function format(int $bytes): string - { - $bytes = max($bytes, 0); - $pow = floor(($bytes ? log($bytes) : 0) / log(1024)); - $pow = min($pow, count(self::UNITS) - 1); - $bytes /= (1 << (10 * $pow)); - - return round($bytes, 2) . ' ' . self::UNITS[$pow]; - } -} - -// ============================================================================ -// ABSTRACT BASE CHECKER (Template Method Pattern) -// ============================================================================ - -abstract class AbstractHealthCheck implements HealthCheckInterface -{ - protected bool $critical = true; - - final public function check(): CheckResult - { - $start = microtime(true); - - try { - $result = $this->performCheck(); - $duration = microtime(true) - $start; - - return new CheckResult( - healthy: $result['healthy'], - status: $result['status'], - details: $result['details'] ?? [], - error: $result['error'] ?? null, - duration: $duration - ); - } catch (\Throwable $e) { - $duration = microtime(true) - $start; - - return new CheckResult( - healthy: false, - status: 'error', - details: [], - error: $e->getMessage(), - duration: $duration - ); - } - } - - abstract protected function performCheck(): array; - - final public function isCritical(): bool - { - return $this->critical; - } -} - -// ============================================================================ -// CONCRETE HEALTH CHECKERS (Single Responsibility Principle) -// ============================================================================ - -final class PhpRuntimeCheck extends AbstractHealthCheck -{ - public function getName(): string - { - return 'php'; - } - - protected function performCheck(): array - { - $memoryLimit = $this->parseMemory(ini_get('memory_limit')); - $memoryUsage = memory_get_usage(true); - $memoryPeakUsage = memory_get_peak_usage(true); - $memoryUsagePercent = $memoryLimit > 0 - ? round(($memoryUsage / $memoryLimit) * 100, 2) - : 0; - - $healthy = $memoryUsagePercent < 90; - - return [ - 'healthy' => $healthy, - 'status' => $healthy ? 'healthy' : 'warning', - 'details' => [ - 'version' => PHP_VERSION, - 'sapi' => PHP_SAPI, - 'memory' => [ - 'limit' => ByteFormatter::format($memoryLimit), - 'usage' => ByteFormatter::format($memoryUsage), - 'peak' => ByteFormatter::format($memoryPeakUsage), - 'usage_percent' => $memoryUsagePercent, - ], - 'zend_version' => zend_version(), - ], - ]; - } - - private function parseMemory(string $value): int - { - $value = trim($value); - if ($value === '-1') { - return PHP_INT_MAX; - } - - $unit = strtolower($value[strlen($value) - 1]); - $value = (int) $value; - - return match ($unit) { - 'g' => $value * 1024 * 1024 * 1024, - 'm' => $value * 1024 * 1024, - 'k' => $value * 1024, - default => $value, - }; - } -} - -final class PhpExtensionsCheck extends AbstractHealthCheck -{ - protected bool $critical = false; - - private const REQUIRED_EXTENSIONS = ['pdo', 'mbstring', 'json', 'curl']; - private const OPTIONAL_EXTENSIONS = ['redis', 'apcu', 'intl', 'zip', 'gd', 'xml']; - - public function getName(): string - { - return 'php_extensions'; - } - - protected function performCheck(): array - { - $loadedExtensions = get_loaded_extensions(); - $requiredLoaded = []; - $requiredMissing = []; - $optionalLoaded = []; - - foreach (self::REQUIRED_EXTENSIONS as $ext) { - if (extension_loaded($ext)) { - $requiredLoaded[] = $ext; - } else { - $requiredMissing[] = $ext; - } - } - - foreach (self::OPTIONAL_EXTENSIONS as $ext) { - if (extension_loaded($ext)) { - $optionalLoaded[] = $ext; - } - } - - $healthy = empty($requiredMissing); - - return [ - 'healthy' => $healthy, - 'status' => $healthy ? 'healthy' : 'critical', - 'details' => [ - 'total_loaded' => count($loadedExtensions), - 'required' => [ - 'loaded' => $requiredLoaded, - 'missing' => $requiredMissing, - ], - 'optional_loaded' => $optionalLoaded, - ], - 'error' => !$healthy ? 'Missing required extensions: ' . implode(', ', $requiredMissing) : null, - ]; - } -} - -final class OpcacheCheck extends AbstractHealthCheck -{ - protected bool $critical = false; - - private const MIN_HIT_RATE = 90.0; - private const MAX_MEMORY_USAGE = 90.0; - - public function getName(): string - { - return 'opcache'; - } - - protected function performCheck(): array - { - if (!function_exists('opcache_get_status')) { - return [ - 'healthy' => false, - 'status' => 'unavailable', - 'details' => [], - 'error' => 'OPcache extension not available', - ]; - } - - $status = @opcache_get_status(false); - - if ($status === false) { - return [ - 'healthy' => false, - 'status' => 'disabled', - 'details' => [], - 'error' => 'OPcache is disabled', - ]; - } - - $memoryUsed = $status['memory_usage']['used_memory'] ?? 0; - $memoryFree = $status['memory_usage']['free_memory'] ?? 0; - $memoryTotal = $memoryUsed + $memoryFree; - $memoryUsagePercent = $memoryTotal > 0 - ? round(($memoryUsed / $memoryTotal) * 100, 2) - : 0; - - $hits = $status['opcache_statistics']['hits'] ?? 0; - $misses = $status['opcache_statistics']['misses'] ?? 0; - $total = $hits + $misses; - $hitRate = $total > 0 ? round(($hits / $total) * 100, 2) : 0; - - $healthy = $hitRate >= self::MIN_HIT_RATE && $memoryUsagePercent < self::MAX_MEMORY_USAGE; - - return [ - 'healthy' => $healthy, - 'status' => $healthy ? 'healthy' : 'warning', - 'details' => [ - 'enabled' => true, - 'memory' => [ - 'used' => ByteFormatter::format($memoryUsed), - 'free' => ByteFormatter::format($memoryFree), - 'usage_percent' => $memoryUsagePercent, - 'wasted_percent' => round($status['memory_usage']['current_wasted_percentage'] ?? 0, 2), - ], - 'statistics' => [ - 'hits' => $hits, - 'misses' => $misses, - 'hit_rate' => $hitRate, - 'cached_scripts' => $status['opcache_statistics']['num_cached_scripts'] ?? 0, - 'max_cached_keys' => $status['opcache_statistics']['max_cached_keys'] ?? 0, - ], - 'jit' => [ - 'enabled' => $status['jit']['enabled'] ?? false, - 'on' => $status['jit']['on'] ?? false, - 'buffer_size' => ByteFormatter::format($status['jit']['buffer_size'] ?? 0), - ], - 'restarts' => [ - 'oom' => $status['opcache_statistics']['oom_restarts'] ?? 0, - 'hash' => $status['opcache_statistics']['hash_restarts'] ?? 0, - 'manual' => $status['opcache_statistics']['manual_restarts'] ?? 0, - ], - ], - ]; - } -} - -final class RedisCheck extends AbstractHealthCheck -{ - protected bool $critical = false; - - private const TIMEOUT = 1.0; - private const MAX_LATENCY_MS = 100.0; - - public function getName(): string - { - return 'redis'; - } - - /** - * Smart Redis Host Resolution - * - * Tenta resolver o hostname. Se falhar, usa 127.0.0.1 (standalone mode). - */ - protected function getRedisHost(): string - { - $host = getenv('REDIS_HOST') ?: '127.0.0.1'; - - // Se for "redis" (docker-compose), verifica se resolve - if ($host === 'redis') { - // Suprime warning de DNS - $resolved = @gethostbyname($host); - - // Se não resolveu (retorna o próprio hostname), usa localhost - if ($resolved === $host) { - return '127.0.0.1'; - } - } - - return $host; - } - - protected function performCheck(): array - { - if (!extension_loaded('redis')) { - return [ - 'healthy' => false, - 'status' => 'unavailable', - 'details' => [], - 'error' => 'Redis extension not installed', - ]; - } - - $redis = new \Redis(); - - $host = $this->getRedisHost(); - $password = getenv('REDIS_PASSWORD') ?: null; - $port = 6379; - - try { - $connectStart = microtime(true); - $connected = @$redis->connect($host, $port, self::TIMEOUT); - $connectDuration = (microtime(true) - $connectStart) * 1000; - - if (!$connected) { - return [ - 'healthy' => false, - 'status' => 'unavailable', - 'details' => [ - 'connect_latency_ms' => round($connectDuration, 2), - ], - 'error' => 'Cannot connect to Redis', - ]; - } - - if ($password !== null && $password !== '') { - if (!@$redis->auth($password)) { - return [ - 'healthy' => false, - 'status' => 'unauthenticated', - 'details' => ['host' => $host], - 'error' => 'Redis authentication failed (NOAUTH). Check REDIS_PASSWORD.', - ]; - } - } - - $pingStart = microtime(true); - $pong = $redis->ping(); - $pingDuration = (microtime(true) - $pingStart) * 1000; - - if ($pong !== true && $pong !== '+PONG') { - throw new \RuntimeException('Redis ping failed'); - } - - $info = $redis->info(); - $healthy = $pingDuration < self::MAX_LATENCY_MS; - - return [ - 'healthy' => $healthy, - 'status' => $healthy ? 'healthy' : 'warning', - 'details' => [ - 'connected' => true, - 'version' => $info['redis_version'] ?? 'unknown', - 'uptime_seconds' => (int) ($info['uptime_in_seconds'] ?? 0), - 'memory' => [ - 'used' => $info['used_memory_human'] ?? 'unknown', - 'peak' => $info['used_memory_peak_human'] ?? 'unknown', - 'fragmentation_ratio' => (float) ($info['mem_fragmentation_ratio'] ?? 0), - ], - 'stats' => [ - 'connected_clients' => (int) ($info['connected_clients'] ?? 0), - 'total_commands_processed' => (int) ($info['total_commands_processed'] ?? 0), - 'keyspace_hits' => (int) ($info['keyspace_hits'] ?? 0), - 'keyspace_misses' => (int) ($info['keyspace_misses'] ?? 0), - ], - 'latency' => [ - 'connect_ms' => round($connectDuration, 2), - 'ping_ms' => round($pingDuration, 2), - ], - ], - ]; - } catch (\Throwable $e) { - return [ - 'healthy' => false, - 'status' => 'error', - 'details' => [], - 'error' => $e->getMessage(), - ]; - } finally { - @$redis->close(); - } - } -} - -/** - * System Resources Check - Defensive Programming - * - * Gracefully handles open_basedir restrictions by using fallback strategies. - * - * @link https://www.php.net/manual/en/ini.core.php#ini.open-basedir - */ -final class SystemResourcesCheck extends AbstractHealthCheck -{ - protected bool $critical = false; - - private const MAX_DISK_USAGE = 90.0; - private const MAX_LOAD_AVERAGE = 10.0; - - public function getName(): string - { - return 'system'; - } - - protected function performCheck(): array - { - $diskFree = @disk_free_space('/'); - $diskTotal = @disk_total_space('/'); - $diskUsagePercent = $diskTotal > 0 - ? round((($diskTotal - $diskFree) / $diskTotal) * 100, 2) - : 0; - - $loadAvg = @sys_getloadavg(); - $load1 = $loadAvg[0] ?? 0; - - $memoryInfo = $this->getMemoryInfoSafely(); - - $healthy = $diskUsagePercent < self::MAX_DISK_USAGE && $load1 < self::MAX_LOAD_AVERAGE; - - return [ - 'healthy' => $healthy, - 'status' => $healthy ? 'healthy' : 'warning', - 'details' => [ - 'disk' => [ - 'total' => ByteFormatter::format($diskTotal ?: 0), - 'free' => ByteFormatter::format($diskFree ?: 0), - 'usage_percent' => $diskUsagePercent, - ], - 'load_average' => [ - '1min' => round($load1, 2), - '5min' => round($loadAvg[1] ?? 0, 2), - '15min' => round($loadAvg[2] ?? 0, 2), - ], - 'memory' => $memoryInfo, - ], - ]; - } - - /** - * Safely reads memory info with fallback strategy. - * - * Strategy 1: Try /proc/meminfo (full system memory) - * Strategy 2: Fallback to PHP memory usage (if restricted) - */ - private function getMemoryInfoSafely(): array - { - set_error_handler(static fn() => true); - $content = @file_get_contents('/proc/meminfo'); - restore_error_handler(); - - if ($content === false) { - return [ - 'source' => 'php_fallback', - 'note' => 'System memory unavailable (open_basedir restriction)', - 'php_memory_usage' => ByteFormatter::format(memory_get_usage(true)), - 'php_memory_peak' => ByteFormatter::format(memory_get_peak_usage(true)), - ]; - } - - $matches = []; - preg_match_all('/^(\w+):\s+(\d+)\s+kB/m', $content, $matches, PREG_SET_ORDER); - - $meminfo = []; - foreach ($matches as $match) { - $meminfo[$match[1]] = (int) $match[2] * 1024; - } - - $memTotal = $meminfo['MemTotal'] ?? 0; - $memAvailable = $meminfo['MemAvailable'] ?? $meminfo['MemFree'] ?? 0; - $memUsed = $memTotal - $memAvailable; - $memUsagePercent = $memTotal > 0 ? round(($memUsed / $memTotal) * 100, 2) : 0; - - return [ - 'source' => 'proc_meminfo', - 'total' => ByteFormatter::format($memTotal), - 'available' => ByteFormatter::format($memAvailable), - 'used' => ByteFormatter::format($memUsed), - 'usage_percent' => $memUsagePercent, - ]; - } -} - -/** - * Application Check - Respects Open Basedir - * - * Only checks directories within open_basedir scope. - * Provides clear feedback about security restrictions. - */ -final class ApplicationCheck extends AbstractHealthCheck -{ - protected bool $critical = false; - - /** - * CRITICAL: Only check paths within typical open_basedir scope. - * - * Common open_basedir: /var/www/html:/tmp:/usr/local/lib/php:/usr/share/php - * - * Excluded: /var/log/* (security policy prevents access) - */ - private const ACCESSIBLE_DIRS = [ - '/var/www/html', - '/var/www/html/public', - '/tmp', - ]; - - public function getName(): string - { - return 'application'; - } - - protected function performCheck(): array - { - $directoryStatus = []; - $allHealthy = true; - $openBasedir = ini_get('open_basedir'); - - foreach (self::ACCESSIBLE_DIRS as $dir) { - $status = $this->checkDirectorySafely($dir); - $directoryStatus[basename($dir)] = $status; - - if (!$status['exists'] || !$status['readable']) { - $allHealthy = false; - } - } - - $details = [ - 'directories' => $directoryStatus, - 'document_root' => $_SERVER['DOCUMENT_ROOT'] ?? 'unknown', - ]; - - // Add informational note about security policy - if (!empty($openBasedir)) { - $details['security_note'] = 'Log directories (/var/log) excluded per open_basedir policy'; - $details['open_basedir'] = $openBasedir; - } - - return [ - 'healthy' => $allHealthy, - 'status' => $allHealthy ? 'healthy' : 'warning', - 'details' => $details, - ]; - } - - /** - * Defensive directory check with error suppression. - * - * Prevents warnings when open_basedir blocks access. - */ - private function checkDirectorySafely(string $path): array - { - set_error_handler(static fn() => true); - - $exists = @is_dir($path); - $readable = $exists && @is_readable($path); - $writable = $exists && @is_writable($path); - - restore_error_handler(); - - return [ - 'path' => $path, - 'exists' => $exists, - 'readable' => $readable, - 'writable' => $writable, - ]; - } -} - -// ============================================================================ -// HEALTH CHECK MANAGER (Facade Pattern) -// ============================================================================ - -final class HealthCheckManager -{ - /** @var array */ - private array $checkers = []; - - public function addChecker(HealthCheckInterface $checker): self - { - $this->checkers[$checker->getName()] = $checker; - return $this; - } - - public function runAll(): array - { - $startTime = microtime(true); - $results = []; - $overallHealthy = true; - - foreach ($this->checkers as $name => $checker) { - $result = $checker->check(); - $results[$name] = $result->toArray(); - - if (!$result->healthy && $checker->isCritical()) { - $overallHealthy = false; - } - } - - $duration = microtime(true) - $startTime; - - return [ - 'status' => $overallHealthy ? 'healthy' : 'unhealthy', - 'timestamp' => date('c'), - 'duration_ms' => round($duration * 1000, 2), - 'checks' => $results, - ]; - } -} - -// ============================================================================ -// MAIN EXECUTION -// ============================================================================ - -header('Content-Type: application/json; charset=utf-8'); -header('Cache-Control: no-cache, no-store, must-revalidate'); -header('Pragma: no-cache'); -header('Expires: 0'); - -try { - $manager = new HealthCheckManager(); - - $manager - ->addChecker(new PhpRuntimeCheck()) - ->addChecker(new PhpExtensionsCheck()) - ->addChecker(new OpcacheCheck()) - ->addChecker(new RedisCheck()) - ->addChecker(new SystemResourcesCheck()) - ->addChecker(new ApplicationCheck()); - - $health = $manager->runAll(); - - $statusCode = $health['status'] === 'healthy' ? 200 : 503; - http_response_code($statusCode); - - echo json_encode($health, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); -} catch (\Throwable $e) { - http_response_code(500); - echo json_encode([ - 'status' => 'error', - 'timestamp' => date('c'), - 'error' => $e->getMessage(), - 'file' => $e->getFile(), - 'line' => $e->getLine(), - ], JSON_PRETTY_PRINT); -} diff --git a/php/index.php b/php/index.php deleted file mode 100644 index 4764b8d..0000000 --- a/php/index.php +++ /dev/null @@ -1,642 +0,0 @@ -performCheck(); - } catch (\Throwable $e) { - return new StatusResult( - healthy: false, - message: 'Error: ' . $e->getMessage(), - details: [] - ); - } - } -} - -/** - * PHP Runtime Check - */ -final class PhpCheck extends AbstractComponentCheck -{ - public function getName(): string - { - return 'PHP'; - } - - protected function performCheck(): StatusResult - { - $version = PHP_VERSION; - $extensions = get_loaded_extensions(); - - return new StatusResult( - healthy: true, - message: "PHP {$version} with " . count($extensions) . " extensions", - details: [ - 'version' => $version, - 'sapi' => PHP_SAPI, - 'extensions_count' => count($extensions), - 'memory_limit' => ini_get('memory_limit'), - 'max_execution_time' => ini_get('max_execution_time') . 's', - ] - ); - } -} - -/** - * OPcache Check - */ -final class OpcacheCheck extends AbstractComponentCheck -{ - public function getName(): string - { - return 'OPcache'; - } - - protected function performCheck(): StatusResult - { - if (!function_exists('opcache_get_status')) { - return new StatusResult( - healthy: false, - message: 'OPcache extension not available', - details: [] - ); - } - - $status = @opcache_get_status(false); - - if ($status === false) { - return new StatusResult( - healthy: false, - message: 'OPcache is disabled', - details: [] - ); - } - - $memoryUsed = $status['memory_usage']['used_memory'] ?? 0; - $memoryFree = $status['memory_usage']['free_memory'] ?? 0; - $memoryTotal = $memoryUsed + $memoryFree; - $usagePercent = $memoryTotal > 0 ? round(($memoryUsed / $memoryTotal) * 100, 1) : 0; - - return new StatusResult( - healthy: true, - message: "OPcache enabled ({$usagePercent}% memory used)", - details: [ - 'memory_used' => $this->formatBytes($memoryUsed), - 'cached_scripts' => $status['opcache_statistics']['num_cached_scripts'] ?? 0, - 'jit_enabled' => ($status['jit']['enabled'] ?? false) ? 'Yes' : 'No', - ] - ); - } - - private function formatBytes(int $bytes): string - { - $units = ['B', 'KB', 'MB', 'GB']; - $bytes = max($bytes, 0); - $pow = floor(($bytes ? log($bytes) : 0) / log(1024)); - $pow = min($pow, count($units) - 1); - $bytes /= (1 << (10 * $pow)); - return round($bytes, 2) . ' ' . $units[$pow]; - } -} - -/** - * Redis Connectivity Check - */ -final class RedisCheck extends AbstractComponentCheck -{ - public function getName(): string - { - return 'Redis'; - } - - - /** - * Smart Redis Host Resolution - * - * Tenta resolver o hostname. Se falhar, usa 127.0.0.1 (standalone mode). - */ - protected function getRedisHost(): string - { - $host = getenv('REDIS_HOST') ?: '127.0.0.1'; - - // Se for "redis" (docker-compose), verifica se resolve - if ($host === 'redis') { - // Suprime warning de DNS - $resolved = @gethostbyname($host); - - // Se não resolveu (retorna o próprio hostname), usa localhost - if ($resolved === $host) { - return '127.0.0.1'; - } - } - - return $host; - } - - - protected function performCheck(): StatusResult - { - if (!extension_loaded('redis')) { - return new StatusResult( - healthy: false, - message: 'Redis extension not installed', - details: [] - ); - } - - $redis = new \Redis(); - - $host = $this->getRedisHost(); - $password = getenv('REDIS_PASSWORD') ?: null; - $port = 6379; - - try { - $connected = @$redis->connect($host, $port, 1.0); - - if (!$connected) { - return new StatusResult( - healthy: false, - message: 'Cannot connect to Redis server', - details: [] - ); - } - - - if ($password !== null && $password !== '') { - if (!@$redis->auth($password)) { - return new StatusResult( - healthy: false, - message: 'Redis authentication failed (NOAUTH). Check REDIS_PASSWORD.', - details: ['host' => $host] - ); - } - } - - - $pong = $redis->ping(); - - if ($pong !== true && $pong !== '+PONG') { - return new StatusResult( - healthy: false, - message: 'Redis ping failed', - details: [] - ); - } - - $info = $redis->info(); - - return new StatusResult( - healthy: true, - message: 'Redis ' . ($info['redis_version'] ?? 'unknown') . ' connected', - details: [ - 'version' => $info['redis_version'] ?? 'unknown', - 'uptime' => $this->formatUptime((int)($info['uptime_in_seconds'] ?? 0)), - 'memory' => $info['used_memory_human'] ?? 'unknown', - ] - ); - } catch (\Throwable $e) { - return new StatusResult( - healthy: false, - message: 'Redis error: ' . $e->getMessage(), - details: [] - ); - } finally { - @$redis->close(); - } - } - - private function formatUptime(int $seconds): string - { - if ($seconds < 60) return "{$seconds}s"; - if ($seconds < 3600) return floor($seconds / 60) . "m"; - if ($seconds < 86400) return floor($seconds / 3600) . "h"; - return floor($seconds / 86400) . "d"; - } -} - -/** - * Status Dashboard (Facade Pattern) - */ -final class StatusDashboard -{ - /** @var ComponentCheckInterface[] */ - private array $checks = []; - - public function addCheck(ComponentCheckInterface $check): self - { - $this->checks[] = $check; - return $this; - } - - public function runAll(): array - { - $results = []; - $allHealthy = true; - - foreach ($this->checks as $check) { - $result = $check->check(); - $results[$check->getName()] = $result; - - if (!$result->healthy) { - $allHealthy = false; - } - } - - return [ - 'overall_healthy' => $allHealthy, - 'checks' => $results, - ]; - } -} - -// Run status checks -$dashboard = new StatusDashboard(); -$dashboard - ->addCheck(new PhpCheck()) - ->addCheck(new OpcacheCheck()) - ->addCheck(new RedisCheck()); - -$status = $dashboard->runAll(); - -// Collect system information -$stackInfo = [ - 'image' => 'kariricode/php-api-stack', - 'version' => getenv('STACK_VERSION') ?: '1.2.1', - 'php_version' => PHP_VERSION, - 'server_software' => $_SERVER['SERVER_SOFTWARE'] ?? 'nginx', - 'document_root' => $_SERVER['DOCUMENT_ROOT'] ?? '/var/www/html/public', -]; - -?> - - - - - - - PHP API Stack - Ready for Development - - - - -
-
-

🚀 PHP API Stack

-

Production-ready development environment

- Version -
- -
-
-

⚠️ Demo Mode Active

-

- This is a placeholder page shown when no application is mounted. - Mount your Symfony/Laravel project to - to replace this demo page with your application. -

-
- -

Component Status

- -
- $result): ?> -
-

- - -

-
- message) ?> -
- details)): ?> -
- details as $key => $value): ?> -
:
- -
- -
- -
- -
-

Stack Information

-
-
- Docker Image: - -
-
- Stack Version: - -
-
- PHP Version: - -
-
- Web Server: - -
-
- Document Root: - -
-
- Overall Status: - - - -
-
-
- -
-

Quick Links

- -
-
- - -
- - - \ No newline at end of file diff --git a/scripts/process-configs.sh b/scripts/process-configs.sh index 1efd9a7..fac2f1b 100755 --- a/scripts/process-configs.sh +++ b/scripts/process-configs.sh @@ -249,11 +249,26 @@ else fi # Validate Nginx -if nginx -t 2>/dev/null; then +log_info "Testing Nginx configuration..." +NGINX_TEST_OUTPUT=$(nginx -t 2>&1) +NGINX_EXIT_CODE=$? + +if [ $NGINX_EXIT_CODE -eq 0 ]; then log_info " ✓ Nginx configuration is valid" +elif echo "$NGINX_TEST_OUTPUT" | grep -q "syntax is ok" && echo "$NGINX_TEST_OUTPUT" | grep -q "Permission denied"; then + log_info " ✓ Nginx configuration syntax is valid (ignoring PID permission during build)" else + echo "-----------------------------------------------" log_error " ✗ Nginx configuration test failed!" - nginx -t + echo "Exit code: $NGINX_EXIT_CODE" + echo "-----------------------------------------------" + echo "Full Nginx test output:" + echo "$NGINX_TEST_OUTPUT" + echo "-----------------------------------------------" + echo "-----------------------------------------------" + echo "Nginx error log:" + cat /var/log/nginx/error.log 2>/dev/null || echo "No error log available" + echo "-----------------------------------------------" exit 1 fi From 59a96366b1ee7ed1b0143400127f2dd007a68833 Mon Sep 17 00:00:00 2001 From: Walmir Silva Date: Wed, 19 Nov 2025 17:35:25 -0300 Subject: [PATCH 2/2] chore: add public directory to gitignore MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The public directory contains application-specific files and should not be tracked in the docker image repository. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index a5d2acc..98f6752 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,9 @@ ./app/public/health.php ./app/public/index.php +# Public directory (application-specific files) +/public/ + # Logs ./logs/ *.log