This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Docker image collection for PHP (7.4 through 8.5) optimized for Drupal, published as skilldlabs/php on Docker Hub. All images are multi-platform (linux/amd64, linux/arm64) and Alpine-based (except FrankenPHP which uses Ubuntu).
Read RELEASE_RUNBOOK.md before doing a monthly PHP patch release. It contains the release checklist and the March 11, 2026 postmortem for the CI failure that was fixed in commit 7531dc0.
# Build and push all default images (requires buildx)
make build
# Build specific tags only
make build TAGS="84 84-fpm 84-unit"
# Build Unit dev variants (adds nodejs, yarn, bash)
make unit
# Build and test locally (amd64 only, tests before push)
make test-local TAGS="85 85-fpm 85-unit"
# Setup QEMU + buildx for multi-platform builds
make prepare
# Tag a release version
make tag VER=1.2.3 TAGS="84 84-fpm"Build args COMPOSER_HASH and DRUSH_VERSION are defined in the root Makefile and passed to base image builds.
# Test all PHP 8.5 variants locally
make test-local TAGS="85 85-fpm 85-unit"
# Test individual images
./tests/run-test.sh skilldlabs/php:85 8.5 base
./tests/run-test.sh skilldlabs/php:85-fpm 8.5 fpm
./tests/run-test.sh skilldlabs/php:85-unit 8.5 unit
# Test with specific runtime (docker/podman/nerdctl)
CONTAINER_RUNTIME=podman ./tests/run-test.sh skilldlabs/php:85 8.5 baseImages are tested in CI before being pushed. The workflow:
- Builds image for
linux/amd64only (fast, with--load) - Runs appropriate test script
- If tests pass, pushes to all platforms
- Uses build cache for faster rebuilds
See .github/workflows/build-php.yml for CI implementation.
# List recent workflow runs
./gh-debug.sh list
# Watch latest run live
./gh-debug.sh watch
# Show failed runs with details
./gh-debug.sh failed
# Get logs from failed steps only
./gh-debug.sh failed-logs
# Trigger a new workflow
./gh-debug.sh trigger
# Resolve release tag from php-src + installed apk version
./release-php.sh check 85
./release-php.sh publish 85
# Full monthly release guide and postmortem
sed -n '1,260p' RELEASE_RUNBOOK.md
# Show all jobs in a run
./gh-debug.sh jobs| Runtime | Use Case | Command |
|---|---|---|
| Docker | Default, CI standard | docker run ... |
| Podman | Rootless, daemonless | CONTAINER_RUNTIME=podman |
| Nerdctl | Containerd compatible | CONTAINER_RUNTIME=nerdctl |
Base CLI (php{VER}/) → alpine:edge, built-in PHP server on :80
├── FPM (php{VER}-fpm/) → FROM skilldlabs/php:{VER}, php-fpm on :9000
├── Unit (php{VER}-unit/) → FROM skilldlabs/php:{VER}, unitd on :80
└── Unit Builder (unit-php-builder/) → compiles Nginx Unit from source
└── Dev (unit-php-builder/dev/) → adds nodejs, yarn, bash
Moodle variants (standalone, not based on skilldlabs/php):
├── php{VER}-moodle/ → FPM + PostgreSQL + moosh
└── php{VER}-moodle-unit/ → Unit + PostgreSQL + moosh + s6-overlay + cron
FrankenPHP (php83-frankenphp/, php84-frankenphp/, php85-frankenphp/)
→ Ubuntu noble, multi-stage build, Caddy webserver
Base images include: Composer 2, Drush 8, git, curl, make, mariadb-client, openssh-client, patch, rsync, sqlite, and standard PHP extensions (apcu, igbinary, xdebug, brotli, uploadprogress, gd, opcache, etc.).
docker-php/
├── php{VER}/ # Base image
├── php{VER}-fpm/ # FPM variant
├── php{VER}-unit/ # Unit variant
├── php{VER}-moodle/ # Moodle + FPM
├── php{VER}-moodle-unit/ # Moodle + Unit
├── php{VER}-frankenphp/ # FrankenPHP variant
├── unit-php-builder/dev/ # Unit dev images
├── tests/ # Test scripts
│ ├── test-lib.sh # Shared test library
│ ├── test-base.sh # Base image tests
│ ├── test-fpm.sh # FPM image tests
│ ├── test-unit.sh # Unit image tests
│ ├── install-rootless.sh # Rootless container setup
│ ├── README.md # Test documentation
│ ├── ROOTLESS.md # Rootless Docker guide
│ └── QUICKREF.md # Quick reference
├── .github/workflows/ # CI workflows
│ ├── build-php.yml # Reusable PHP build workflow
│ ├── build-{VER}.yml # Version-specific workflows
│ └── build-frankenphp.yml # FrankenPHP workflows
├── gh-debug.sh # GitHub Actions debug helper
└── Makefile # Build automation
php{VERSION}[-VARIANT]/ where VERSION is two digits (82, 83, 84, 85) and VARIANT is one of: fpm, unit, moodle, moodle-unit, frankenphp.
| Variant | Key files |
|---|---|
| Base | Dockerfile, php.ini, drush.phar |
| FPM | Dockerfile, php-fpm.conf |
| Unit | Dockerfile, conf.json |
| Moodle | Dockerfile, php.ini, xx-moodle.ini, optional cron.sh, s6 service dirs |
| FrankenPHP | Dockerfile, conf/Caddyfile, conf/php.ini, docker-php-ext-enable |
- Copy the most recent base directory (e.g.
cp -r php85 php86) - Replace all version-specific references in Dockerfile (
php85→php86) - Create corresponding
-fpmand-unitdirectories following the same pattern - Add the new tags to
TAGSin the rootMakefile - For Unit images, update
unit-php-builder/dev/MakefileTAGS - Create a new workflow file
.github/workflows/build-php86.yml(or update existing reusable workflow)
- PHP 8.3+ Dockerfiles include
apk add '!usr-merge-nag'to suppress Alpine warnings - XDebug is installed but disabled by default; enable via
-d zend_extension=xdebug.so - Composer is installed from source with SHA384 hash verification (hash in root Makefile)
- Drush is copied as a pre-downloaded
drush.pharbinary (PHP 8.2+) - FPM/Unit variants create
web-user:web-group(UID/GID 1000) - Config files are named
xx-drupal.ini(base) orxx-moodle.ini(moodle) in/etc/php{VER}/conf.d/ - Test scripts use runtime detection (docker/podman/nerdctl) via
CONTAINER_RUNTIMEenv var
-
Build Test Image (amd64 only, with cache)
- Uses GitHub Actions cache for Docker layers
- Loads image for testing with
--load
-
Run Tests
- Executes
tests/run-test.sh, which starts the container and delegates to the matching in-container test script - Tests use single container with multiple
execcalls (optimized)
- Executes
-
Push All Platforms (if tests pass)
- Builds for all platforms using cache
- Pushes to Docker Hub with version tags
- Uses
type=localcache with GitHub Actions cache - Writes to temporary directory then moves (prevents corruption)
- Shared across rebuilds for faster builds
# Using gh CLI
gh workflow run "Build PHP 8.5"
# Using debug script
./gh-debug.sh trigger
# With version tag
gh workflow run "Build PHP 8.5" -f version=8.5.3
# Resolve the version tag before dispatching
./release-php.sh publish 85Versioned Docker tags should come from the latest stable php/php-src tag and must match the installed Alpine phpXX package version in the image.
- PHP version match
- Composer installed and working
- Drush installed and working
- PHP extensions: opcache, apcu, gd, igbinary, xdebug
- Required binaries: git, curl, patch, rsync, mysql
- Built-in PHP server functionality
- PHP-FPM version and process running
- PHP CLI availability
- Log directory ownership
- Work directory exists
- web-user/web-group (UID/GID 1000)
- Configuration file present
- Port 9000 listening
- Unit version and processes (main, controller, router)
- PHP CLI availability
- Unit PHP module loaded
- Configuration file exists
- Work directory exists
- web-user/web-group (UID/GID 1000)
- Process ownership (running as web-user)
- Old pattern: One container per test (~15 starts for base image)
- New pattern: One long-running container, all tests via
exec(1 start) - Improvement: 93% reduction in container starts
- FPM and Unit jobs run in parallel (both depend on base)
- Unit-dev runs after Unit completes
- Tests run immediately after each build completes
- Uses GitHub Actions cache for Docker layers
- Cache persists between runs for faster rebuilds
- Shared across similar builds (same PHP version)
| File | Purpose |
|---|---|
.github/workflows/build-php.yml |
Reusable workflow for all PHP versions |
.github/workflows/build-{VER}.yml |
Version-specific caller workflows |
.github/workflows/build-frankenphp.yml |
FrankenPHP builds |
gh-debug.sh |
Debug helper for GitHub Actions |