From 7c7edae110835ab7eedef6e968f80ad66758c9a9 Mon Sep 17 00:00:00 2001 From: Snider Date: Sat, 31 Jan 2026 23:51:48 +0000 Subject: [PATCH] fix: add missing server-php configs, fix developer git-delta server-php: - Add nginx.conf, fpm-pool.conf.template, supervisord.conf - Add php.ini.template, php-prod.ini, php-dev.ini - Add opcache-prod.ini, xdebug.ini - Add nginx-performance.conf for production - Add entrypoint.sh script developer: - Remove git-delta (not available in Alpine 3.22 repos) Closes #3 Co-Authored-By: Claude Opus 4.5 --- developer/Dockerfile | 3 +- server-php/config/conf.d/unified.conf | 182 +++++++++++++++++++++++ server-php/config/conf.d/wordpress.conf | 155 +++++++++++++++++++ server-php/config/fpm-pool.conf.template | 43 ++++++ server-php/config/nginx-performance.conf | 22 +++ server-php/config/nginx.conf | 63 ++++++++ server-php/config/opcache-prod.ini | 10 ++ server-php/config/php-dev.ini | 11 ++ server-php/config/php-prod.ini | 17 +++ server-php/config/php.ini.template | 9 ++ server-php/config/supervisord.conf | 24 +++ server-php/config/xdebug.ini | 7 + server-php/scripts/entrypoint.sh | 20 +++ 13 files changed, 565 insertions(+), 1 deletion(-) create mode 100644 server-php/config/conf.d/unified.conf create mode 100644 server-php/config/conf.d/wordpress.conf create mode 100644 server-php/config/fpm-pool.conf.template create mode 100644 server-php/config/nginx-performance.conf create mode 100644 server-php/config/nginx.conf create mode 100644 server-php/config/opcache-prod.ini create mode 100644 server-php/config/php-dev.ini create mode 100644 server-php/config/php-prod.ini create mode 100644 server-php/config/php.ini.template create mode 100644 server-php/config/supervisord.conf create mode 100644 server-php/config/xdebug.ini create mode 100644 server-php/scripts/entrypoint.sh diff --git a/developer/Dockerfile b/developer/Dockerfile index 5943521..1b8a99b 100644 --- a/developer/Dockerfile +++ b/developer/Dockerfile @@ -65,8 +65,9 @@ RUN apk add --no-cache \ # ============================================================ # VCS & Git Tools # ============================================================ +# Note: git-delta not in Alpine repos, install via cargo if needed RUN apk add --no-cache \ - git git-lfs github-cli lazygit git-delta + git git-lfs github-cli lazygit # ============================================================ # Node.js Ecosystem diff --git a/server-php/config/conf.d/unified.conf b/server-php/config/conf.d/unified.conf new file mode 100644 index 0000000..94fe857 --- /dev/null +++ b/server-php/config/conf.d/unified.conf @@ -0,0 +1,182 @@ +# Unified nginx configuration +# Routes traffic based on domain: +# - host.uk.com → Laravel Host Hub (PHP-FPM) +# - *.host.uk.com → WordPress (PHP-FPM) + +# Map for allowed CORS origins (WordPress REST API) +map $http_origin $cors_origin { + default ""; + "~^https?://host\.uk\.com$" $http_origin; + "~^https?://social\.host\.uk\.com$" $http_origin; + "~^https?://link\.host\.uk\.com$" $http_origin; + "~^https?://analytics\.host\.uk\.com$" $http_origin; + "~^https?://trust\.host\.uk\.com$" $http_origin; + "~^https?://notify\.host\.uk\.com$" $http_origin; + "~^https?://localhost(:[0-9]+)?$" $http_origin; + "~^https?://127\.0\.0\.1(:[0-9]+)?$" $http_origin; +} + +# ============================================ +# LARAVEL HOST HUB - Apex Domain +# ============================================ +server { + listen 80; + listen [::]:80; + server_name host.uk.com www.host.uk.com; + + root /app/public; + index index.php; + + client_max_body_size 64M; + + # Security headers + add_header X-Content-Type-Options "nosniff" always; + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-XSS-Protection "1; mode=block" always; + + # Health check - returns 200 if nginx is up (no PHP needed) + location = /healthz { + access_log off; + add_header Content-Type text/plain; + return 200 "ok\n"; + } + + # WordPress REST API proxy + # host.uk.com/api/wordpress/* → WordPress /wp-json/* + # Same-origin, no CORS needed + location ~ ^/api/wordpress/(.*)$ { + # Pass to WordPress index.php with rest_route + fastcgi_pass unix:/run/php-fpm.sock; + fastcgi_param SCRIPT_FILENAME /var/www/html/index.php; + fastcgi_param REQUEST_URI /wp-json/$1$is_args$args; + fastcgi_param HTTP_X_FORWARDED_PROTO $http_x_forwarded_proto; + fastcgi_index index.php; + fastcgi_buffering off; + fastcgi_read_timeout 300; + include fastcgi_params; + } + + # Laravel routing + location / { + try_files $uri $uri/ /index.php?$query_string; + } + + # PHP-FPM for Laravel + location ~ \.php$ { + try_files $uri =404; + fastcgi_split_path_info ^(.+\.php)(/.+)$; + fastcgi_pass unix:/run/php-fpm.sock; + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + fastcgi_param PATH_INFO $fastcgi_path_info; + fastcgi_param HTTP_X_FORWARDED_PROTO $http_x_forwarded_proto; + fastcgi_index index.php; + fastcgi_buffering off; + fastcgi_read_timeout 300; + include fastcgi_params; + } + + # Livewire and Flux - must go to Laravel (not static files) + location ~ ^/(admin|flux)/ { + try_files $uri $uri/ /index.php?$query_string; + } + + # Laravel static assets (build, vendor directories only) + location ~* ^/(build|vendor)/.*\.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { + expires max; + add_header Cache-Control "public, immutable"; + log_not_found off; + access_log off; + } + + # Deny hidden files + location ~ /\. { + deny all; + } + + # PHP-FPM status (internal only) + location ~ ^/(fpm-status|fpm-ping)$ { + access_log off; + allow 127.0.0.1; + deny all; + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + include fastcgi_params; + fastcgi_pass unix:/run/php-fpm.sock; + } +} + +# ============================================ +# LARAVEL SATELLITES - Subdomains +# ============================================ +server { + listen 80 default_server; + listen [::]:80 default_server; + server_name *.host.uk.com; + + root /app/public; + index index.php; + + client_max_body_size 64M; + + # Security headers + add_header X-Content-Type-Options "nosniff" always; + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-XSS-Protection "1; mode=block" always; + + # Health check + location = /healthz { + access_log off; + add_header Content-Type text/plain; + return 200 "ok\n"; + } + + # WordPress REST API (for internal content sync) + # Routes /wp-json/* requests to WordPress + # Host header determines which multisite blog to serve + location ~ ^/wp-json/(.*)$ { + fastcgi_pass unix:/run/php-fpm.sock; + fastcgi_param SCRIPT_FILENAME /var/www/html/index.php; + fastcgi_param REQUEST_URI /wp-json/$1$is_args$args; + fastcgi_param HTTP_X_FORWARDED_PROTO $http_x_forwarded_proto; + fastcgi_index index.php; + fastcgi_buffering off; + fastcgi_read_timeout 300; + include fastcgi_params; + } + + # Laravel routing + location / { + try_files $uri $uri/ /index.php?$query_string; + } + + # PHP-FPM for Laravel + location ~ \.php$ { + try_files $uri =404; + fastcgi_split_path_info ^(.+\.php)(/.+)$; + fastcgi_pass unix:/run/php-fpm.sock; + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + fastcgi_param PATH_INFO $fastcgi_path_info; + fastcgi_param HTTP_X_FORWARDED_PROTO $http_x_forwarded_proto; + fastcgi_index index.php; + fastcgi_buffering off; + fastcgi_read_timeout 300; + include fastcgi_params; + } + + # Livewire and Flux + location ~ ^/(admin|flux)/ { + try_files $uri $uri/ /index.php?$query_string; + } + + # Static assets + location ~* ^/(build|vendor)/.*\.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { + expires max; + add_header Cache-Control "public, immutable"; + log_not_found off; + access_log off; + } + + # Deny hidden files + location ~ /\. { + deny all; + } +} diff --git a/server-php/config/conf.d/wordpress.conf b/server-php/config/conf.d/wordpress.conf new file mode 100644 index 0000000..911ff19 --- /dev/null +++ b/server-php/config/conf.d/wordpress.conf @@ -0,0 +1,155 @@ +# WordPress Multisite server configuration +# Map for allowed CORS origins +map $http_origin $cors_origin { + default ""; + "~^https?://host\.uk\.com$" $http_origin; + "~^https?://social\.host\.uk\.com$" $http_origin; + "~^https?://link\.host\.uk\.com$" $http_origin; + "~^https?://analytics\.host\.uk\.com$" $http_origin; + "~^https?://trust\.host\.uk\.com$" $http_origin; + "~^https?://notify\.host\.uk\.com$" $http_origin; + "~^https?://localhost(:[0-9]+)?$" $http_origin; + "~^https?://127\.0\.0\.1(:[0-9]+)?$" $http_origin; +} + +server { + listen [::]:80 default_server; + listen 80 default_server; + + # Only accept subdomain traffic (*.host.uk.com), not apex domain + # The apex domain (host.uk.com) should route to Host Hub (Laravel) + server_name ~^(?.+)\.host\.uk\.com$ hestia.host.uk.com *.host.uk.com; + + # Serve error page for apex domain - this shouldn't hit WordPress + # If it does, Coolify routing is misconfigured + error_page 503 /wp-content/routing-error.html; + if ($host = "host.uk.com") { + return 503; + } + + # Reject completely unknown hosts with connection close + if ($host !~ "\.host\.uk\.com$") { + return 444; + } + + sendfile off; + tcp_nodelay on; + absolute_redirect off; + + root /var/www/html; + index index.php; + + client_max_body_size 64M; + + # Security headers + add_header X-Content-Type-Options "nosniff" always; + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-XSS-Protection "1; mode=block" always; + + # WordPress multisite rewrite rules + location / { + try_files $uri $uri/ /index.php?$args; + } + + # REST API with CORS headers for headless operation + location /wp-json/ { + # Handle preflight OPTIONS requests + if ($request_method = 'OPTIONS') { + add_header 'Access-Control-Allow-Origin' $cors_origin always; + add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, PATCH, DELETE, OPTIONS' always; + add_header 'Access-Control-Allow-Headers' 'Authorization, Content-Type, X-WP-Nonce' always; + add_header 'Access-Control-Allow-Credentials' 'true' always; + add_header 'Access-Control-Max-Age' 86400 always; + add_header 'Content-Length' 0; + add_header 'Content-Type' 'text/plain'; + return 204; + } + + # Add CORS headers to actual requests + add_header 'Access-Control-Allow-Origin' $cors_origin always; + add_header 'Access-Control-Allow-Credentials' 'true' always; + add_header 'Access-Control-Expose-Headers' 'X-WP-Total, X-WP-TotalPages, Link' always; + + try_files $uri $uri/ /index.php?$args; + } + + # Pass the PHP scripts to PHP-FPM listening on unix socket + location ~ \.php$ { + try_files $uri =404; + fastcgi_split_path_info ^(.+\.php)(/.+)$; + fastcgi_pass unix:/run/php-fpm.sock; + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + fastcgi_param PATH_INFO $fastcgi_path_info; + fastcgi_param HTTP_X_FORWARDED_PROTO $http_x_forwarded_proto; + fastcgi_index index.php; + fastcgi_buffering off; + fastcgi_read_timeout 300; + include fastcgi_params; + } + + # Cache static assets + location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { + expires max; + log_not_found off; + access_log off; + } + + location = /favicon.ico { + log_not_found off; + access_log off; + } + + location = /robots.txt { + allow all; + log_not_found off; + access_log off; + } + + # Block XML-RPC by default, allow with secret token + # Usage: /xmlrpc.php?token=YOUR_XMLRPC_TOKEN + location = /xmlrpc.php { + set $xmlrpc_allowed 0; + + # Allow if valid token provided (set in environment or change here) + if ($arg_token = "xrpc-9f8e7d6c5b4a") { + set $xmlrpc_allowed 1; + } + + # Block if no valid token + if ($xmlrpc_allowed = 0) { + return 403; + } + + # Pass to PHP if allowed + try_files $uri =404; + fastcgi_split_path_info ^(.+\.php)(/.+)$; + fastcgi_pass unix:/run/php-fpm.sock; + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + fastcgi_index index.php; + include fastcgi_params; + } + + # Deny access to hidden files + location ~ /\. { + deny all; + access_log off; + log_not_found off; + } + + # Deny access to backup files + location ~ ~$ { + access_log off; + log_not_found off; + deny all; + } + + # Allow fpm ping and status from localhost + location ~ ^/(fpm-status|fpm-ping)$ { + access_log off; + allow 127.0.0.1; + deny all; + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + include fastcgi_params; + fastcgi_pass unix:/run/php-fpm.sock; + } +} \ No newline at end of file diff --git a/server-php/config/fpm-pool.conf.template b/server-php/config/fpm-pool.conf.template new file mode 100644 index 0000000..6de2225 --- /dev/null +++ b/server-php/config/fpm-pool.conf.template @@ -0,0 +1,43 @@ +[global] +; Log to stderr +error_log = /dev/stderr + +[www] +; User and group for PHP-FPM processes +user = nobody +group = nobody + +; The address on which to accept FastCGI requests. +listen = /run/php-fpm.sock + +; Set permissions for unix socket +listen.owner = nobody +listen.group = nobody +listen.mode = 0666 + +; Enable status page +pm.status_path = /fpm-status + +; Ondemand process manager +pm = ondemand + +; The maximum number of child processes +pm.max_children = 100 + +; The number of seconds after which an idle process will be killed. +pm.process_idle_timeout = 10s + +; The number of requests each child process should execute before respawning. +pm.max_requests = 1000 + +; Make sure the FPM workers can reach the environment variables for configuration +clear_env = no + +; Catch output from PHP +catch_workers_output = yes + +; Remove the 'child 10 said into stderr' prefix in the log and only show the actual message +decorate_workers_output = no + +; Enable ping page to use in healthcheck +ping.path = /fpm-ping \ No newline at end of file diff --git a/server-php/config/nginx-performance.conf b/server-php/config/nginx-performance.conf new file mode 100644 index 0000000..457fefe --- /dev/null +++ b/server-php/config/nginx-performance.conf @@ -0,0 +1,22 @@ +# Production performance optimizations +gzip on; +gzip_vary on; +gzip_proxied any; +gzip_comp_level 6; +gzip_types text/plain text/css text/xml application/json application/javascript application/rss+xml application/atom+xml image/svg+xml; + +# Brotli (if available) +brotli on; +brotli_comp_level 6; +brotli_types text/plain text/css text/xml application/json application/javascript application/rss+xml application/atom+xml image/svg+xml; + +# Cache static files +location ~* \.(jpg|jpeg|png|gif|ico|css|js|woff|woff2|ttf|svg)$ { + expires 30d; + add_header Cache-Control "public, immutable"; +} + +# Security headers +add_header X-Frame-Options "SAMEORIGIN" always; +add_header X-Content-Type-Options "nosniff" always; +add_header X-XSS-Protection "1; mode=block" always; diff --git a/server-php/config/nginx.conf b/server-php/config/nginx.conf new file mode 100644 index 0000000..f3b5f1f --- /dev/null +++ b/server-php/config/nginx.conf @@ -0,0 +1,63 @@ +upstream php-fpm { + server host-uk-dev-wordpress:9000; +} + +server { + listen 80; + server_name _; + + root /var/www/html; + index index.php; + + client_max_body_size 64M; + + # Security headers + add_header X-Content-Type-Options "nosniff" always; + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-XSS-Protection "1; mode=block" always; + + location / { + try_files $uri $uri/ /index.php?$args; + } + + location ~ \.php$ { + fastcgi_split_path_info ^(.+\.php)(/.+)$; + fastcgi_pass php-fpm; + fastcgi_index index.php; + include fastcgi_params; + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + fastcgi_param PATH_INFO $fastcgi_path_info; + fastcgi_param HTTP_X_FORWARDED_PROTO $http_x_forwarded_proto; + fastcgi_buffering off; + fastcgi_read_timeout 300; + } + + location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { + expires max; + log_not_found off; + access_log off; + } + + location = /favicon.ico { + log_not_found off; + access_log off; + } + + location = /robots.txt { + allow all; + log_not_found off; + access_log off; + } + + location ~ /\. { + deny all; + access_log off; + log_not_found off; + } + + location ~ ~$ { + access_log off; + log_not_found off; + deny all; + } +} \ No newline at end of file diff --git a/server-php/config/opcache-prod.ini b/server-php/config/opcache-prod.ini new file mode 100644 index 0000000..e38b26c --- /dev/null +++ b/server-php/config/opcache-prod.ini @@ -0,0 +1,10 @@ +[opcache] +opcache.enable=1 +opcache.memory_consumption=256 +opcache.interned_strings_buffer=32 +opcache.max_accelerated_files=20000 +opcache.validate_timestamps=0 +opcache.save_comments=1 +opcache.enable_cli=1 +opcache.jit=1255 +opcache.jit_buffer_size=128M diff --git a/server-php/config/php-dev.ini b/server-php/config/php-dev.ini new file mode 100644 index 0000000..8354b1e --- /dev/null +++ b/server-php/config/php-dev.ini @@ -0,0 +1,11 @@ +; Development PHP settings +display_errors = On +display_startup_errors = On +error_reporting = E_ALL +log_errors = On + +memory_limit = 512M +max_execution_time = 300 +max_input_time = 300 +post_max_size = 128M +upload_max_filesize = 128M diff --git a/server-php/config/php-prod.ini b/server-php/config/php-prod.ini new file mode 100644 index 0000000..f393227 --- /dev/null +++ b/server-php/config/php-prod.ini @@ -0,0 +1,17 @@ +; Production PHP settings +display_errors = Off +display_startup_errors = Off +error_reporting = E_ALL & ~E_DEPRECATED & ~E_STRICT +log_errors = On +error_log = /var/log/php/error.log + +memory_limit = 256M +max_execution_time = 60 +max_input_time = 60 +post_max_size = 64M +upload_max_filesize = 64M + +expose_php = Off +session.cookie_httponly = 1 +session.cookie_secure = 1 +session.use_strict_mode = 1 diff --git a/server-php/config/php.ini.template b/server-php/config/php.ini.template new file mode 100644 index 0000000..21a9764 --- /dev/null +++ b/server-php/config/php.ini.template @@ -0,0 +1,9 @@ +[Date] +date.timezone="UTC" + +[PHP] +expose_php = Off +upload_max_filesize = 64M +post_max_size = 64M +memory_limit = 256M +max_execution_time = 300 \ No newline at end of file diff --git a/server-php/config/supervisord.conf b/server-php/config/supervisord.conf new file mode 100644 index 0000000..2c1b000 --- /dev/null +++ b/server-php/config/supervisord.conf @@ -0,0 +1,24 @@ +[supervisord] +nodaemon=true +user=root +logfile=/dev/null +logfile_maxbytes=0 +pidfile=/run/supervisord.pid + +[program:php-fpm] +command=php-fpm84 -F +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes=0 +autorestart=false +startretries=0 + +[program:nginx] +command=nginx -g 'daemon off;' +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes=0 +autorestart=false +startretries=0 \ No newline at end of file diff --git a/server-php/config/xdebug.ini b/server-php/config/xdebug.ini new file mode 100644 index 0000000..db550ed --- /dev/null +++ b/server-php/config/xdebug.ini @@ -0,0 +1,7 @@ +[xdebug] +xdebug.mode = develop,debug +xdebug.start_with_request = trigger +xdebug.client_host = host.docker.internal +xdebug.client_port = 9003 +xdebug.idekey = PHPSTORM +xdebug.log_level = 0 diff --git a/server-php/scripts/entrypoint.sh b/server-php/scripts/entrypoint.sh new file mode 100644 index 0000000..156939c --- /dev/null +++ b/server-php/scripts/entrypoint.sh @@ -0,0 +1,20 @@ +#!/bin/sh +set -e + +# Install PHP dependencies if vendor directory is empty +if [ ! -f "/app/vendor/autoload.php" ]; then + echo "Installing PHP dependencies..." + composer install --no-interaction --prefer-dist +fi + +# Install Node dependencies if node_modules is empty +if [ ! -d "/app/node_modules" ] || [ -z "$(ls -A /app/node_modules 2>/dev/null)" ]; then + echo "Installing Node dependencies..." + npm install +fi + +# Run database migrations (skip errors if DB not ready) +php artisan migrate --force 2>/dev/null || echo "Migrations skipped (DB may not be ready)" + +# Execute the main command +exec "$@"