Skip to content

Releases

v5.1.1

Choose a tag to compare

@jaydrogers jaydrogers released this 05 Nov 16:05
Immutable release. Only release title and notes can be modified.

🔐 Security update: FrankenPHP 1.13.5

Important

Pull and redeploy if you use frankenphp. FrankenPHP 1.13.5 fixes 17 vulnerabilities. Several affect the default php_server setup our images use, including one that runs an uploaded non-PHP file as PHP. It also moves to Caddy 2.11.9, which has security fixes of its own.

Note

These issues are in php/frankenphp and caddyserver/caddy upstream, not in this repo. cli, fpm, fpm-nginx, and fpm-apache are not affected.


🐘 FrankenPHP v1.13.5

Advisory Issue
GHSA-j99q-3wr8-xj43 High. php_server could run a file as PHP even when its name didn't end in .php, such as /uploads/photos.php.php/avatar.jpg. Anyone who could upload a file under the document root could execute it.
GHSA-jvmv-5538-pqpg The trailing-slash redirect for directories could send visitors to another host
GHSA-2725-rh2w-9j9w Over HTTP/2, a host header sent next to :authority set $_SERVER['HTTP_HOST'] to a different host than the one Caddy routed
GHSA-j45x-h238-h4xx One slow request body per PHP thread could block every thread, even with request_body_timeout set
GHSA-5359-vg53-5464 A request with more parameters than max_input_vars, sent repeatedly, crashed the process
GHSA-q6vv-3p75-53wr exit() or die() with a negative status, such as die(-1), stopped FrankenPHP
GHSA-2x4j-r638-wxff Dot segments after a PHP script, such as /admin/tool.php/../../missing.php, could get past a Caddy path rule meant to deny it
GHSA-388m-835f-7723 A client's Authorization: Basic header, and four other variables, could replace values set with env
GHSA-g7pw-whv8-97j7 A php_server or php directive in a Caddy named route could run with another site's root, env values, and workers
GHSA-v7hf-4gqv-hf8p A php_server with a worker match rule could serve PHP source code
GHSA-8qcf-v46r-g2mc In worker mode, destructors from the previous request, including session objects, could send output and cookies to the next client
GHSA-j2f3-wxf2-8qgv Embedded apps: a config reload deleted the extracted app directory while it was still being served
GHSA-xh92-4p94-ch7x php_server served a Caddyfile stored in the document root
GHSA-hfxc-999w-mp67 FrankenPHP crashed when the OS refused to create a new PHP thread
GHSA-g9v4-rp77-rv76 Clients could probe whether files under the document root exist through PATH_INFO
GHSA-cp2q-fc46-38f5, GHSA-28vm-87fh-3px5 Custom builds with a Go extension that uses the experimental worker APIs only

Our images already returned 403 for any .php path under /storage, which covers Laravel's public uploads. Uploads anywhere else in your document root were exposed to the first one.

Our Caddyfile lives in /etc/frankenphp, not the document root. Embedded apps, named routes, and custom Go extensions need setups our images don't use by default.

It also upgrades Caddy to 2.11.9 and Mercure to 1.1.6. Request bodies over Caddy's request_body max_size now get a 413, missing scripts return 404 No input file specified. like PHP-FPM, and $_SERVER['SERVER_ADDR'] is now set.

Worker mode keeps a little more state between requests

If you run Laravel Octane or another worker, error_get_last(), strtok(), http_get_last_response_headers(), and libxml errors now carry over between requests, like the rest of your app's state. FrankenPHP still resets superglobals, the request body, headers, output, and the session. Call error_clear_last(), libxml_clear_errors(), or http_clear_last_response_headers() if your code expects a clean slate. See php/frankenphp#2790.

FrankenPHP images now match the official runtime image

  • Fonts, video, and other static files get the right Content-Type. The image had no /etc/mime.types, so Caddy fell back to Go's short built-in list and guessed the rest. CSS, JavaScript, and common images were fine, but a .woff2 font could go out as text/plain. We now install mailcap like the official FrankenPHP images.
  • GODEBUG defaults to cgocheck=0, like the official images, so Go skips its pointer checks on every call into PHP. If you set GODEBUG yourself, it replaces this default, so include cgocheck=0 in your value: GODEBUG=cgocheck=0,http2debug=1. See the environment variable reference.

