Skip to content
← All posts

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.

· 8 min read · Sylvester Damgaard
PHP Docker Images: Tiers, Not Dockerfiles

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, and opcache for 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.enable is off. memory_limit is 128MB (too low for many applications, too high for microservices). pm.max_children is 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-nginx runs PHP-FPM, nginx and Cbox Init together. One container, complete web stack.

  • php-fpm is a single FPM process, for when something else terminates HTTP.

  • php-cli is for queue workers and cron jobs.

  • nginx is 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, sockets

  • Data: bcmath, intl, zip, msgpack

  • Caching: redis, apcu

  • Images: gd with 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, and gd with AVIF support

  • Data: mongodb

  • Integration: soap, xsl, ldap, calendar, gettext, sysv IPC

  • Tools: 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:

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

dockerfile
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-bookworm follows the latest release. Good for development.

  • 8.5-bookworm-v1 is 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

Sylvester Damgaard
Sylvester Damgaard

The person behind Cbox. Has been writing code and running servers since 2000. Built the CMS and infrastructure behind TV2's regional news sites, co-founded a drone inspection startup, and makes open source packages for PHP teams.