Every Laravel Dockerfile I inherit has the same block in it: pecl install redis, docker-php-ext-enable redis, and a pile of build dependencies installed and purged in the same RUN to keep the layer small. It works until a rebuild picks a newer extension version and the image stops booting. PECL is now deprecated, and PIE — the PHP Foundation's official replacement — fixes exactly the thing that keeps biting: version pinning.
Know what PIE replaces and what it does not#
PIE is the official installer for PHP extensions and replaces PECL, which the PHP project now marks as deprecated. It ships as a PHAR, behaves like Composer, and resolves extensions from Packagist using the php-ext and php-ext-zend package types — so an extension is a package name with a version constraint, not a channel and a guess. It installs modules into a PHP installation rather than pulling libraries into your project.
PECL still works today. This is a migration, not an emergency. What PIE does not do is replace the extensions bundled with PHP: pdo_mysql, mbstring, intl and friends still come from docker-php-ext-install or your distribution. The rule of thumb is simple — if you previously had to compile it yourself, PIE owns it now.
# The old way, and the reason rebuilds break
pecl install redis && docker-php-ext-enable redis
Install PIE and verify the PHAR#
Download pie.phar from the latest release, verify its provenance with the GitHub CLI, and drop it on your $PATH. The attestation check is not optional ceremony — you are about to compile and load native code into every PHP process you run, so treat it with the same suspicion you apply when you audit PHP dependencies.
curl -fL --output /tmp/pie.phar \
https://github.com/php/pie/releases/latest/download/pie.phar \
&& gh attestation verify --owner php /tmp/pie.phar \
&& sudo mv /tmp/pie.phar /usr/local/bin/pie \
&& sudo chmod +x /usr/local/bin/pie
PIE itself needs PHP 8.1 or newer to run, though it can install an extension into any PHP version on the machine. Homebrew (brew install pie) and EPEL (sudo dnf install pie) both package it, but distribution packages lag the release and may carry patches — verify before you trust them.
Install an extension and read where it landed#
Use the Composer package name, not the old PECL short name. The full directory of PIE-compatible packages lives at packagist.org/extensions. The output is the useful part: PIE tells you which PHP it targeted, which php-config it configured against, where the .so was written and whether the extension was actually enabled.
$ pie install phpredis/phpredis
🥧 PHP Installer for Extensions (PIE) 1.5.0, from The PHP Foundation
You are running PHP 8.4.14
Target PHP installation: 8.4.14 nts, on Linux/OSX/etc x86_64 (from /usr/bin/php8.4)
Found package: phpredis/phpredis:6.3.0 which provides ext-redis
phpize complete.
Configure complete with options: --with-php-config=/usr/bin/php-config8.4
Build complete: /tmp/pie/phpredis/modules/redis.so
Install complete: /usr/lib/php/20240924/redis.so
✅ Extension is enabled and loaded in /usr/bin/php8.4
Enabling is handled for you: PIE uses phpenmod on deb.sury.org builds, docker-php-ext-enable inside the official Docker PHP images, or writes to the additional .ini directory. Pass --skip-enable-extension if you want to place the .ini yourself, and PIE will warn rather than silently leave the extension dark.
Point PIE at the right PHP build#
This is the failure mode PECL never solved. On a machine running Herd, or any box with several distro PHPs, pecl install would report success and write into a PHP you are not running. PIE makes the target explicit with --with-php-config on Linux and macOS, --with-php-path on Windows, and --with-phpize-path when the matching phpize is not where PIE expects it.
# Install into PHP 8.3 while running PIE under 8.4
pie install --with-php-config=/usr/bin/php-config8.3 phpredis/phpredis
# Windows: point at the php.exe you actually want
php.exe pie.phar install --with-php-path=C:\php-8.4.14\php.exe xdebug/xdebug
On Windows PIE downloads a precompiled DLL rather than building, so no toolchain is needed at all. On Linux and macOS, PIE 1.4+ detects missing build tools and offers to install them — and if an extension publishes a prepackaged binary, PIE will use it. Note the trade-off: specifying any configure option makes PIE fall back to compiling from source, which is fine but a lot slower in a build.
Pin the version so the build is reproducible#
Give pie install a Composer-style constraint and PIE records it in a pie.json scoped to the target PHP installation. That file plus its pie.lock is the reproducibility story — pecl install redis gave you whatever was newest that morning, which is the whole reason rebuilds broke.
pie install "phpredis/phpredis:^6.3"
{
"require": {
"phpredis/phpredis": "^6.3"
}
}
Find the path for a given PHP with pie show -v, which prints a Using pie.json: line. Commit the pie.json and pie.lock into your repository, copy them into that path during the build, and run pie install --from-lock so CI, your laptop and production all land on the same extension build. It is the same discipline that makes caching Composer and npm in a Laravel CI pipeline worth the effort.
Install project extensions non-interactively in CI#
Run pie install with no arguments inside a project and PIE reads the ext-* entries from composer.json, reports which are missing, and offers to install them. That prompt is a problem in a container build or a CI job. From PIE 1.5 the old --allow-non-interactive-project-install flag no longer works — you must name the package for each extension with --select.
pie install --no-dev \
--select redis=phpredis/phpredis \
--select igbinary=igbinary/igbinary
--no-dev skips extensions declared only in require-dev, which is how you keep Xdebug out of a production image. Add --auto-install-build-tools and --auto-install-system-dependencies if you want PIE to resolve missing toolchain pieces without a prompt, or --no-build-tools-check to skip the check entirely.
Wire PIE into a multi-stage Laravel Dockerfile#
PIE belongs in the builder stage with the compiler, never in the runtime image. The cleanest way to get the binary in is a bind mount from the published binary-only image — it adds nothing to your layers and carries no PHP runtime of its own. The surrounding image structure is covered in detail in optimising Laravel Docker images with multi-stage builds; this is just the extension slice of it.
FROM php:8.4-cli AS extensions
RUN --mount=type=bind,from=ghcr.io/php/pie:bin,source=/pie,target=/usr/local/bin/pie \
set -eux; \
export DEBIAN_FRONTEND=noninteractive; \
apt-get update; \
# PIE needs unzip (or the zip extension, or git) to extract packages
apt-get install -y --no-install-recommends unzip; \
pie install --no-cache "phpredis/phpredis:^6.3"; \
apt-get purge -y --auto-remove unzip; \
rm -rf /var/lib/apt/lists/*
FROM php:8.4-fpm AS runtime
# Carry the built module and its ini forward — leave gcc behind
COPY --from=extensions --chown=root:root \
/usr/local/lib/php/extensions/no-debug-non-zts-20240924/redis.so \
/usr/local/lib/php/extensions/no-debug-non-zts-20240924/redis.so
COPY --from=extensions --chown=root:root \
/usr/local/etc/php/conf.d/docker-php-ext-redis.ini \
/usr/local/etc/php/conf.d/docker-php-ext-redis.ini
Pin the bin tag if you want the PIE version fixed too — ghcr.io/php/pie:1.5-bin and 1.5.0-bin are both published alongside the rolling bin tag.
Verify the extension in the final image#
Never trust a build that did not assert anything. Add a verification step that runs against the runtime stage, not the builder, so a COPY that silently missed a path fails the build rather than production.
$ php -m | grep redis
redis
$ php --ri redis | head -4
redis
Redis Support => enabled
Redis Version => 6.3.0
Available serializers => php, json, igbinary
That Available serializers line is the one to watch. If igbinary is missing from it, the extensions loaded in the wrong order — covered next.
Fix the ini load order before trusting the serializer#
PIE writes each extension its own .ini file and does not reorder them for you. igbinary must be loaded before redis, or Redis::SERIALIZER_IGBINARY is unavailable and your cache quietly falls back to PHP serialisation. The conventional fix is numeric prefixes on the conf.d filenames.
/usr/local/etc/php/conf.d/10-docker-php-ext-igbinary.ini
/usr/local/etc/php/conf.d/20-docker-php-ext-redis.ini
Worth knowing before you reach for igbinary: at the time of writing, the only release igbinary/igbinary has published for PIE is 3.2.17RC1, so you need an explicit @RC stability flag or you keep using the distribution package. Serializer choice matters more than it looks once Redis is carrying your sessions and cache — the same trade-off comes up when migrating from Redis to Valkey.
Avoid the traps that are not in the docs#
A handful of things will cost you an afternoon if nobody warns you. The pie-extensions/* packages that still rank well in search — pie-extensions/redis, pie-extensions/igbinary — are abandoned mirrors. Use the upstream names: phpredis/phpredis, igbinary/igbinary, xdebug/xdebug.
On Alpine you need the musl-compatible toolchain (apk add build-base autoconf), and some extensions simply will not build there. When you hit one, move that image to a Debian slim base rather than fighting it — the size difference is smaller than the hours.
There is also no pie uninstall. The PECL comparison table in the PIE docs leaves that row blank, so removing an extension is still a matter of deleting the .so and its .ini by hand. And from PIE 1.5, installing several extensions in one command fails if they share configure options, so keep them as separate pie install invocations in a Dockerfile where that applies.
Start with one extension on one image — phpredis is the usual first candidate — commit the pie.json and pie.lock, and confirm php --ri reports the version you pinned. Once extensions are reproducible, the next wins in the same image are OPcache preloading and production tuning and pruning the Composer packages the app never uses.
FAQ#
What is PIE and how is it different from pecl?
PIE is the PHP Foundation's official installer for PHP extensions and the designated replacement for PECL, which is now deprecated. The practical difference is resolution: PECL used its own channel system and installed whatever was newest, while PIE resolves extensions from Packagist as Composer packages with the php-ext type, so you can express a version constraint and record it. PIE also makes the target PHP installation explicit rather than guessing.
How do I install a PHP extension with PIE?
Download pie.phar from the php/pie releases page, verify it with gh attestation verify --owner php pie.phar, move it onto your $PATH, then run pie install vendor/package using the Composer package name — for example pie install phpredis/phpredis. Add a constraint such as :^6.3 to pin the version. PIE builds the extension, installs the .so into the correct extension directory and enables it in the appropriate ini file.
Can I use PIE in a Docker image for Laravel?
Yes, and the binary-only image makes it clean. Bind-mount ghcr.io/php/pie:bin into a RUN instruction in your builder stage, install the extensions there, then copy the compiled .so files and their .ini files into the runtime stage so the final image ships no compiler. PIE needs unzip, the zip extension or git available to extract packages, so install and purge that in the same layer.
Does PIE work with multiple PHP versions on one machine?
That is one of its main improvements over PECL. PIE needs PHP 8.1 or newer to run itself but can install into any PHP version present, and you choose the target with --with-php-config=/usr/bin/php-config8.3 on Linux and macOS or --with-php-path on Windows. If the matching phpize is not where PIE expects, point at it with --with-phpize-path. Each PHP installation also gets its own pie.json.
Is pecl deprecated in 2026?
Yes. The php/pie project states plainly that PIE is the official installer for PHP extensions and replaces PECL, which is now deprecated. PECL has not been switched off and existing pecl install lines still run, so there is no emergency — but new extension tooling, documentation and packaging effort is going into PIE, and the official PHP Docker image guidance is moving with it.
How do I pin a PHP extension version in a reproducible build?
Pass a Composer-style constraint to pie install, for example pie install "phpredis/phpredis:^6.3". PIE writes that constraint into a pie.json scoped to the target PHP installation, and produces a pie.lock alongside it. Run pie show -v to find the directory holding those files, commit both to your repository, copy them into place during the build, and run pie install --from-lock so every environment resolves to the same extension version.