Skip to content

build(php): install extensions with PIE and fail the build on missing modules - #367

Open
mrrobot47 wants to merge 4 commits into
EasyEngine:developfrom
mrrobot47:feat/php-extensions-pie
Open

mrrobot47 wants to merge 4 commits into
EasyEngine:developfrom
mrrobot47:feat/php-extensions-pie

Conversation

@mrrobot47

Copy link
Copy Markdown
Member

Summary

Install the PECL extensions of the PHP 8.1, 8.2, 8.3, 8.4 and 8.5 images (the ones CI builds and publishes) with PIE, the PHP Foundation's replacement for pecl, and make the build fail when any extension doesn't install or load, when php-fpm's config is broken, or when wp/composer don't run. The workflow now passes the job's GITHUB_TOKEN to these builds as a BuildKit secret.

This PR edits .github/workflows/test_and_publish.yml (two secrets: lines on the PHP build steps, last commit). Nothing else in CI changes.

Why

  • On 2026-10-21 pecl.php.net returned 500/502/504 errors for hours. The pecl install steps are chained with ;, so a build during the outage still "succeeds", just without memcached, redis, apcu and the rest, and PHP then logs Unable to load dynamic library on every start. CI only builds the images, so a tag run during such an outage would publish broken images.
  • PIE resolves extensions through Packagist and downloads them from GitHub, so pecl.php.net is no longer part of the build. PECL itself is deprecated in favour of PIE.
  • The 8.3 and 8.5 images built imagick from the tip of Imagick/imagick master, with no version (php --ri imagick reports @PACKAGE_VERSION@). They now get the same 3.8.1 release as the other versions.

Commits

  1. fix(php): stop installing opcache as a shared extension on 8.5. Opcache is always compiled into PHP 8.5, so docker-php-ext-install opcache has failed in every 8.5 build (cp: cannot stat 'modules/*'); the ; chain hid it. Zend OPcache is still loaded, and the current 8.5 image has no docker-php-ext-opcache.ini either, so the image doesn't change. This comes first because the next commits make that failure fatal.
  2. fix(php): fail the build when the wp-cli download fails. curl -O without -f saved an HTTP error page as wp-cli.phar; now curl -fsSLo /usr/local/bin/wp.
  3. build(php): install extensions with PIE and fail the build on errors. The main change, described below.
  4. ci: pass the GitHub token to the PHP image builds. Adds secrets: github_token=${{ secrets.GITHUB_TOKEN }} to both PHP docker/build-push-action steps (PR build and tag build).

Each commit builds on its own.

What changed

Same structure in all five Dockerfiles:

  • The extension RUN starts with set -eu, so every step in it (PIE, the source builds, docker-php-ext-install, apt-get purge) fails the build instead of being skipped.
  • PIE 1.5.2 is downloaded from its GitHub release, checked against its SHA-256, used, and removed again (with /root/.pie and its Composer cache) in the same layer.
  • Extensions are pinned to the versions the current images ship: imagick 3.8.1, memcached 3.4.0, redis 6.3.0, apcu 5.1.28, mcrypt 1.0.9.
  • timezonedb is the one unpinned extension: it only exists to ship newer time zone data than PHP bundles, so every build takes its latest release, as pecl install timezonedb did.
  • Before PIE's working directory is deleted, the build log gets one line per PIE-installed extension with the exact commit it was built from (PIE installed phpredis/phpredis 6.3.0 <sha>), so every build records what went into it.
  • PIE runs with --skip-enable-extension. Extensions are enabled exactly as before, with docker-php-ext-enable and the existing memcached.ini / gmagick.ini / imap.ini lines. conf.d is byte-identical to the current images, so anything relying on those file names keeps working.
  • imap (8.4, 8.5) and gmagick (8.1 to 8.4) are not on Packagist, so the sources of their pecl releases are built from GitHub: php/pecl-mail-imap tag 1.0.3 (with Kerberos and SSL, as the pecl prompts were answered) and vitoc/gmagick at the commit 2.0.6RC1 was released from (the repo has no tags). Both tarballs are checked against a SHA-256, and their source files are identical to the pecl .tgz releases.
  • A new RUN near the end fails the build, with a Build check failed: … line explaining why, when:
    • php -v/php -m print any startup warning, error or notice (for example Unable to load dynamic library);
    • php-fpm -t fails;
    • wp --allow-root --version or composer --version fails;
    • a module from the expected list is missing. The list is the current image's full php -m, on purpose: it also catches PHP dropping a module from core (as imap left core in 8.4), not only a failed extension install.
  • The check doesn't look for Deprecated: the images' php.ini masks E_DEPRECATED, and 8.4/8.5 already emit two masked startup deprecations (session.sid_length, session.sid_bits_per_character).
  • The check runs as root, so php-fpm -t creates /var/log/php/access.log and error.log, and on 8.1 to 8.4 the New Relic agent creates /var/log/newrelic/php_agent.log, all owned by root. The check deletes them in the same RUN: left in the image, the root-owned error.log would stop php-fpm from starting as www-data. /var/log/php and /var/log/newrelic stay empty, as in the current images.
  • CI passes its GITHUB_TOKEN as the optional github_token BuildKit secret, and the Dockerfile turns it into COMPOSER_AUTH for PIE before set -x, so it is never traced or written to a layer. Without the secret (a local docker build), PIE downloads anonymously, which also works.

