Upgrading

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.

compose.yml
services:
  php:
    image: serversideup/php:8.5-fpm-nginx-v4.5.3

Breaking changes

ChangeLikelihood of impactYou're affected if
OPcache no longer checks for file changesHighYou set PHP_OPCACHE_ENABLE=1 and change code without restarting the container
FrankenPHP logs go to stderr as JSONMediumYou run frankenphp and read or filter its logs
Octane uses the image's CaddyfileMediumYou run Laravel Octane on frankenphp
Mercure 1.0 rejects the old JWT optionsMediumYou run the Mercure hub on frankenphp
Session cookies are HttpOnlyLowYour JavaScript reads PHP's native session cookie
Session upload progress is offLowYour app reads upload progress from $_SESSION
S6 dependencies use dependencies.d/LowYour Dockerfile appends to an S6 dependencies file
The S6 user bundle movedLowYour Dockerfile registers your own S6 service
FrankenPHP sets REMOTE_ADDR to the client IPLowYour app compares REMOTE_ADDR to a proxy address
Caddy limits request headersLowRequests to frankenphp carry more than 16 KiB of headers
num_threads no longer includes workersLowYou 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 optimize and 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 with 2>/dev/null or a log driver rule.
  • Your log pipeline parses the console lines.
  • 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:

Dockerfile
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:

Dockerfile
RUN touch /etc/s6-overlay/user-bundles.d/user/contents.d/my-service
Don't recreate /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.

Variablev4v5
PHP_OPCACHE_VALIDATE_TIMESTAMPS10
PHP_OPCACHE_MEMORY_CONSUMPTION128256
PHP_OPCACHE_INTERNED_STRINGS_BUFFER832
PHP_OPCACHE_MAX_ACCELERATED_FILES1000032531
PHP_REALPATH_CACHE_TTL120600
PHP_SESSION_COOKIE_HTTPONLYPHP default (off)On
PHP_SESSION_UPLOAD_PROGRESS_ENABLEDPHP default (on)0 (since v5.0.3)
CADDY_LOG_OUTPUT (frankenphp)stdoutstderr
CADDY_LOG_FORMAT (frankenphp)consoleauto

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

VariableWhat it does
PHP_OPCACHE_ENABLE_CLIWhether CLI commands use OPcache when it's on. Defaults to 1, as v4 did.
PHP_OPCACHE_PRELOADPath 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_USERThe user to preload as when the container runs as root.
PHP_DISABLE_FUNCTIONSFunctions to disable. Empty by default.
PHP_FILE_UPLOADS, PHP_MAX_FILE_UPLOADSTurn file uploads off, or cap the files per request.
PHP_HTML_ERRORSFormat on-screen errors as HTML.
PHP_REALPATH_CACHE_SIZESize of PHP's realpath cache.
PHP_SESSION_COOKIE_HTTPONLY, PHP_SESSION_UPLOAD_PROGRESS_ENABLEDSee the breaking changes above.
TRUSTED_PROXYWhich proxies to trust for the client IP on fpm-nginx, fpm-apache, and frankenphp.
CADDY_ACME_PROFILERequest a Let's Encrypt certificate profile on frankenphp, such as shortlived.
MERCURE_*Run FrankenPHP's Mercure hub.
AUTORUN_LARAVEL_SKIP_IF_NOT_FOUNDStart the container when Laravel isn't in APP_BASE_DIR yet.
LARAVEL_OCTANESet 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_PROXY behaves the same on fpm-nginx, fpm-apache, and frankenphp, 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-nightwatch runs php 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 .caddyfile in /etc/frankenphp/caddyfile-server.d/ to add headers, redirects, or matchers to your site.
  • Redacted Mercure tokens. FrankenPHP request logs replace the authorization query parameter in every log format.
  • Tested before publishing. Every image is started and checked on amd64 and arm64 before it reaches Docker Hub. If one image fails, nothing from that build is published.