Upgrade to v5
Version 5 tunes OPcache for production and follows Caddy's logging defaults on FrankenPHP. TRUSTED_PROXY now works the same on every web server. Most apps only need to check the first two changes below.
PHP 7.4 and 8.0 are no longer built. See PHP and OS versions we no longer build.
Stay on v4 while you review
Pin your tag to the last v4 release. Version-pinned tags are never rebuilt. You get no security updates until you move to v5.
services:
php:
image: serversideup/php:8.5-fpm-nginx-v4.5.3
Breaking changes
| Change | Likelihood of impact | You're affected if |
|---|---|---|
| OPcache no longer checks for file changes | High | You set PHP_OPCACHE_ENABLE=1 and change code without restarting the container |
| FrankenPHP logs go to stderr as JSON | Medium | You run frankenphp and read or filter its logs |
| Octane uses the image's Caddyfile | Medium | You run Laravel Octane on frankenphp |
| Mercure 1.0 rejects the old JWT options | Medium | You run the Mercure hub on frankenphp |
| Session cookies are HttpOnly | Low | Your JavaScript reads PHP's native session cookie |
| Session upload progress is off | Low | Your app reads upload progress from $_SESSION |
S6 dependencies use dependencies.d/ | Low | Your Dockerfile appends to an S6 dependencies file |
The S6 user bundle moved | Low | Your Dockerfile registers your own S6 service |
FrankenPHP sets REMOTE_ADDR to the client IP | Low | Your app compares REMOTE_ADDR to a proxy address |
| Caddy limits request headers | Low | Requests to frankenphp carry more than 16 KiB of headers |
num_threads no longer includes workers | Low | You set num_threads in FRANKENPHP_CONFIG with workers |
OPcache no longer checks for file changes
PHP_OPCACHE_VALIDATE_TIMESTAMPS now defaults to 0. With OPcache on, PHP caches each file until the container restarts, which is right for code built into the image. Version 4 checked every file every two seconds and used PHP's stock memory sizes. Version 5 uses the values from Symfony's performance guide instead (see Changed defaults).
You're 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 run WordPress from a volume and update it with
git pull, WP-CLI, or SFTP. - You run commands like
docker exec ... php artisan optimizeand expect the web server to pick up the new files.
Restart the container after code changes (recommended). To get the v4 behavior instead, set PHP_OPCACHE_VALIDATE_TIMESTAMPS=1. Production performance tuning covers the OPcache settings.
FrankenPHP logs go to stderr as JSON
CADDY_LOG_OUTPUT now defaults to stderr and CADDY_LOG_FORMAT to auto. Caddy, the official FrankenPHP image, and Laravel Octane all write logs to stderr. With auto, Caddy writes JSON when there is no terminal, which is always the case in a container. Version 4 forced console lines on stdout, which put ANSI color codes into log pipelines.
docker logs, Docker Compose, and Kubernetes show both streams, so most setups only notice the format. You're affected if:
- You only capture
stdout, for example with2>/dev/nullor a log driver rule. - Your log pipeline parses the
consolelines. - You prefer reading colored lines with
docker compose logs.
Let your pipeline read the level field of each JSON entry (recommended). To restore v4, set CADDY_LOG_FORMAT=console and CADDY_LOG_OUTPUT=stdout. Don't use either override with Octane, which only relays stderr and only parses JSON.
Octane uses the image's Caddyfile
Pass --caddyfile=/etc/frankenphp/Caddyfile to octane:start. The Caddyfile now adds the worker and routing itself when Octane starts FrankenPHP. Remove any worker { } block from FRANKENPHP_CONFIG and the routing you added to CADDY_PHP_SERVER_OPTIONS. Keeping the worker block fails with "global workers must not have duplicate filenames".
Octane needs the Caddy admin API and sets CADDY_GLOBAL_OPTIONS itself. While Octane runs, CADDY_ADMIN and CADDY_GLOBAL_OPTIONS are ignored. Put global options in a file in /etc/frankenphp/caddyfile-global.d/ instead. Classic mode doesn't change. See Laravel Octane.
Mercure 1.0 rejects the old JWT options
Version 5 ships FrankenPHP 1.13, which includes Mercure 1.0. FrankenPHP fails to start if you configure the hub with publisher_jwt or subscriber_jwt, including through the mercure array in config/octane.php.
Remove those options and turn the hub on with MERCURE_ENABLED=true and the other MERCURE_* variables, which work in classic mode and with Octane. If your app still signs 0.x tokens, add protocol_version_compatibility 8 to MERCURE_EXTRA_DIRECTIVES while you migrate. See FrankenPHP and the Mercure 1.0 upgrade guide.
Session cookies are HttpOnly
PHP_SESSION_COOKIE_HTTPONLY now defaults to On, as PHP recommends, so browser scripts can't read PHP's native session cookie. Laravel, Symfony, and WordPress set their own session cookies and aren't affected.
If your app calls session_start() and reads the session cookie from JavaScript, set PHP_SESSION_COOKIE_HTTPONLY=Off.
Session upload progress is off
Since v5.0.3, PHP_SESSION_UPLOAD_PROGRESS_ENABLED defaults to 0, following PHP's October 23, 2026 security advisory. PHP's own default is on.
If your app reads upload progress from $_SESSION, set it to 1, and only if your app never changes session.serialize_handler at runtime.
S6 dependencies use dependencies.d/
fpm-nginx and fpm-apache now declare service dependencies with the dependencies.d/ directory that S6 Overlay documents, instead of the deprecated dependencies file. This fixed a race where php-fpm could start before its pool user was written when the container runs as root.
S6 ignores the dependencies file once dependencies.d/ exists. If your Dockerfile appends to /etc/s6-overlay/s6-rc.d/<service>/dependencies for php-fpm, nginx, or apache2, create an empty file in dependencies.d/ instead:
RUN touch /etc/s6-overlay/s6-rc.d/nginx/dependencies.d/my-service
The S6 user bundle moved
Version 5 ships S6 Overlay v3.2.3.2, which defines the user bundle in /etc/s6-overlay/user-bundles.d/ instead of /etc/s6-overlay/s6-rc.d/. docker-php-serversideup-s6-init already writes to the new location.
If your Dockerfile creates /etc/s6-overlay/s6-rc.d/user/contents.d/<service>, create the empty file in the new directory instead:
RUN touch /etc/s6-overlay/user-bundles.d/user/contents.d/my-service
/etc/s6-overlay/s6-rc.d/user. When it exists, S6 Overlay ignores user-bundles.d/, so php-fpm and the web server never start. As www-data, the container doesn't boot at all, because S6 Overlay tries to write to /etc/s6-overlay at startup.See Using S6 Overlay for writing your own services.
FrankenPHP sets REMOTE_ADDR to the client IP
Version 4 set $_SERVER['REMOTE_ADDR'] to the TCP peer on FrankenPHP. It's now the client IP that Caddy resolves from trusted proxies, the same as NGINX and Apache. Caddy runs in strict mode, so a client behind a trusted proxy can't forge it. See Configuring trusted proxies.
Caddy limits request headers
FrankenPHP 1.13 includes Caddy 2.11.7. Requests with more than 16 KiB of headers now get 431 Request Header Fields Too Large, where v4 allowed 1 MB. Large cookies are the usual cause. Headers with a . in their name are now dropped, like headers with a _, because PHP reads both as - and a client could use them to spoof headers such as X-Forwarded-For.
num_threads no longer includes workers
If you set num_threads in FRANKENPHP_CONFIG while running workers, including Octane, FrankenPHP now starts that many threads on top of the worker threads. It fails to start if max_threads is lower than the total. Lower num_threads to the number of threads you want for regular requests.
Changed defaults
The OPcache values only apply when PHP_OPCACHE_ENABLE=1. OPcache only uses the memory as it caches files. PHP_REALPATH_CACHE_TTL applies with OPcache on or off and comes from the same Symfony recommendation.
| 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 |
PHP_SESSION_COOKIE_HTTPONLY | PHP default (off) | On |
PHP_SESSION_UPLOAD_PROGRESS_ENABLED | PHP default (on) | 0 (since v5.0.3) |
CADDY_LOG_OUTPUT (frankenphp) | stdout | stderr |
CADDY_LOG_FORMAT (frankenphp) | console | auto |
PHP_OPCACHE_FORCE_RESTART_TIMEOUT existed in v4 but never reached php.ini. It works now. Its default of 180 matches PHP's. Nothing changes unless you set it.
New variables
| Variable | What it does |
|---|---|
PHP_OPCACHE_ENABLE_CLI | Whether CLI commands use OPcache when it's on. Defaults to 1, as v4 did. |
PHP_OPCACHE_PRELOAD | Path to a preload script. Set it on the running service, not as an ENV in your Dockerfile, so build steps such as composer install don't depend on it. |
PHP_OPCACHE_PRELOAD_USER | The user to preload as when the container runs as root. |
PHP_DISABLE_FUNCTIONS | Functions to disable. Empty by default. |
PHP_FILE_UPLOADS, PHP_MAX_FILE_UPLOADS | Turn file uploads off, or cap the files per request. |
PHP_HTML_ERRORS | Format on-screen errors as HTML. |
PHP_REALPATH_CACHE_SIZE | Size of PHP's realpath cache. |
PHP_SESSION_COOKIE_HTTPONLY, PHP_SESSION_UPLOAD_PROGRESS_ENABLED | See the breaking changes above. |
TRUSTED_PROXY | Which proxies to trust for the client IP on fpm-nginx, fpm-apache, and frankenphp. |
CADDY_ACME_PROFILE | Request a Let's Encrypt certificate profile on frankenphp, such as shortlived. |
MERCURE_* | Run FrankenPHP's Mercure hub. |
AUTORUN_LARAVEL_SKIP_IF_NOT_FOUND | Start the container when Laravel isn't in APP_BASE_DIR yet. |
LARAVEL_OCTANE | Set by Octane, not by you. |
Defaults and accepted values are in the environment variable reference.
New features
- Octane with the image's Caddyfile. Octane gets the same trusted proxies, security headers, asset caching, SSL modes, and health check as classic mode. See Laravel Octane.
- Trusted proxies on every web server.
TRUSTED_PROXYbehaves the same onfpm-nginx,fpm-apache, andfrankenphp, through more than one Docker hop. See Configuring trusted proxies. - Short-lived and IP-address certificates on FrankenPHP with
CADDY_ACME_PROFILE. See Configuring SSL. healthcheck-nightwatchrunsphp artisan nightwatch:status. See Laravel health checks.- Mercure from environment variables, in classic mode or with Octane. See FrankenPHP.
- Caddyfile rules from a folder. Put a
.caddyfilein/etc/frankenphp/caddyfile-server.d/to add headers, redirects, or matchers to your site. - Redacted Mercure tokens. FrankenPHP request logs replace the
authorizationquery parameter in every log format. - Tested before publishing. Every image is started and checked on
amd64andarm64before it reaches Docker Hub. If one image fails, nothing from that build is published.