Per version:

8.1 8.2 8.3 8.4 8.5
imagick pecl latest → PIE 3.8.1 pecl latest → PIE 3.8.1 git master → PIE 3.8.1 pecl latest → PIE 3.8.1 git master → PIE 3.8.1
memcached, redis, apcu, mcrypt pecl latest → PIE, pinned same same same same
timezonedb pecl latest → PIE latest same same same same
imap bundled (unchanged) bundled (unchanged) bundled (unchanged) pecl → source 1.0.3 pecl → source 1.0.3
gmagick pecl 2.0.6RC1 → source, same release same same same not shipped (unchanged)
removed – – imagick-from-git RUN commented-out imagick block imagick-from-git RUN, docker-php-ext-install opcache

Verification

  • Fork CI, https://github.com/mrrobot47/dockerfiles/actions/runs/39730135511 (all green): a copy of the PHP job's PR build step (same context, tags and secrets: github_token=${{ secrets.GITHUB_TOKEN }}, plus load, no push) for 8.1 to 8.5, then a comparison of each new image with the published easyengine/php8.X:v4.13.3 (8.1 also with v4.13.0, which core pins). For every version php -m, the full conf.d contents (file names and bodies) and the /var/log/php and /var/log/newrelic listings are identical, php -v shows no startup warnings, and php-fpm -t passes as www-data. Extension versions are identical apart from imagick on 8.3/8.5 (@PACKAGE_VERSION@ → 3.8.1); 8.1 vs v4.13.0 additionally differs only in newrelic (12.11 → 12.12) and timezonedb (2026.4 → 2026.5), both newer. A separate job in the same run proves the secrets: input reaches RUN --mount=type=secret,id=github_token. Each build log lists the PIE-installed commits, e.g. PIE installed phpredis/phpredis 6.3.0 df4fab2de7fc327c54c94a13af2b9542e4fbd720.
  • The previous run of the same workflow, https://github.com/mrrobot47/dockerfiles/actions/runs/39729111922, failed on all five versions and caught a real bug in an earlier draft: the new check runs php-fpm -t as root, which created a root-owned /var/log/php/error.log that then stopped php-fpm from starting as www-data. The check now deletes the logs it creates (see above).
  • Local --pull --no-cache builds of all five versions with the image smoke test (php -v/php-fpm -v = the FROM version, php-fpm -t, php-fpm serving the ping and a PHP page through nginx, FPM loads the same extensions as the CLI, php -m equal to v4.13.3, wp/composer work): PASS for 8.1, 8.2, 8.3, 8.4 and 8.5.
  • Local extra comparison with v4.13.3: php --ri of imagick, gmagick, imap, memcached, redis, apcu, mcrypt and timezonedb is identical (configure options such as imap Kerberos/SSL and memcached SASL/session match) apart from apcu's build date and the imagick version on 8.3/8.5; the installed Debian package list is identical (8.3/8.5: only the leftover git entry is gone); in the compared trees (/usr/local/bin, the extension directory, /usr/local/etc and /tmp) the only file difference is that pecl's /tmp/pear cache is gone.
  • Negative tests: an ini loading a missing extension fails the build with Build check failed: PHP printed these startup warnings or errors: followed by the Unable to load dynamic library line; removing an enabled extension's ini fails it with Build check failed: PHP module redis is missing; a broken php-fpm pool config fails it at php-fpm -t; a pie install of a version that doesn't exist fails the extension RUN (Could not install package: phpredis/phpredis).

Deviations from the current (v4.13.3) images

  • imagick on 8.3 and 8.5 moves from a git master build (@PACKAGE_VERSION@) to the 3.8.1 release. Master is 4 commits ahead of 3.8.1, so these two images lose: a memory leak fix in Imagick::polynomialImage() (Fix memory leak in polynomialImage() Imagick/imagick#790), error propagation from MagickEvaluateImages() (#792) and DrawSetStrokeDashArray() (#784), and a build fix for PHP 8.6 (#795). #795 only matters for PHP 8.6; the leak and the two swallowed errors come back on 8.3/8.5 until imagick tags a release with those fixes. A pinned release is still preferable to an unversioned master build that changes on every rebuild.
  • pecl's /tmp/pear download cache is no longer left in the images, and 8.3/8.5 lose the leftover git dpkg entry (removed package, config only).
  • pecl list inside a container no longer lists the extensions, since pecl didn't install them.

What stays on pecl, and why

  • php/5.6, 7.0, 7.2, 7.3, 7.4, 8.0 and php/stable still use pecl install. CI doesn't build them (they aren't in the php matrix), so their published images don't change, and PIE needs PHP 8.1 or later anyway.
  • Every image CI builds (8.1 to 8.5) is off pecl.

