Skip to content

Custom Extensions

Custom PHP Extensions

Cbox includes 40+ extensions, but you may need others. This guide covers adding extensions via PECL, compiling from source, and version management.

Quick Reference

# PECL extension (most common)
RUN pecl install extension-name && docker-php-ext-enable extension-name

# Core extension (bundled with PHP)
RUN docker-php-ext-install extension-name

# Extension with dependencies
RUN apt-get update && apt-get install -y dependency-package \
    && pecl install extension-name \
    && docker-php-ext-enable extension-name

PECL Extensions

Basic Installation

FROM ghcr.io/cboxdk/php-baseimages/php-fpm-nginx:8.5-bookworm-v1

# Install build dependencies, extension, enable, cleanup
RUN apt-get update && apt-get install -y $PHPIZE_DEPS \
    && pecl install swoole \
    && docker-php-ext-enable swoole \
    && apt-get purge -y --auto-remove $PHPIZE_DEPS \
    && rm -rf /var/lib/apt/lists/*

With Version Pinning

FROM ghcr.io/cboxdk/php-baseimages/php-fpm-nginx:8.5-bookworm-v1

# Pin specific version for reproducibility
RUN apt-get update && apt-get install -y $PHPIZE_DEPS \
    && pecl install swoole-5.1.1 \
    && docker-php-ext-enable swoole \
    && apt-get purge -y --auto-remove $PHPIZE_DEPS \
    && rm -rf /var/lib/apt/lists/*

Common PECL Extensions

Swoole (Async/Coroutines)

RUN apt-get update && apt-get install -y $PHPIZE_DEPS libssl-dev libcurl4-openssl-dev \
    && pecl install swoole \
    && docker-php-ext-enable swoole \
    && apt-get purge -y --auto-remove $PHPIZE_DEPS \
    && rm -rf /var/lib/apt/lists/*

gRPC + Protobuf

FROM ghcr.io/cboxdk/php-baseimages/php-fpm-nginx:8.5-bookworm-v1

RUN apt-get update && apt-get install -y \
    libgrpc-dev libprotobuf-dev protobuf-compiler \
    && pecl install grpc protobuf \
    && docker-php-ext-enable grpc protobuf \
    && rm -rf /var/lib/apt/lists/*

Memcached

RUN apt-get update && apt-get install -y $PHPIZE_DEPS libmemcached-dev zlib1g-dev \
    && pecl install memcached \
    && docker-php-ext-enable memcached \
    && apt-get purge -y --auto-remove $PHPIZE_DEPS \
    && rm -rf /var/lib/apt/lists/*

SSH2

RUN apt-get update && apt-get install -y $PHPIZE_DEPS libssh2-1-dev \
    && pecl install ssh2 \
    && docker-php-ext-enable ssh2 \
    && apt-get purge -y --auto-remove $PHPIZE_DEPS \
    && rm -rf /var/lib/apt/lists/*

AMQP (RabbitMQ)

RUN apt-get update && apt-get install -y $PHPIZE_DEPS librabbitmq-dev \
    && pecl install amqp \
    && docker-php-ext-enable amqp \
    && apt-get purge -y --auto-remove $PHPIZE_DEPS \
    && rm -rf /var/lib/apt/lists/*

Event (libevent)

RUN apt-get update && apt-get install -y $PHPIZE_DEPS libevent-dev libssl-dev \
    && pecl install event \
    && docker-php-ext-enable event \
    && apt-get purge -y --auto-remove $PHPIZE_DEPS \
    && rm -rf /var/lib/apt/lists/*

Core Extensions

Extensions bundled with PHP source (use docker-php-ext-install):

# Single extension
RUN docker-php-ext-install sockets

# Multiple extensions
RUN docker-php-ext-install pdo_mysql mysqli

# Extension requiring configuration
RUN docker-php-ext-configure gd --with-freetype --with-jpeg \
    && docker-php-ext-install gd

Available Core Extensions

Already included in Cbox:

  • bcmath, calendar, exif, gettext, intl, opcache, pcntl, pdo_mysql, pdo_pgsql, sockets, zip

Can be added if needed:

  • dba, enchant, ffi, ftp, oci8, odbc, pdo_odbc, pspell, shmop, snmp, sysvmsg, sysvsem, sysvshm, tidy

Compiling from Source

For extensions not on PECL or needing custom options:

Example: OpenSwoole with Custom Options

FROM ghcr.io/cboxdk/php-baseimages/php-fpm-nginx:8.5-bookworm-v1

RUN apt-get update && apt-get install -y $PHPIZE_DEPS git libssl-dev libcurl4-openssl-dev \
    && git clone https://github.com/openswoole/swoole-src.git \
    && cd swoole-src \
    && phpize \
    && ./configure \
        --enable-openssl \
        --enable-http2 \
        --enable-mysqlnd \
        --enable-sockets \
    && make -j$(nproc) \
    && make install \
    && docker-php-ext-enable openswoole \
    && cd .. && rm -rf swoole-src \
    && apt-get purge -y --auto-remove $PHPIZE_DEPS git \
    && rm -rf /var/lib/apt/lists/*

Example: Xhprof (Facebook's Profiler)

RUN apt-get update && apt-get install -y $PHPIZE_DEPS git \
    && git clone https://github.com/longxinH/xhprof.git \
    && cd xhprof/extension \
    && phpize \
    && ./configure \
    && make \
    && make install \
    && docker-php-ext-enable xhprof \
    && cd ../.. && rm -rf xhprof \
    && apt-get purge -y --auto-remove $PHPIZE_DEPS git \
    && rm -rf /var/lib/apt/lists/*

Extension Configuration

php.ini Settings

# Create custom ini file
RUN echo "extension_setting=value" > /usr/local/etc/php/conf.d/99-extension.ini

Example: Swoole Configuration

RUN echo "swoole.use_shortname=Off" > /usr/local/etc/php/conf.d/99-swoole.ini

Example: OPcache for Production

RUN echo "opcache.enable=1" > /usr/local/etc/php/conf.d/99-opcache.ini \
    && echo "opcache.memory_consumption=256" >> /usr/local/etc/php/conf.d/99-opcache.ini \
    && echo "opcache.validate_timestamps=0" >> /usr/local/etc/php/conf.d/99-opcache.ini

Using FFI

FFI is compiled into the standard, chromium and dev tiers (not slim) and shipped with ffi.enable=preload. That is upstream's own default and the right one: FFI loads arbitrary shared libraries and calls into them, so an image that enabled it for request code would hand every request in every application a way out of PHP.

Under FPM that means request code cannot call FFI::cdef(). Preloaded code can, and PHP_OPCACHE_PRELOAD is how you get a preload script.

The pattern that works

Preloading persists declarations, not runtime state - a static property assigned during preload is empty again by the time a request runs. Use FFI::load() on a header with an FFI_SCOPE, then look the scope up per request:

/* /opt/ffi/libc.h */
#define FFI_SCOPE "LIBC"
#define FFI_LIB "libc.so.6"