How to update

If you use Example Do this
A floating tag 8.5-frankenphp Pull and redeploy
A release tag 8.5-frankenphp-v5.1.0 Change it to 8.5-frankenphp-v5.1.1
A patch version tag 8.5.12-frankenphp Pull and redeploy

If you build your own image FROM one of these, rebuild it too.

Then confirm the version:

docker compose exec php frankenphp version

What's Changed

  • Security: Update FrankenPHP to v1.13.5 by @jaydrogers
  • Install mailcap and set GODEBUG=cgocheck=0 in the frankenphp images to match the official FrankenPHP images by @jaydrogers

Full Changelog: v5.1.0...v5.1.1

v5.1.0

Choose a tag to compare

@jaydrogers jaydrogers released this 04 Nov 17:05
Immutable release. Only release title and notes can be modified.

🤩 What's new

PHP security releases land much faster (#739, #742)

During the last critical PHP release (#721), we waited hours for Docker Hub's php images, then PECL went down. We now compile PHP ourselves with the same Dockerfiles as the official images, so neither one can hold up a release. A new hourly check starts a build as soon as php.net lists a patch, on any day of the week.

From php.net to your image
Before php.net → wait ~4 hours for Docker Hub's php images → PECL → wait for our next release or Tuesday rebuild
After php.net → about an hour later, we compile PHP and publish

Every build also starts from current Debian and Alpine packages, so OS security fixes reach your image on the weekly rebuild (Tuesday 08:00 UTC). PHP 8.1 benefits most: its official images haven't been rebuilt since it reached end of life.

Verify that an image came from us (#742)

Every image is now signed with cosign and ships with an SBOM (every package inside it) and build provenance (how and where it was built). Check an image before you run it:

cosign verify serversideup/php:8.5-cli \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com \
  --certificate-identity-regexp '^https://github\.com/serversideup/docker-php/\.github/workflows/service_publish-images\.yml@refs/(heads/main|tags/v.+)$'

Images published before 5.1 have provenance but no signature or SBOM. See Verifying images.

Every image is scanned for vulnerabilities (#742)

After every production build, Grype scans each published image. The results are on each run's summary page. You can see the known vulnerabilities in any image without running a scanner yourself. See Vulnerability scanning.

PHP 8.6 release candidates (#698, #739)

Test your app on PHP 8.6 before it ships. Every rebuild uses the newest release candidate on php.net, so a new RC arrives with the next weekly rebuild. These tags are for testing only.

serversideup/php:8.6-rc-fpm-nginx

New docs

The docs have been optimized from top to bottom. Every environment variable and helper command is now in a searchable reference, generated from the same data our CI checks against the code. Upgrading, Security, and Troubleshooting each have their own section. Every old URL redirects to its new page.

⬆️ Upgrading

Nothing to change. Every existing tag is still published and the images behave the same. Three things you may notice:

  • php -v now says Built by Server Side Up instead of Built by https://github.com/docker-library/php
  • install-php-extensions is upgraded from 2.12.0 to 2.12.6
  • If you run docker-php-serversideup-install-php-ext-installer without a version in your own Dockerfile, it now installs the latest release instead of 2.12.0. Pass a version to pin it.

🐛 Bug fixes

  • docker-php-serversideup-download printed every shell option to stdout before the file when you didn't give it an output file (#739)
  • FrankenPHP exited at start-up, logging no error, when CADDY_AUTO_HTTPS was disable_redirects, disable_certs, or ignore_loaded_certs and SSL_MODE was mixed or full
  • docker-php-serversideup-set-id failed the build with "UID already in use" when another user in the image already had the UID you passed, because it moved the wrong user. It now moves the other user or group to 99<id> and gives yours the ID you asked for

🏗️ For contributors

  • Every pinned version lives in scripts/conf/php-versions-base-config.yml, and Renovate replaces Dependabot to keep them current (#739)
  • scripts/dev.sh --build-php-base builds exactly what CI builds. See Build an image
  • Every GitHub Action is pinned to a commit SHA, so a moved tag can't change what runs in CI. A new zizmor lint checks the workflows on every PR, and an OpenSSF Scorecard badge tracks our supply chain practices (#742)
  • Image tests are split into one file per feature area in scripts/tests/image/, and every failure shows up in one run. New tests cover Laravel automations, startup scripts, SSL modes, trusted proxies, PHP-FPM pool variables, file permissions, and shutdown (#741)
  • Failed CI runs now actually retry once on their own (#739)
  • Building src/variations/*/Dockerfile directly now needs the dependency build args from scripts/conf/php-versions-base-config.yml, so use scripts/dev.sh

Full Changelog: v5.0.5...v5.1.0

v5.0.5

Choose a tag to compare

@jaydrogers jaydrogers released this 27 Oct 16:22
Immutable release. Only release title and notes can be modified.
af1462e

🔐 Security update: file permissions in the fpm and frankenphp images

Important

Pull and redeploy if you use fpm or frankenphp. Releases v4.3.1 through v5.0.4 shipped with bad default permissions. This release patches those permissions. See GHSA-xx74-jh2r-mgm3 for more details.

Note

cli, fpm-nginx, and fpm-apache are not affected. This applies to Docker Hub and GitHub Container Registry.


What we fixed

Two COPY instructions in the fpm and frankenphp Dockerfiles didn't set permissions, so the images picked up directory modes from the build environment. Both now set them explicitly.

We also added a check to CI. Every image is now tested for world-writable files and directories before it's published, so this can't ship again.

Thanks to @lance-compuco for reporting this responsibly.

How to update

If you use Example Do this
A floating tag 8.4-fpm Pull and redeploy
A release tag 8.4-fpm-v5.0.4 Change it to 8.4-fpm-v5.0.5
A patch version tag 8.4.27-fpm Pull and redeploy

If you build your own image FROM one of these, rebuild it too.

What's Changed

  • Security: Set explicit permissions on copied configs in the fpm and frankenphp images by @jaydrogers
  • Fail CI when an image contains world-writable files or directories by @jaydrogers

Full Changelog: v5.0.4...v5.0.5

v5.0.4

Choose a tag to compare

@jaydrogers jaydrogers released this 26 Oct 23:04
Immutable release. Only release title and notes can be modified.
399b38b

🔐 Security update: Brotli CVE-2026-111497 and FrankenPHP 1.13.4

Important

Pull and redeploy. The official PHP images ship a version of the Brotli library with an out-of-bounds write that can lead to remote code execution (CVE-2026-111497, also called Node2Shell). PHP's curl extension uses it, so every variation is affected. This release upgrades Brotli in every image. The FrankenPHP images also move to FrankenPHP 1.13.4, which fixes a crash that 1.13.2 (shipped in v5.0.3) introduced.

Note

These issues are in google/brotli and php/frankenphp upstream, not in this repo. PHP versions are the same as v5.0.3.


Brotli is patched in every image

CVE Issue Fix
CVE-2026-111497 Out-of-bounds write in the Brotli decoder that permits remote code execution Brotli 1.1.1 and 1.2.1, plus distribution backports

PHP reaches the Brotli decoder through curl when it decodes a Brotli-compressed response. A server your app calls could send one.

The official PHP images haven't been rebuilt with the fix yet. Our images now upgrade Brotli during the build:

Base OS Before Now
Debian Bookworm 1.0.9-2+b6 1.0.9-2+deb12u1
Debian Trixie 1.1.0-2+b7 1.1.0-2+deb13u1
Alpine 3.23 and 3.24 1.2.0-r1 1.2.1-r0
Alpine 3.22 (PHP 8.1) 1.1.0-r2 1.1.1-r0

Alpine fixed 3.22 with Brotli 1.1.1. It hasn't added the CVE to that branch's security database yet, so vulnerability scanners may still flag the PHP 8.1 Alpine images until it does.

We'll drop this workaround once the official PHP images ship the fix.

🐘 FrankenPHP v1.13.4

FrankenPHP 1.13.4 is a security release. It fixes these issues in FrankenPHP itself:

Advisory Issue
GHSA-5f4m-ff4w-g458 High. In worker mode, a single unauthenticated request could crash the whole process. Only 1.13.2 and 1.13.3 are affected.
GHSA-rgpj-f2qx-xwhf Calling putenv() from a shutdown function, destructor, or session handler could crash the process or leak the previous request's value
GHSA-cm3q-jw2q-9rh5 In worker mode, an SplTempFileObject kept across requests had its stream freed
GHSA-8386-64gm-3m8q In worker mode, per-request environment variables leaked into later requests
GHSA-pmvg-8483-mw84 In worker mode, http_get_last_response_headers() returned headers from a previous request
GHSA-h764-4wh7-m7j2 In worker mode, ending the worker loop after a JSON POST freed the same pointer twice
GHSA-x6j6-qv7w-fq3c In worker mode, a fatal error during request startup left the request waiting forever

If you run Laravel Octane or another worker mode setup on v5.0.3, the first one affects you. FrankenPHP says the others need specific app code or non-default configuration. They don't affect common Laravel and Symfony setups.

It also upgrades Mercure to 1.1.5.

Only the frankenphp images include FrankenPHP.

How to update

If you use Example Do this
A floating tag 8.4-fpm-nginx Pull and redeploy
A release tag 8.4-fpm-nginx-v5.0.3 Change it to 8.4-fpm-nginx-v5.0.4
A patch version tag 8.4.27-fpm-nginx Pull and redeploy

Then confirm Brotli is patched. On Debian:

docker compose exec php dpkg-query -W libbrotli1

On Alpine:

docker compose exec php sh -c "apk list -I | grep brotli-libs"

On FrankenPHP, confirm the version too:

docker compose exec php frankenphp version

FrankenPHP 1.13.4 needs a newer Brotli than Debian Bookworm ships. The Bookworm FrankenPHP images bundle Brotli 1.2.1 in /usr/local/lib, and both FrankenPHP and curl use it.

What's Changed

Full Changelog: v5.0.3...v5.0.4

v5.0.3

Choose a tag to compare

@jaydrogers jaydrogers released this 23 Oct 12:47
Immutable release. Only release title and notes can be modified.
6a0b5be

🔐 Security update: PHP session upload progress and FrankenPHP 1.13.2

Important

Pull and redeploy. PHP has announced a new remote code execution vulnerability (CVE-2026-115560) that isn't fixed yet. It's separate from the two CVEs that v5.0.2 patched, so v5.0.2 doesn't protect you from it. This release turns off session upload progress, which blocks it. The FrankenPHP images also move to FrankenPHP 1.13.2.

Note

These issues are in php/php-src and php/frankenphp upstream, not in this repo. PHP versions are the same as v5.0.2.


A new PHP vulnerability, not yet fixed

CVE Issue PHP fix
CVE-2026-115560 Remote code execution through session upload progress Not released yet. PHP will publish the details and the fix in its next regular release.

This is a different vulnerability from CVE-2026-109618 and CVE-2026-109619, which PHP fixed on October 21. Updating to PHP 8.5.12, 8.4.27, 8.3.36, or 8.2.35 doesn't fix it.

Until PHP ships the fix, they recommend turning off session upload progress. That's what this release does. The same setting was already a suggested workaround for CVE-2026-109619. If you turned it off yourself on October 21, keep it off.

Your app is only exposed if all of these are true:

  1. session.serialize_handler is set to php_binary or php_serialize in your config
  2. Your code switches it to php with ini_set() before calling session_start()
  3. Session upload progress is enabled (PHP's default)

Our images default session.serialize_handler to php. That makes this unlikely. We'd still rather ship the safe default than have you check every app by hand.

Who this affects

Only apps that read upload progress from $_SESSION.

If you need it and never change session.serialize_handler at runtime, turn it back on:

services:
  php:
    image: serversideup/php:8.4-fpm-nginx
    environment:
      PHP_SESSION_UPLOAD_PROGRESS_ENABLED: "1"

We'll revisit this default once PHP ships its fix.

🐘 FrankenPHP v1.13.2

FrankenPHP 1.13.2 came out after v5.0.2 was built. It fixes these issues in FrankenPHP itself:

Advisory Issue
GHSA-fq24-r53r-xcx3 An unauthenticated client could keep every PHP thread busy by queuing requests and disconnecting
GHSA-4gfw-9v29-gp9h In worker mode, error_get_last() and libxml_get_errors() could leak a previous request's input and server paths
GHSA-ccgp-ch9h-533f A self-referencing array passed to frankenphp_log() or a worker callback crashed the whole process

It also makes frankenphp_log() available in php-cli. Hot reload no longer serves stale pages.

Only the frankenphp images include FrankenPHP.

How to update

If you use Example Do this
A floating tag 8.4-fpm-nginx Pull and redeploy
A release tag 8.4-fpm-nginx-v5.0.2 Change it to 8.4-fpm-nginx-v5.0.3
A patch version tag 8.4.27-fpm-nginx Pull and redeploy

Then confirm the setting is off:

docker compose exec php php -i | grep session.upload_progress.enabled

On FrankenPHP, confirm the version too:

docker compose exec php frankenphp version

What's Changed

Full Changelog: v5.0.2...v5.0.3

v5.0.2

Choose a tag to compare

@jaydrogers jaydrogers released this 21 Oct 17:31
Immutable release. Only release title and notes can be modified.
31651b0

🚨 Critical PHP security update

Important

Update now. Patched images for PHP 8.5.12, 8.4.27, 8.3.36, and 8.2.35 are live on Docker Hub and GitHub Packages. Every image was built and tested before anything was published. See #721 for how today's release unfolded.

Note

The CVEs below are in php/php-src upstream, not in this repo. This release rebuilds every image on the patched official Docker PHP images. We didn't need to change any code for it.


Upstream CVEs (PHP 8.5.12, 8.4.27, 8.3.36, 8.2.35)

CVE Severity Issue PHP advisory
CVE-2026-109618 Critical (9.2) Remote code execution through out-of-memory corruption GHSA-w8hw-w9fx-j79w
CVE-2026-109619 Critical (9.2) Remote code execution through stale global state GHSA-6wmf-4rv5-p3rp

Both can be exploited without logging in. See php.net's advisory for who is at risk.

PHP Version in this release
8.5 8.5.12
8.4 8.4.27
8.3 8.3.36
8.2 8.2.35
8.1 8.1.34 (not patched, end of life)

PHP 8.1 is end of life, so php.net didn't release a fix for it. If you're on 8.1, move to 8.2 or newer.

How to update

If you use Example Do this
A floating tag 8.4-fpm-nginx Pull and redeploy
A release tag 8.4-fpm-nginx-v5.0.1 Change it to 8.4-fpm-nginx-v5.0.2
A patch version tag 8.4.26-fpm-nginx Change it to 8.4.27-fpm-nginx

Then confirm you're on the patched version:

docker compose exec php php -v

🐘 FrankenPHP v1.13.1

FrankenPHP 1.13.0, which shipped in v5.0.0 and v5.0.1, had several regressions. v1.13.1 fixes them, including:

  • Laravel and Symfony apps that use fastcgi_finish_request() over HTTP/2 could crash the process (php/frankenphp#2694)
  • Workers declared in the Caddyfile gave up after the first failed boot instead of retrying (php/frankenphp#2704)
  • php-cli passed the wrong $argv, which broke Symfony Console (php/frankenphp#2690)
  • Requests served during a graceful shutdown returned HTTP 500 (php/frankenphp#2644)
  • The process segfaulted when FrankenPHP restarted an unhealthy thread (php/frankenphp#2688)

We also made two fixes of our own in the FrankenPHP image:

  • The Mercure hub is no longer compressed. Each subscriber held its own compression encoder (a few MiB each with zstd). Compressing private updates also opens the door to BREACH-style attacks. Everything else is still compressed.
  • A failed extension install now fails the build. Before, if pecl.php.net timed out mid-build, the FrankenPHP image could publish without extensions like redis and zip. The other variations already worked this way.

Full Changelog: v5.0.1...v5.0.2

v4.5.3

Choose a tag to compare

@jaydrogers jaydrogers released this 21 Oct 20:56
Immutable release. Only release title and notes can be modified.

🚨 Critical PHP and NGINX security update for v4 users

Important

This release is only for people who can't move to v5 yet. It rebuilds v4.5.1 on PHP 8.5.12, 8.4.27, 8.3.36, and 8.2.35, and updates NGINX to 1.30.6. If you're already on v5, use v5.0.2 instead. See #721 for the full story.

Warning

v4.5.3 is published to release tags only (like 8.4-fpm-nginx-v4.5.3). Floating tags like 8.4-fpm-nginx and latest stay on v5, so you need to update your pin to get this fix.

v4.5.2 was tagged but never published. Go straight from v4.5.1 to v4.5.3.

Note

The CVEs below are in php/php-src and nginx/nginx upstream, not in this repo. This release rebuilds the v4 images on the patched official Docker PHP images and bumps the NGINX we install.


Upstream CVEs (PHP 8.5.12, 8.4.27, 8.3.36, 8.2.35)

CVE Severity Issue PHP advisory
CVE-2026-109618 Critical (9.2) Remote code execution through out-of-memory corruption GHSA-w8hw-w9fx-j79w
CVE-2026-109619 Critical (9.2) Remote code execution through stale global state GHSA-6wmf-4rv5-p3rp

Both can be exploited without logging in. See php.net's advisory for who is at risk.

PHP Version in this release
8.5 8.5.12
8.4 8.4.27
8.3 8.3.36
8.2 8.2.35
8.1 and older Not included (no official fix)

php.net didn't release a fix for 8.1 or older, so this release doesn't rebuild them. If you're on 8.1 or older, move to 8.2 or newer.

Upstream CVEs (NGINX 1.30.6)

These were fixed for v5 in v5.0.1. This brings the same fixes to v4.

CVE nginx Issue F5 advisory
CVE-2026-15176 Med Buffer overflow when using alias with rewrite K000162416
CVE-2026-93479 Med Buffer overflow in script evaluation (string expressions with variables) K000162607
CVE-2026-95101 Med Request body injection in HTTP/3 K000162613
CVE-2026-97629 Med Session forgery with TLS session ticket key rotation K000162606
CVE-2026-97205 Med Use-after-free in ngx_http_ssi_module with AIO K000162608

Only the fpm-nginx images include NGINX.

🐘 FrankenPHP build pinned

FrankenPHP stays on 1.12.6. Its build used to pull the latest Caddy and Caddy modules, which no longer build with FrankenPHP 1.12.6. Caddy is now pinned to 2.11.4, with Mercure, Vulcain, and Brotli on the versions FrankenPHP 1.12.6 ships with. This also keeps out the Caddy 2.11.6 HTTP/2 crash from #712.

How to update

Change v4.5.1 (or whichever v4 release you pin) to v4.5.3:

Before After
8.4-fpm-nginx-v4.5.1 8.4-fpm-nginx-v4.5.3
8.4-fpm-nginx-alpine-v4.5.1 8.4-fpm-nginx-alpine-v4.5.3
8.3-cli-bookworm-v4.5.1 8.3-cli-bookworm-v4.5.3

Then confirm you're on the patched version:

docker compose exec php php -v

Plan your move to v5

v4 will not get regular updates. This was a one-off because of how severe these CVEs are. The upgrade guide walks through the v5 changes.

Full Changelog: v4.5.1...v4.5.3

v4.5.2

Choose a tag to compare

@jaydrogers jaydrogers released this 21 Oct 20:39
Immutable release. Only release title and notes can be modified.

Superseded by v4.5.3. No images were published for v4.5.2.

v5.0.1

Choose a tag to compare

@jaydrogers jaydrogers released this 14 Oct 16:17
Immutable release. Only release title and notes can be modified.

🔐 Security update for NGINX users

Note

CVEs below are in nginx/nginx upstream, not in this repo. This release only bumps the NGINX we install.


Upstream CVEs (NGINX 1.30.6)

CVE nginx Issue F5 advisory
CVE-2026-15176 Med Buffer overflow when using alias with rewrite K000162416
CVE-2026-93479 Med Buffer overflow in script evaluation (string expressions with variables) K000162607
CVE-2026-95101 Med Request body injection in HTTP/3 K000162613
CVE-2026-97629 Med Session forgery with TLS session ticket key rotation K000162606
CVE-2026-97205 Med Use-after-free in ngx_http_ssi_module with AIO K000162608

Fixed in 1.30.6+ stable (1.31.7+ mainline) per nginx.org advisories.

What's Changed

  • Security: Update NGINX to 1.30.6 for Alpine and Debian by @jaydrogers
  • Updated SECURITY.md with clearer reporting instructions, including where to report vulnerabilities in PHP itself
  • Updated CI/CD Actions versions to latest versions

Full Changelog: v5.0.0...v5.0.1

v5.0.0

Choose a tag to compare

@jaydrogers jaydrogers released this 08 Oct 17:15
Immutable release. Only release title and notes can be modified.
4e7f8c5

🤩 What's new

Short-lived & IP address certificates for FrankenPHP

image FrankenPHP can now request Let's Encrypt's new short-lived (~6-day) certificate profile through the new `CADDY_ACME_PROFILE` variable. Short-lived certificates skip revocation entirely, and the same profile is what lets you issue certificates for raw IP addresses, which is handy for services that don't have a domain in front of them.

Configure short-lived & IP certificates →

Native Laravel Octane support with a hardened and optimized Caddyfile (#604)

Add --caddyfile=/etc/frankenphp/Caddyfile to octane:start and FrankenPHP switches into worker mode with the same trusted proxies, security headers, asset caching, SSL modes, and health check as classic mode. Octane's --workers, --watch, --max-requests, octane:status, and octane:reload all work. No wrapper script, no second Caddyfile.

command: ["php", "artisan", "octane:start", "--server=frankenphp", "--port=8080", "--caddyfile=/etc/frankenphp/Caddyfile"]

Thanks to @aSeriousDeveloper for opening the original PR and coolio85 on Discord for the workaround that started it.

Read the Octane guide →

Expanded OPcache support and better defaults (#704)

OPcache is still off by default to save your hair when you're in development. When you set PHP_OPCACHE_ENABLE=1, you now get the settings from Symfony's performance guide instead of PHP's stock values: more memory, room for more files, and no filesystem checks per request. Preloading is available with PHP_OPCACHE_PRELOAD. This is a breaking change if you mount your code as a volume with OPcache enabled (see below).

Read the production performance tuning guide →

Trusted proxy support for every variation (#643)

TRUSTED_PROXY now works the same way across all variations (FrankenPHP, NGINX, and Apache), so your app sees the real client IP whether it's behind Cloudflare, a load balancer, or your own reverse proxy. The version in the v4.6.0 betas returned the wrong IP in multi-hop Docker setups (#703). That's fixed, and FrankenPHP's REMOTE_ADDR now matches what Caddy resolves. FrankenPHP users also get /etc/frankenphp/caddyfile-global.d/ for global Caddy config without escaping CADDY_GLOBAL_OPTIONS.

Configuring trusted proxies →

Native Laravel Nightwatch support (#572)

Added a native health check for Laravel Nightwatch, complete with documentation to wire it up.

Laravel Nightwatch with Docker →

CI improvements: Every image is tested before it's published (#700)

Build and publish used to be one step. Now every image is built for amd64 and arm64, pulled on native runners, and checked before anything ships: it starts, runs unprivileged, loads the default extensions, passes its health check, and serves PHP through its web server. If one image fails, nothing from that run is published. Read more about how this works in #700.

New environment variables

All of them are documented in the environment variable specification.

Variable Default Applies to What it does Learn more
TRUSTED_PROXY cloudflare fpm-nginx, fpm-apache, frankenphp Which proxy IPs are trusted to pass the real client IP. Options: cloudflare, sucuri, local, or off. #643
CADDY_ACME_PROFILE off frankenphp Select a Let's Encrypt certificate profile: shortlived (~6-day certs, required for IP-address certificates), tlsserver, classic, or off. Configuring SSL
LARAVEL_OCTANE unset frankenphp Set by Octane, not by you. The Caddyfile uses it to switch into worker mode. #604
AUTORUN_LARAVEL_SKIP_IF_NOT_FOUND false all When true, Laravel automations exit gracefully if Laravel is not found in APP_BASE_DIR instead of failing the container. Requires AUTORUN_ENABLED=true. Laravel automations
PHP_OPCACHE_ENABLE_CLI 1 all Whether CLI commands use OPcache when PHP_OPCACHE_ENABLE=1. 1 is what v4 did. Set to 0 to keep OPcache on for the web server only. #704
PHP_OPCACHE_PRELOAD "" all Path to a preload script. #704
PHP_OPCACHE_PRELOAD_USER "" all The user to preload as when the container runs as root. #704
PHP_FILE_UPLOADS On all Whether to allow HTTP file uploads. Set to Off to harden apps that never accept uploads. #697
PHP_MAX_FILE_UPLOADS 20 all Maximum number of files that can be uploaded in a single request. #697
PHP_DISABLE_FUNCTIONS "" all Comma-separated list of PHP functions to disable, such as exec,shell_exec. Empty because Laravel, Composer, and Symfony Process rely on proc_open. #692
PHP_HTML_ERRORS On all Format on-screen errors as HTML when PHP_DISPLAY_ERRORS is on. Never affects logged errors, and PHP forces it off for the CLI. #692
PHP_REALPATH_CACHE_SIZE 4096K all Size of PHP's realpath cache. Apps with a large vendor/ directory may benefit from raising it. #692
PHP_SESSION_COOKIE_HTTPONLY On all Adds the HttpOnly flag to the native PHP session cookie so browser scripts cannot read it. See the note below. #692

Changed defaults

The PHP_OPCACHE_* values only apply when PHP_OPCACHE_ENABLE=1. CADDY_* applies to FrankenPHP only.

Variable v4 v5
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
CADDY_LOG_OUTPUT stdout stderr
CADDY_LOG_FORMAT console auto

Add your own Caddyfile rules from a folder

FrankenPHP users can now drop a .caddyfile into /etc/frankenphp/caddyfile-server.d/ to add headers, redirects, or request matchers to their app's site. Before, the only way to do that was CADDY_SERVER_EXTRA_DIRECTIVES, which gets awkward past one line and is replaced when Laravel Octane starts FrankenPHP. The folder works in classic mode and with Octane.

# embed-cache.caddyfile
@embed path /embed.js
header @embed >Cache-Control "public, max-age=3600"
volumes:
  - ./embed-cache.caddyfile:/etc/frankenphp/caddyfile-server.d/embed-cache.caddyfile

Each level of the Caddyfile now has an environment variable for one-liners and a folder for files:

Level Environment variable Folder
Global options CADDY_GLOBAL_OPTIONS /etc/frankenphp/caddyfile-global.d/
Your app's site CADDY_SERVER_EXTRA_DIRECTIVES /etc/frankenphp/caddyfile-server.d/ (new)
Extra sites none /etc/frankenphp/caddyfile.d/

Rules in caddyfile-server.d/ apply on every listener SSL_MODE creates, and Docker's health check goes through them too. If you add a catch-all rule like basic_auth, leave /healthcheck out of it. The docs show how.

Add your own Caddyfile rules →

⚠️ Breaking changes

The v4 → v5 migration guide walks through each of these with a checklist. To stay on v4 while you review, pin your tag to v4.5.1 (for example serversideup/php:8.4-fpm-nginx-v4.5.1).

Dropped PHP 7.4 and 8.0 (#699)

Debian 11 (Bullseye) LTS ended on 2026-08-31, and the following week Debian removed the Bullseye packages from its mirrors, so we can no longer install anything inside the build. PHP 7.4 and 8.0 only ever shipped on Bullseye and Alpine 3.16, which reached EOL in 2024, so both go with it. PHP 8.1 is EOL too, but stays built (pinned to 8.1.34) on bookworm, trixie, and alpine3.22 because those bases still receive security updates.

PHP 8.1 also drops alpine3.21. Alpine 3.21 is supported until 2026-11-01, but 8.1 is EOL and only needs one Alpine base, so 8.1 on Alpine now means alpine3.22. Change 8.1-*-alpine3.21 tags to 8.1-*-alpine3.22.

Your existing tags still work. Every 7.4, 8.0, and 8.1 alpine3.21 tag stays pullable, frozen at its last successful build. If you need to install a package on one while you migrate, the migration guide has an unsupported recipe.

Our supported matrix is now PHP 8.2 – 8.5 on bookworm, trixie, alpine3.23, and alpine3.24, plus PHP 8.1 on bookworm, trixie, and alpine3.22. SECURITY.md now states the rule: we build an image while its official base image exists and the operating system still receives security updates.

Read more