PHP Docker Images: Tiers, Not Dockerfiles
Every team builds their own PHP Docker images. Most end up with 800MB images full of build tools. Here is how I designed a set of tiers that cover almost every use case without a custom Dockerfile.
This post is from 2024, when the images still lived under the phpeek org. The tier thinking below is unchanged, but the names, the tag format and the release model have all moved on since. PHP Base Images v1 has the current picture. The text below has been updated to match what the images are today.
Every PHP team I've worked with has their own custom Docker image. A Dockerfile that started as 10 lines two years ago is now 150 lines of apt-get install, pecl install, and build flags that nobody fully understands. The image takes 12 minutes to build, weighs 800MB, and includes gcc, make, and autoconf in production because someone needed to compile the Redis extension and never added a multi-stage build.
I've seen this pattern enough times to believe there's a better way. That's why I built the Cbox PHP Base Images: opinionated images that cover the vast majority of PHP workloads without requiring a custom Dockerfile.
The Problem with Official PHP Images
The official php:8.3-fpm image from Docker Hub is intentionally minimal. It includes PHP with a handful of core extensions and nothing else. This is philosophically correct (minimal base, add what you need) but practically painful:
No common extensions. You need
pdo_mysql,redis,gd,intl,zip,bcmath, andopcachefor virtually every Laravel application. None are included.No process manager. PHP-FPM runs as PID 1, which means no signal handling, no zombie reaping, and no multi-process support.
No sensible defaults.
opcache.enableis off.memory_limitis 128MB (too low for many applications, too high for microservices).pm.max_childrenis 5 regardless of available memory.No nginx. You either run nginx in a separate container (more operational complexity) or add it yourself (more Dockerfile complexity).
The result is that every team writes their own Dockerfile layering extensions, configuration, and tooling on top of the official image. This duplicated effort is a waste of engineering time.
Two Axes, Not One
The original version of this post described three tiers where each one added both extensions and processes. That was one axis too few, and the images were later split along the seam that was already there. What you pick today is two independent choices.
The image type decides which processes run in the container:
php-fpm-nginxruns PHP-FPM, nginx and Cbox Init together. One container, complete web stack.php-fpmis a single FPM process, for when something else terminates HTTP.php-cliis for queue workers and cron jobs.nginxis standalone nginx, for the split-container setups.
The tier decides which extensions and tooling are baked in. There are four, and each builds on the previous:
Slim
The smallest practical image for PHP workloads. Around 120 MiB, with the extensions almost every application needs:
Core:
opcache,pcntl,pdo_mysql,pdo_pgsql,mysqli,pgsql,socketsData:
bcmath,intl,zip,msgpackCaching:
redis,apcuImages:
gdwith WebP,exif
OPcache is enabled with sane production defaults. memory_limit is set to 256MB. The image is based on Debian 12.
Use Slim when: you're running a microservice, a queue worker, or an API that doesn't need image processing beyond GD.
Standard
The default tier, around 250 MiB. Slim plus everything a full application tends to reach for:
Everything in Slim
Images:
imagick,vips, andgdwith AVIF supportData:
mongodbIntegration:
soap,xsl,ldap,calendar,gettext, sysv IPCTools: Node.js 22, npm, exiftool
Use Standard when: you're running a normal Laravel or Symfony application. This is the one most people want.
Chromium
Standard plus a browser, around 700 MiB. That's the entire difference.
Use Chromium when: you generate PDFs with Browsershot, run Dusk tests, or drive Puppeteer.
Dev
Chromium plus the tooling you want in development and CI and nowhere near production: Xdebug 3.5, PCOV, and the SPX profiler.
Use Dev when: you're running tests, measuring coverage, or profiling. Never in production, which is why it's a separate tier rather than a flag.
The Philosophy
Both axes follow one principle: each image should work out of the box for its target use case without any extra Dockerfile instructions.
For the standard tier of the php-fpm-nginx image, this means you should be able to write:
FROM ghcr.io/cboxdk/php-baseimages/php-fpm-nginx:8.5-bookworm-v1
COPY . /var/www/html
RUN composer install --no-dev --optimize-autoloader
That's the entire Dockerfile. Cbox Init manages PHP-FPM and nginx. OPcache is configured for production. The health check endpoint is ready. Graceful shutdown works.
If you need to customize, you extend rather than replace. Override the cbox-init.yaml, add an nginx config include, or set environment variables for PHP INI settings:
FROM ghcr.io/cboxdk/php-baseimages/php-fpm-nginx:8.5-bookworm-v1
ENV PHP_MEMORY_LIMIT=512M
ENV PHP_OPCACHE_MAX_FILES=20000
ENV PHP_FPM_MAX_CHILDREN=50
COPY nginx/custom.conf /etc/nginx/conf.d/app.conf
COPY . /var/www/html
Init Integration
The images ship with Cbox Init as their entrypoint, so you get the PID 1 benefits without any extra setup:
PHP-FPM starts first, with a health check on its listener
Nginx starts only after FPM is healthy
Cron runs as a managed child process
SIGTERM triggers graceful shutdown: FPM drains connections, nginx stops accepting, cron finishes current jobs
The default configuration is designed for a typical Laravel application. Most teams never need to modify it.
Image Sizes
Keeping images small is a constant battle. Every tier uses aggressive multi-stage builds. Extensions like gd and imagick need build dependencies (development headers, compilers) during installation but not at runtime. The build stage installs, compiles, and copies only the resulting .so files into the final image.
Slim lands around 120 MiB and Standard around 250 MiB. Compare that with the typical custom Dockerfile that ships its build tools: 300-400MB compressed, 800MB+ uncompressed. The difference matters for pull times, especially in autoscaling scenarios where new nodes need to pull images quickly.
Versioning
This is what has changed most since 2024. Images used to carry a rolling tag and a dated tag. There are three kinds now, and they mean different things on purpose:
8.5-bookwormfollows the latest release. Good for development.8.5-bookworm-v1is the release channel: rebuilt weekly with OS security patches, never crossing a tooling major. This is the recommended production pin.A digest is immutable, for when you need byte-for-byte reproducibility and want to own the update cadence yourself.
Images are published for PHP 8.2, 8.3, 8.4 and 8.5, across all four tiers, with rootless variants of each. 8.5 is the default. Every release runs through a test suite that verifies extension loading, Init process management, nginx proxying, and OPcache configuration, and production tags only move after that suite passes against the pushed image.
Full documentation, configuration options, and available extensions for each tier are in the PHP Base Images docs.
// Sylvester Damgaard