v5.0.0-beta1
Pre-release🤩 What's new
Short-lived & IP address certificates for FrankenPHP
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.
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.
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 |
⚠️ 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.
- Full detail on what changed and why (#699) →
- EOL versions and the legacy-modernization path →
- Choosing an image →
OPcache no longer checks for file changes (#704)
PHP_OPCACHE_VALIDATE_TIMESTAMPS now defaults to 0, so with OPcache enabled, PHP files are cached until the container restarts. You are affected if you set PHP_OPCACHE_ENABLE=1 and mount your code as a volume, update WordPress on a volume outside the admin, or run docker exec ... artisan optimize against a live container. Restart the container after code changes, or set PHP_OPCACHE_VALIDATE_TIMESTAMPS=1 to keep the v4 behavior. Nothing changes with PHP_OPCACHE_ENABLE=0.
FrankenPHP logs go to stderr and default to JSON (#604)
| Variable | v4 | v5 |
|---|---|---|
CADDY_LOG_OUTPUT |
stdout |
stderr |
CADDY_LOG_FORMAT |
console |
auto (Caddy's default: console on a terminal, json otherwise) |
docker logs, Compose, and Kubernetes capture both streams, so most setups only notice the format. You are affected if you read the two streams separately or parse the console lines. Set CADDY_LOG_FORMAT=console and CADDY_LOG_OUTPUT=stdout to get the v4 behavior back, unless you run Octane, which needs the new defaults. These are Caddy's own defaults. Fixing both in the same major release means one upgrade instead of two.
PHP_SESSION_COOKIE_HTTPONLY now defaults to On (#692)
PHP recommends session.cookie_httponly=On for production, so the images now ship it that way. Laravel, Symfony, and WordPress manage their own session cookies and are not affected. If your app calls session_start() directly and reads the session cookie from JavaScript, set PHP_SESSION_COOKIE_HTTPONLY=Off to keep the previous behavior.
S6 dependency files moved to dependencies.d/ (#689)
If your Dockerfile appends lines to /etc/s6-overlay/s6-rc.d/<service>/dependencies for php-fpm, nginx, or apache2, S6 now ignores that file. Create an empty file in dependencies.d/ instead:
RUN touch /etc/s6-overlay/s6-rc.d/nginx/dependencies.d/my-serviceStock images and rootless containers are not affected.
Laravel Octane: remove the old workaround (#604)
If you combined a worker { } block in FRANKENPHP_CONFIG with --caddyfile, FrankenPHP now fails with "global workers must not have duplicate filenames". Remove the block and any CADDY_PHP_SERVER_OPTIONS you added for Octane. CADDY_ADMIN and CADDY_GLOBAL_OPTIONS are also ignored when Octane starts FrankenPHP. Use /etc/frankenphp/caddyfile-global.d/ for global options.
🔐 Security
- Blocked a
PATH_INFObypass of the/storagePHP rule in Apache, NGINX, and FrankenPHP./storage/evil.phpwas denied, but/storage/evil.php/anythingstill ran (#691) - FrankenPHP runs with
trusted_proxies_strict, so a client behind a trusted proxy can't forge its IP (#703) - FrankenPHP request logs redact the
authorizationquery parameter that Mercure subscribers pass in the URL (#604) - Native PHP session cookies are
HttpOnlyby default (#692)
👨💻 DX improvements
- Quieter one-off commands like
composer install. The SSL script only runs when the container is starting its web server (#671) - The startup banner shows whether Laravel automations are on and warns when OPcache needs a restart to pick up code changes (#704)
LOG_OUTPUT_LEVELdocs list all eight valid values.offwas removed because the container never accepted it. UseSHOW_WELCOME_MESSAGE=falseto hide the banner (#671)- Caddy no longer logs "No files matching import glob pattern" on every FrankenPHP start (#604)
- Two new build commands for your own Dockerfiles:
docker-php-serversideup-downloadretries downloads, anddocker-php-serversideup-install-php-extensionslets you swap an extension's source with thePHP_EXTENSION_OVERRIDESbuild arg (#699, #700) - New guides for trusted proxies, production performance tuning, logging, and Nightwatch, plus a rewritten Octane guide
🐛 Bug fixes
All variations
- Migration isolation no longer fails the container. Laravel older than 9.38.0 continues without
--isolated, and first deployments skip it until the migrations table exists (#628, fixes #627) - Added the
DBfacade import forLARAVEL_AUTORUNon modern Laravel versions (#673, fixes #672) PHP_OPCACHE_FORCE_RESTART_TIMEOUTnever reachedphp.ini(#704)
All web servers (FrankenPHP, NGINX, Apache)
- Fixed a bug where duplicate SEO content was possible.
/index.php/pathnow redirects to/path(#646) - Don't generate SSL if
DISABLE_DEFAULT_CONFIGis true (#644)
NGINX and Apache
- Fixed a startup race when running as root where
php-fpmcould start before its pool user was set (#689) - Fixed
fpm-nginxandfpm-apachefailing to start with s6-overlay 3.2.3. This only affected thev4.6.0betas (#700) docker-php-serversideup-s6-initnow works when a base image runs it and an image built on that base runs it again. Scripts you add in the second image are ordered with the ones converted earlier, and a script with the same name replaces the earlier one instead of running twice, once through the entrypoint and once through S6. This matches what aCOPYinto/etc/entrypoint.ddoes without the command
Apache
FrankenPHP
- Fixed the
FRANKENPHP_CONFIGtypo in the image's declared environment (#702) REMOTE_ADDRwas the TCP peer instead of the client IP resolved from trusted proxies (fixes #703)- FrankenPHP's default command wasn't recognized by
SERVERSIDEUP_DEFAULT_COMMAND(#671) LOG_OUTPUT_LEVEL=debugdidn't apply to the per-site Caddy log (#671)SSL_MODE=fullnow works when Octane is the container command (#604)
⏫ Dependency updates
- PHP extension installer 2.9.27 → 2.11.27
- S6 Overlay v3.2.1.0 → v3.2.3.2
- FrankenPHP 1.12.6 → 1.12.7, with Caddy plugins pinned to match (caddy-cbrotli v1.0.1, Mercure v0.24.2, Vulcain v1.4.2)
- NGINX is now running 1.30.5
🙏 Thanks
Thanks to @aSeriousDeveloper, @Abdulmajeed-Jamaan, @agabi10, @devhammed, @LorenzoRogai, @victorlap, @5ergiu, and @arnaud-ritti for the PRs, and to @mbrodala, @kohenkatz, @QarthO, @dbpolito, @tomschlick, @sertxudev, and @andreasdorfer for the reviews and testing that went into this release.