long time(long *t);
int getpid(void);
<?php // /opt/ffi/preload.php - runs once at startup
FFI::load(__DIR__ . '/libc.h');
<?php // request code - no cdef() anywhere
$libc = FFI::scope('LIBC');
echo $libc->getpid();
services:
  app:
    image: ghcr.io/cboxdk/php-baseimages/php-fpm-nginx:8.5-bookworm
    environment:
      PHP_OPCACHE_PRELOAD: /opt/ffi/preload.php
    volumes:
      - ./ffi:/opt/ffi:ro

Verified on php-cli:8.5-bookworm: without the preload, FFI::scope('LIBC') fails with Failed loading scope 'LIBC'; with it, the call returns.

Two things that will bite you

A static property set in the preload script is not there at request time. FFI::cdef() assigned to Libc::$ffi during preload looks like it should work and fails with "Typed static property must not be accessed before initialization". Scopes survive; values do not.

The CLI is not gated. ffi.enable=preload still allows FFI::cdef() in the CLI SAPI - that is PHP's behaviour, not ours. The restriction protects web requests, so a CLI script that works proves nothing about what FPM will allow.

A preload path that does not exist fails the container at boot with a message naming the path, rather than PHP's startup fatal that names only the ini setting. That is deliberate - see PHP_OPCACHE_PRELOAD.