Risks and rollback

  • New build-time dependencies: Packagist metadata, GitHub release/archive downloads, PIE 1.5.2. pecl.php.net is no longer used. A GitHub or Packagist outage now fails the build loudly instead of producing a half-built image.
  • The extension versions are pinned and Dependabot only tracks FROM lines, so new imagick/memcached/redis/apcu/mcrypt releases need a manual bump (previously every build took whatever pecl served). mcrypt 1.0.9 declares PHP ≤ 8.6.0, so a future 8.6 image will need a newer mcrypt; PIE enforces that and fails loudly.
  • GitHub /archive/ tarballs (imap, gmagick) have changed bytes before. If that happens, the SHA-256 check fails the build and the pin needs updating; it can't produce a different image silently.
  • PIE prints The following build tools are missing: libtoolize on every install. It's harmless (phpize ships its own libtool scripts, and all extensions build and load); libtool isn't installed to keep the image contents unchanged.
  • The RUN --mount=type=secret line needs BuildKit, which CI (buildx) and Docker 23+ use by default; only a build forced onto the legacy builder (DOCKER_BUILDKIT=0) fails.
  • If the token caused trouble, the PIE step would fail with Could not authenticate against github.com; removing the two secrets: lines brings back anonymous downloads, which work too.
  • Rollback: revert this PR; the images are rebuilt from the previous pecl install steps. Already published tags are unaffected.

Opcache is always compiled into PHP 8.5, so docker-php-ext-install opcache fails (cp: cannot stat 'modules/*'). The ;-chained RUN hid the failure, and the image still loads Zend OPcache, so dropping the step changes nothing in the image.
curl -O without -f saved an HTTP error page as wp-cli.phar, so a failed download produced an image with a broken wp.
The pecl install steps were chained with ;, so when pecl.php.net was down a build still succeeded, just without memcached, redis, apcu and the rest, and PHP then warned about the missing libraries on every start.

For the 8.1 to 8.5 images (the ones CI builds and publishes):
- Install imagick, memcached, redis, apcu, mcrypt and timezonedb with PIE 1.5.2 (checked against its SHA-256) from Packagist/GitHub. Versions are pinned to what the current images ship; timezonedb stays unpinned because it only exists to ship newer tz data than PHP bundles. The resolved commits are printed to the build log.
- Build imap (8.4, 8.5) and gmagick (8.1 to 8.4), which aren't on Packagist, from SHA-256-pinned GitHub tarballs of their pecl releases.
- Run the extension step under set -eu so any failing step fails the build.
- Add a final check that fails the build on PHP startup warnings or errors, a failing php-fpm -t, wp or composer, or a module missing from the expected list.
- 8.3 and 8.5 now get the imagick 3.8.1 release instead of an unversioned git master build.

Extensions are still enabled with docker-php-ext-enable and the existing ini lines, so conf.d is unchanged.
The PHP Dockerfiles read an optional github_token BuildKit secret and hand it to PIE, so its GitHub downloads are authenticated instead of anonymous.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

No unresolved issues were identified, and the linked verification workflow completed successfully.

No open issues 🎉

What changed in this PR

This PR moves the five CI-published PHP images off PECL and adds build-time checks to prevent publishing images with missing extensions or broken tooling.

Changes:

  • Uses checksum-verified PIE and source archives, pins most extension versions, and makes installation failures fatal.
  • Adds checks for PHP startup diagnostics, expected modules, PHP-FPM configuration, WP-CLI, and Composer, then removes logs created by validation.
  • Passes the GitHub token through BuildKit secrets for extension downloads.
File Description
php/​8.5/​Dockerfile Migrates extensions, removes the shared OPcache build, and adds validation.
php/​8.4/​Dockerfile Migrates extensions, builds IMAP and gmagick from verified sources, and adds validation.
php/​8.3/​Dockerfile Replaces Imagick master with a pinned release, migrates other extensions, and adds validation.
php/​8.2/​Dockerfile Migrates extensions to PIE and verified sources and adds validation.
php/​8.1/​Dockerfile Migrates extensions to PIE and verified sources and adds validation.
.github/​workflows/​test_and_publish.yml Supplies the optional BuildKit secret to both PHP build steps.

Review effort: Balanced

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants