gRPC in your container image#
The Temporal backend talks to the cluster over gRPC, so it needs ext-grpc — and ext-grpc ships
no prebuilt binary. install-php-extensions grpc protobuf compiles it from source: measured on this
repository, 6 min 58 s for php:8.3-cli-alpine, because grpc pulls in abseil, boringssl, re2 and
upb and compiles as C++17. Your image build pays that every time, on every branch.
So the compiled extensions are published, and you copy them into your image:
ghcr.io/gplanchat/php-grpc:8.4-cliPHP 8.2, 8.3, 8.4 and 8.5, each in four forms — cli, cli-alpine, zts, zts-alpine. Public, no
authentication needed.
The recipes below use the rolling tags, which are rebuilt every Monday so that they follow PHP’s
own patch releases and their base’s security updates. If you would rather your build not move under
you, every publication also lays a dated tag — 8.4-cli-20260828 — and pinning it is a one-word
change. A dated tag is never rebuilt.
Dated tags are pruned: the eight most recent are kept for each PHP-and-flavour pair, roughly two months of pinning points. Pin for a release you are about to cut, not for a base image you will still be building from next year — for that, the rolling tag is the one that keeps working.
Picking the tag: three things have to match#
An extension is a shared object compiled for one particular PHP. Three properties of that PHP have to line up — and they fail in three different ways, which is the part worth knowing:
| Must match | What happens if it doesn’t | When you find out |
|---|---|---|
| PHP minor version | the extension directory doesn’t exist in your base image, COPY finds nothing |
docker build, immediately |
| Thread-safety (ZTS / NTS) | same — the directory name carries it | docker build, immediately |
| libc (glibc / musl) | the path is identical, the file copies cleanly, and PHP refuses to load it | at run time, unless you check |
The patch version does not have to match: php:8.4.22 loads an extension built against 8.4.25.
The minor does — 8.3 and 8.4 are a different ABI.
The directory name encodes the first two:
| PHP | NTS | ZTS |
|---|---|---|
| 8.2 | no-debug-non-zts-20220829 |
no-debug-zts-20220829 |
| 8.3 | no-debug-non-zts-20230831 |
no-debug-zts-20230831 |
| 8.4 | no-debug-non-zts-20240924 |
no-debug-zts-20240924 |
| 8.5 | no-debug-non-zts-20250925 |
no-debug-zts-20250925 |
It does not encode libc. Debian and Alpine use the exact same path, so copying an Alpine-built
grpc.so into a Debian base succeeds without a word, and the failure surfaces much later as:
Unable to load dynamic library 'grpc.so' … Error loading shared library libstdc++.so.6That is why every recipe below ends with the same line:
RUN php -m | grep -qx grpc && php -m | grep -qx protobufIt costs nothing and it turns a production incident into a failed build.
Which tag for which base:
| Your base image | Copy from |
|---|---|
php:8.4-cli, php:8.4-fpm, php:8.4-apache |
8.4-cli |
php:8.4-cli-alpine, php:8.4-fpm-alpine |
8.4-cli-alpine |
dunglas/frankenphp:1-php8.4 |
8.4-zts |
dunglas/frankenphp:1-php8.4-alpine |
8.4-zts-alpine |
Five stacks, three recipes#
Nginx + php-fpm, Caddy + php-fpm and Apache over FastCGI (mod_proxy_fcgi) are one recipe, not
three. In all three, PHP runs in its own php:X-fpm container and the web server runs in another one that never
loads a PHP extension. Nothing in your nginx.conf, your Caddyfile or your virtual host changes.
The two setups that genuinely differ are the ones where PHP lives inside the server image:
mod_php, which is NTS, and FrankenPHP, which is ZTS.
php-fpm, behind Nginx, Caddy or Apache-over-FastCGI#
FROM ghcr.io/gplanchat/php-grpc:8.4-cli AS ext
FROM php:8.4-fpm
COPY --from=ext /usr/local/etc/php/conf.d/docker-php-ext-grpc.ini /usr/local/etc/php/conf.d/
COPY --from=ext /usr/local/etc/php/conf.d/docker-php-ext-protobuf.ini /usr/local/etc/php/conf.d/
COPY --from=ext /usr/local/lib/php/extensions/no-debug-non-zts-20240924/grpc.so /usr/local/lib/php/extensions/no-debug-non-zts-20240924/
COPY --from=ext /usr/local/lib/php/extensions/no-debug-non-zts-20240924/protobuf.so /usr/local/lib/php/extensions/no-debug-non-zts-20240924/
RUN php -m | grep -qx grpc && php -m | grep -qx protobufOn Alpine, check libstdc++. grpc is C++, and it needs that library at load time. Every Debian
base carries it; Alpine bases are inconsistent — php:8.4-fpm-alpine does not have it, the
FrankenPHP Alpine image does:
FROM ghcr.io/gplanchat/php-grpc:8.4-cli-alpine AS ext
FROM php:8.4-fpm-alpine
RUN apk add --no-cache libstdc++
COPY --from=ext /usr/local/etc/php/conf.d/docker-php-ext-grpc.ini /usr/local/etc/php/conf.d/
COPY --from=ext /usr/local/etc/php/conf.d/docker-php-ext-protobuf.ini /usr/local/etc/php/conf.d/
COPY --from=ext /usr/local/lib/php/extensions/no-debug-non-zts-20240924/grpc.so /usr/local/lib/php/extensions/no-debug-non-zts-20240924/
COPY --from=ext /usr/local/lib/php/extensions/no-debug-non-zts-20240924/protobuf.so /usr/local/lib/php/extensions/no-debug-non-zts-20240924/
RUN php -m | grep -qx grpc && php -m | grep -qx protobufWithout that apk add, the build fails on the last line with Error loading shared library libstdc++.so.6 — which is the check doing its job.
Apache with mod_php#
Here PHP is inside the web server image, and php:X-apache is NTS, like the fpm images. Same
extension directory, same source tag:
FROM ghcr.io/gplanchat/php-grpc:8.4-cli AS ext
FROM php:8.4-apache
COPY --from=ext /usr/local/etc/php/conf.d/docker-php-ext-grpc.ini /usr/local/etc/php/conf.d/
COPY --from=ext /usr/local/etc/php/conf.d/docker-php-ext-protobuf.ini /usr/local/etc/php/conf.d/
COPY --from=ext /usr/local/lib/php/extensions/no-debug-non-zts-20240924/grpc.so /usr/local/lib/php/extensions/no-debug-non-zts-20240924/
COPY --from=ext /usr/local/lib/php/extensions/no-debug-non-zts-20240924/protobuf.so /usr/local/lib/php/extensions/no-debug-non-zts-20240924/
RUN php -m | grep -qx grpc && php -m | grep -qx protobufFrankenPHP#
FrankenPHP embeds PHP in the server process and runs several workers in one process, so it is built
thread-safe. An NTS extension will not load in it — this is the case the zts images exist for.
Note the zts in the paths — no-debug-zts-20240924, not no-debug-non-zts-20240924:
FROM ghcr.io/gplanchat/php-grpc:8.4-zts AS ext
FROM dunglas/frankenphp:1-php8.4
COPY --from=ext /usr/local/etc/php/conf.d/docker-php-ext-grpc.ini /usr/local/etc/php/conf.d/
COPY --from=ext /usr/local/etc/php/conf.d/docker-php-ext-protobuf.ini /usr/local/etc/php/conf.d/
COPY --from=ext /usr/local/lib/php/extensions/no-debug-zts-20240924/grpc.so /usr/local/lib/php/extensions/no-debug-zts-20240924/
COPY --from=ext /usr/local/lib/php/extensions/no-debug-zts-20240924/protobuf.so /usr/local/lib/php/extensions/no-debug-zts-20240924/
RUN php -m | grep -qx grpc && php -m | grep -qx protobufOn dunglas/frankenphp:1-php8.4-alpine, copy from 8.4-zts-alpine instead. That one already ships
libstdc++, so it needs no apk add — Alpine bases differ on this, and the RUN php -m line is
what tells you which kind you have.
A base image that isn’t in the table#
Ask it directly, before writing any COPY:
docker run --rm --entrypoint php <your-base-image> -r \
'printf("%s %s %s\n", PHP_VERSION, PHP_ZTS ? "ZTS" : "NTS", ini_get("extension_dir"));'
docker run --rm --entrypoint sh <your-base-image> -c \
'[ -f /etc/alpine-release ] && echo musl || echo glibc'Three answers, three columns of the table above. If no published tag matches all three, compile — which is the next section.
If you would rather not copy anything#
Copying is an optimisation, not a requirement. Compiling in your own image is correct, just slow:
COPY --from=mlocati/php-extension-installer:latest /usr/bin/install-php-extensions /usr/local/bin/
RUN install-php-extensions grpc protobufThis is the right answer when your base has no matching published tag, or when a seven-minute image build is not a problem you have. It is also what builds the images above.
GitHub Actions doesn’t need any of this.
shivammathur/setup-phpinstallsgrpcfrom a prebuilt binary in about five seconds. This page is about container images, where no such binary exists.
The images and their build workflow live in docker/php-grpc/.