Version Pinning Best Practices

Pin Versions in Production

# Good: Pin specific versions
RUN pecl install redis-6.0.2 \
    && pecl install msgpack-3.0.0

# Bad: Use latest (unpredictable)
RUN pecl install redis msgpack

Use Build Arguments

ARG REDIS_VERSION=6.0.2
ARG SWOOLE_VERSION=5.1.1

RUN pecl install redis-${REDIS_VERSION} \
    && pecl install swoole-${SWOOLE_VERSION}

Override at Build Time

docker build \
  --build-arg REDIS_VERSION=6.1.0 \
  --build-arg SWOOLE_VERSION=5.2.0 \
  -t myapp .

Testing Extensions

Verify Installation

# Check extension loaded
docker run --rm myapp php -m | grep swoole

# Check extension version
docker run --rm myapp php -r "echo phpversion('swoole');"

# Check extension config
docker run --rm myapp php -i | grep swoole

Automated Testing

# Add health check for extension
HEALTHCHECK --interval=30s --timeout=3s \
    CMD php -r "if (!extension_loaded('swoole')) exit(1);" || exit 1

Troubleshooting

Extension Won't Compile

# Check build logs
docker build --progress=plain -t test .

# Interactive debugging
docker run --rm -it ghcr.io/cboxdk/php-baseimages/php-fpm-nginx:8.5-bookworm-v1 sh
apt-get update && apt-get install -y build-essential
pecl install extension-name

Missing Dependencies

# Search for Debian packages
apt-cache search libname

# Or use apt-file to find which package provides a file
apt-file search library-name

Common Dependency Mappings (Debian)

Extension Debian Package
gd libfreetype6-dev libpng-dev
imagick libmagickwand-dev
memcached libmemcached-dev
mongodb libssl-dev
ssh2 libssh2-1-dev
amqp librabbitmq-dev
event libevent-dev
curl libcurl4-openssl-dev
zip libzip-dev

Extension Conflicts

# Load order matters for some extensions
# msgpack should load before redis (for serialization)
RUN pecl install msgpack \
    && docker-php-ext-enable msgpack \
    && pecl install --configureoptions 'enable-redis-msgpack="yes"' redis \
    && docker-php-ext-enable redis

Complete Examples

Laravel with Swoole + Redis

FROM ghcr.io/cboxdk/php-baseimages/php-fpm-nginx:8.5-bookworm-v1

# Build dependencies
RUN apt-get update && apt-get install -y $PHPIZE_DEPS libssl-dev libcurl4-openssl-dev \
    && pecl install swoole-5.1.1 \
    && docker-php-ext-enable swoole \
    && echo "swoole.use_shortname=Off" > /usr/local/etc/php/conf.d/99-swoole.ini \
    && apt-get purge -y --auto-remove $PHPIZE_DEPS \
    && rm -rf /var/lib/apt/lists/*

COPY . /var/www/html
RUN composer install --no-dev --optimize-autoloader

API with gRPC + Protobuf

FROM ghcr.io/cboxdk/php-baseimages/php-fpm-nginx:8.5-bookworm-v1

RUN apt-get update && apt-get install -y \
    libgrpc-dev libprotobuf-dev protobuf-compiler \
    && pecl install grpc-1.60.0 protobuf-3.25.1 \
    && docker-php-ext-enable grpc protobuf \
    && rm -rf /var/lib/apt/lists/*

COPY . /var/www/html
RUN composer install --no-dev --optimize-autoloader

Next Steps