Воодушевленный созданием контейнера PHP для FreeBSD, я поставил перед собой более амбициозную цель: скомпилировать FrankenPHP. Напомню, что это веб-сервер на базе Caddy, который взаимодействует с PHP как с библиотекой и помимо этого предоставляет еще ряд различных оптимизаций. В целом все получилось, но, как говорится, есть нюансы.

FrankenPHP в пакетах FreeBSD, скорее всего, невозможен, так как требует особых опций компиляции PHP как такового. Отсюда план: сначала научиться компилировать PHP по аналогии с образом Docker, а потом уже разбираться с добрым доктором. Он написан на Go, так что за этот шаг я особо и не переживал.

Компиляция PHP

Во FreeBSD (и, к счастью, в образе freebsd-runtime) для загрузки файлов из интернета есть утилита fetch(1). Будем работать в /usr/src:

cd /usr/src
fetch -o php.tar.xz https://www.php.net/distributions/php-8.4.11.tar.xz
echo '04cd331380a8683a5c2503938eb51764d48d507c53ad4208d2c82e0eed779a00 php.tar.xz' > checksumfile
sha256sum -c checksumfile
rm checksumfile
mkdir php
tar -Jxf php.tar.xz -C /usr/src/php --strip-components=1
cd php

Версия PHP у нас 8.4.11. Скачали ее, проверили контрольную сумму, распаковали и перешли в директорию исходников. Общий подход к компиляции следующий: через утилиту configure все настроить, а далее make && make install. Дьявол, как говорится, в деталях.

Установка ПО

Для начала нужно установить некоторые дополнительные средства сборки. Их, а также необходимые зависимости времени выполнения, можно вычислить по базе портов FreeBSD. В частности:

Build dependencies:

  1. re2c>0 : devel/re2c
  2. pkgconf>=1.3.0_1 : devel/pkgconf
  3. autoconf>=2.72 : devel/autoconf
  4. automake>=1.17 : devel/automake

Library dependencies:

  1. libargon2.so : security/libargon2
  2. libpcre2-8.so : devel/pcre2
  3. libxml2.so : textproc/libxml2

Перед установкой предлагаю переключиться на репозиторий «самых последних» версий вместо ежеквартального (по умолчанию):

mkdir -p /usr/local/etc/pkg/repos
echo 'FreeBSD: { url: "pkg+http://pkg.FreeBSD.org/${ABI}/latest" }' > /usr/local/etc/pkg/repos/FreeBSD.conf

Так как в состав официальных образов PHP входят еще несколько расширений помимо присутствующих в php84, набор устанавливаемых пакетов будет следующим:

pkg install \
    # Build tools
    autoconf \
    automake \
    pkgconf \
    re2c \
    # Runtime dependencies
    brotli \
    capstone \
    curl \
    devel/oniguruma \
    libargon2 \
    libedit \
    libiconv \
    libsodium \
    libxml2 \
    pcre2 \
    sqlite3

Можем начинать.

Настройка компиляции

С помощью php -i мы можем посмотреть, с какими опциями компилировались PHP в обоих вариантах.

FreeBSD:

PKG_CONFIG=pkgconf \
PKG_CONFIG_LIBDIR="/wrkdirs/usr/ports/lang/php84/work/.pkgconfig:/usr/local/libdata/pkgconfig:/usr/local/share/pkgconfig:/usr/libdata/pkgconfig" \
CFLAGS="-O2 -pipe -fstack-protector-strong -isystem /usr/local/include -fno-strict-aliasing" \
LDFLAGS=" -L/usr/lib -lcrypto -lssl" \
CPP=cpp \
CXXFLAGS="-O2 -pipe -fstack-protector-strong -isystem /usr/local/include -fno-strict-aliasing -isystem /usr/local/include" \
OPENSSL_CFLAGS="-I/usr/include" \
OPENSSL_LIBS="-L/usr/lib -lssl -lcrypto" \
./configure \
	--disable-all \
	--program-prefix= \
	--with-config-file-scan-dir=/usr/local/etc/php \
	--with-layout=GNU \
	--with-libxml \
	--with-openssl \
	--with-password-argon2=/usr/local \
	--enable-dtrace \
	--enable-embed \
	--enable-fpm \
	--with-fpm-group=www \
	--with-fpm-user=www\
	--enable-mysqlnd \
	--with-external-pcre=/usr/local \
	--prefix=/usr/local \
	--localstatedir=/var \
	--mandir=/usr/local/share/man \
	--infodir=/usr/local/share/info/ \
	--build=amd64-portbld-freebsd14.2

FrankenPHP:

CFLAGS="-fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64" \
CPPFLAGS="-fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64" \
LDFLAGS="-Wl,-O1 -pie" \
PHP_UNAME="Linux - Docker" \
PHP_BUILD_PROVIDER=https://github.com/docker-library/php \
./configure \
	--build=x86_64-linux-gnu \
	--with-config-file-path=/usr/local/etc/php \
	--with-config-file-scan-dir=/usr/local/etc/php/conf.d \
	--enable-option-checking=fatal \
	--with-mhash \
	--with-pic \
	--enable-mbstring \
	--enable-mysqlnd \
	--with-password-argon2 \
	--with-sodium=shared \
	--with-pdo-sqlite=/usr \
	--with-sqlite3=/usr \
	--with-curl \
	--with-iconv \
	--with-openssl \
	--with-readline \
	--with-zlib \
	--enable-phpdbg \
	--enable-phpdbg-readline \
	--with-pear \
	--with-libdir=lib/x86_64-linux-gnu \
	--enable-embed \
	--enable-zts \
	--disable-zend-signals

Теперь все это надо превратить в некую сборную солянку. FPM и dtrace не нужны, но в соответствии с требованиями FrankenPHP обязательны флаги --enable-embed --enable-zts --disable-zend-signals. Для FreeBSD также надо подправить пути к библиотекам. Вот что получилось у меня:

CPP=cpp \
CFLAGS="-O2 -pipe -fstack-protector-strong -isystem /usr/local/include -fno-strict-aliasing" \
LDFLAGS=" -L/usr/lib -lcrypto -lssl" \
CXXFLAGS="-O2 -pipe -fstack-protector-strong -isystem /usr/local/include -fno-strict-aliasing -isystem /usr/local/include" \
OPENSSL_CFLAGS="-I/usr/include" \
OPENSSL_LIBS="-L/usr/lib -lssl -lcrypto" \
./configure \
    # FreeBSD
    --program-prefix= \
    --prefix=/usr/local \
    --with-layout=GNU \
    --infodir=/usr/local/share/info/ \
    --localstatedir=/var \
    --mandir=/usr/local/share/man \
    # Adapted from Docker
    --with-config-file-path="/usr/local/etc/php" \
    --with-config-file-scan-dir="/usr/local/etc/php/conf.d" \
    --enable-option-checking=fatal \
    --with-mhash \
    --with-pic \
    --enable-mbstring \
    --enable-mysqlnd \
    --with-password-argon2=/usr/local \
    --with-sodium=shared \
    --with-pdo-sqlite=/usr/local \
    --with-sqlite3=/usr/local \
    --with-curl \
    --with-iconv=/usr/local \
    --with-openssl \
    --with-readline \
    --with-zlib \
    --enable-phpdbg \
    --enable-phpdbg-readline \
    --with-pear \
    --enable-embed \
    # FrankenPHP
    --enable-zts \
    --disable-zend-signals

Честно говоря, необходимость некоторых опций из официальных образов у меня вызывает сомнения, например включение эмуляции mhash, --enable-phpdbg, да и CGI наверное можно было бы выключить. Так или иначе, можно что-то скомпилировать.

Компиляция

Как и было анонсировано ранее:

make -j "$(nproc)"
make install

Флагом -j распараллеливаем выполнение на все процессоры (ядра). Пример вывода второй команды:

Installing PHP SAPI module:       embed
Installing shared extensions:     /usr/local/lib/php/20240924-zts/
Installing PHP CLI binary:        /usr/local/bin/
Installing PHP CLI man page:      /usr/local/share/man/man1/
Installing phpdbg binary:         /usr/local/bin/
Installing phpdbg man page:       /usr/local/share/man/man1/
Installing PHP CGI binary:        /usr/local/bin/
Installing PHP CGI man page:      /usr/local/share/man/man1/
Installing build environment:     /usr/local/lib/php/build/
Installing header files:          /usr/local/include/php/
Installing helper programs:       /usr/local/bin/
  program: phpize
  program: php-config
Installing man pages:             /usr/local/share/man/man1/
  page: phpize.1
  page: php-config.1
Installing PEAR environment:      /usr/local/share/pear/
[PEAR] Archive_Tar    - installed: 1.5.0
[PEAR] Console_Getopt - installed: 1.4.3
[PEAR] Structures_Graph- installed: 1.2.0
[PEAR] XML_Util       - installed: 1.4.5
warning: pear/PEAR dependency package "pear/Archive_Tar" installed version 1.5.0 is not the recommended version 1.4.4
warning: pear/PEAR dependency package "pear/Structures_Graph" installed version 1.2.0 is not the recommended version 1.1.1
[PEAR] PEAR           - installed: 1.10.16
Wrote PEAR system config file at: /usr/local/etc/pear.conf
You may want to add: /usr/local/share/pear to your php.ini include_path

Немного странно, что PEAR ругается на более новые версии. Интересное и далее – в официальных образах удаляют символы отладки из получившихся исполняемых и библиотечных файлов. Непосредственно во FreeBSD ее переносить не стоит, там и параметры поиска слишком обширные получаются, и в случае если попадается «неправильный» файл, то похоже что действие в принципе не выполняется. Поэтому в немалой степени перечисляем файлы поименно, только библиотеки можно массово «раздеть»:

strip --strip-all /usr/local/bin/perl
strip --strip-all /usr/local/bin/php-cgi
strip --strip-all /usr/local/bin/php
strip --strip-all /usr/local/bin/phpdbg
strip --strip-all /usr/local/lib/libphp.so
find /usr/local/lib/php \
    -type f \
    -name '*.so' \
    -exec sh -euxc 'strip --strip-all "$@" || :' sh {} +

Прибираемся за собой и копируем образцово-показательные файлы конфигурации PHP:

make clean
cp -v php.ini-* /usr/local/etc/php

Далее предлагается обновить каналы PECL:

pecl update-channels

Наконец, надо включить расширения opcache и sodium, минимально для этого надо создать файлы вида:

/usr/local/etc/php/conf.d/opcache.ini

zend_extension=opcache

/usr/local/etc/php/conf.d/sodium.ini

 extension=sodium

На этом PHP практически в том же виде, что и официальном образе, готов.

Установка расширений

С одной стороны, можно было бы скомпилировать PHP сразу с нужным набором расширений. Скорее всего, если делать для себя, так даже проще и лучше. Но я сделал иначе – позаимствовал скрипты из официального образа, заменил везде docker на podman и убрал фрагменты, касающихся конкретных дистрибутивов Linux.

Из специфики FreeBSD – понадобилось установить GNU-совместимую программу getopt. BSD-шная работает иначе, тем самым ломая проверку переданных параметров в скрипты.

pkg install getopt

Вместо переменной окружения CPPFLAGS во FreeBSD применяется CXXFLAGS (кстати про переменные окружения – мы объявим их в контейнере). Подправил команды find – где-то потребовалось явно указать путь, а где-то немного переделать:

find modules \
    -maxdepth 1 \
    -name '*.so' \
    -exec sh -euxc ' \
        strip --strip-all "$@" || :
    ' sh {} +

Таким образом вместо создания ini-файлов вручную ранее для включения расширений можно (и будет нужно в контейнере) выполнить команды:

podman-php-ext-enable opcache
podman-php-ext-enable sodium

В результате можем устанавливать расширения примерно как в официальных образах, разве что менеджер пакетов у нас pkg. К примеру, установка gd будет выглядеть примерно так:

pkg install \
    graphics/libavif \
    graphics/png \
    libgd \
    x11/libXpm
export \
	PHP_CFLAGS="-O2 -pipe -fstack-protector-strong -isystem /usr/local/include -fno-strict-aliasing" \
	PHP_LDFLAGS=" -L/usr/lib -lcrypto -lssl" \
	PHP_CXXFLAGS="-O2 -pipe -fstack-protector-strong -isystem /usr/local/include -fno-strict-aliasing -isystem /usr/local/include"
podman-php-ext-configure gd --with-webp --with-jpeg --with-xpm --with-freetype --with-avif
podman-php-ext-install -j"$(nproc)" gd

Без вспомогательных скриптов (с учетом заранее установленных зависимостей и переменных окружения) базово было бы что-то такое:

cd /usr/src/php/ext/gd
phpize
./configure \
	--enable-option-checking=fatal \
	--with-webp \
	--with-jpeg \
	--with-xpm \
	--with-freetype \
	--with-avif
make -j "$(nproc)"
make install
make clean
echo 'extension=gd' > /usr/local/etc/php/conf.d/gd.ini

Для примера установим еще и PECL uploadprogress:

pecl install uploadprogress-2.0.2
podman-php-ext-enable uploadprogress

На первый взгляд расширения PECL устанавливаются просто, но если в pkg нет нужных зависимостей, то тогда… не будем о грустном. pardon

На всякий случай, исходные коды моих скриптов на момент публикации статьи можно посмотреть в репозитории:

https://git.dmkos.ru/containers/php/-/tree/frankenphp-freebsd-scripts/freebsd/frankenphp/8.4-14.3/sbin

Что ж, переходим к основному блюду.

Компиляция FrankenPHP

Для этого необходимо установить lang/go:

pkg install lang/go
go telemetry off

Мне представляется целесообразным сразу же отключить сбор телеметрии. Казалось бы, можно приступать, но на самом деле еще не совсем.

Компиляция watcher-c

Эта библиотека требуется для функции отслеживания изменения файлов в режиме worker. Теоретически можно собрать FrankenPHP без нее, но если вы планируете этот самый режим включать, то наверное стоит его заранее протестировать. Что будет сделать довольно затруднительно без отслеживания изменившихся файлов, пришлось бы вручную перезапускать этих самых рабочих:

curl -X POST http://localhost:2019/frankenphp/workers/restart

Согласно документации предлагается компилировать по стандарту C++ 2017, но то ли документация отстает, то ли это особенности FreeBSD, но у меня получилось скомпилировать библиотеку только по следующему стандарту, и то с предупреждениями:

c++ -o libwatcher-c.so ./src/watcher-c.cpp -I ./include -I ../include -std=c++20 -fPIC -shared

Фрагмент целиком с распаковкой и установкой:

cd /usr/src
fetch -o watcher.tar.gz https://github.com/e-dant/watcher/archive/refs/tags/0.13.6.tar.gz
tar xzf watcher.tar.gz -C /usr/src
cd watcher-0.13.6/watcher-c
c++ -o libwatcher-c.so ./src/watcher-c.cpp -I ./include -I ../include -std=c++20 -fPIC -shared
strip --strip-all libwatcher-c.so
cp libwatcher-c.so /usr/local/lib/libwatcher-c.so
cp -R include/wtr /usr/local/include

Соответственно кладем библиотеку в /usr/local/lib и копируем заголовочные файлы в /usr/local/include, они потребуются на следующем этапе.

Сборка

Наконец-то. Мне показалась излишней установка xcaddy, раз можно обойтись и без нее. Нюансы, впрочем, возникли и здесь. Оказалось, что с параметрами как в документации, даже если отключить brotli через тег сборки, все равно требуются заголовочные файлы, и что по умолчанию версия FrankenPHP значится как dev, хотя мы конечно же скачаем релиз.

Обе проблемы решаются добавлением параметров в переменную окружения CGO_CFLAGS:

  1. -I/usr/local/include - подключает стандартный каталог FreeBSD для заголовочных файлов,
  2. -DFRANKENPHP_VERSION=v1.9.0 - прописывает версию.

Итого команда компиляции приобретает вид:

CGO_CFLAGS="-DFRANKENPHP_VERSION=v1.9.0 $(php-config --includes) -I/usr/local/include" \
CGO_LDFLAGS="$(php-config --ldflags) $(php-config --libs)" \
go build -tags=nobadger,nomysql,nopgx

Будьте готовы к тому, что в процессе скачается «весь» GitHub в качестве зависимостей. Но в целом как бы и все? Осталось разложить по полочкам, т.е. frankenphp в /usr/local/bin, а Caddyfile – в /usr/local/etc/frankenphp:

cd /usr/src
fetch -o frankenphp.tar.gz https://github.com/php/frankenphp/archive/refs/tags/v1.9.0.tar.gz
tar xzf frankenphp.tar.gz -C /usr/src
cd frankenphp-1.9.0/caddy/frankenphp
CGO_CFLAGS="-DFRANKENPHP_VERSION=v1.9.0 $(php-config --includes) -I/usr/local/include" \
CGO_LDFLAGS="$(php-config --ldflags) $(php-config --libs)" \
go build -tags=nobadger,nomysql,nopgx
mkdir -p /usr/local/etc/frankenphp/Caddyfile.d
mv frankenphp /usr/local/bin/frankenphp
cp Caddyfile /usr/local/etc/frankenphp/Caddyfile

Проверка

Создадим традиционный файл с phpinfo() и запустим веб-сервер. Предлагаю явно, чтобы видеть логи:

mkdir -p /usr/local/www/app/public
cd /usr/local/www/app
echo '<?php phpinfo(); ?>' > public/index.php
frankenphp run --config /usr/local/etc/frankenphp/Caddyfile

Если ваша FreeBSD обладает графическим интерфейсом и/или браузером, то заходим на localhost и подтверждаем самоподписанный сертификат. Иначе в другом сеансе можно воспользоваться curl:

curl -vkL localhost

Надеюсь, у вас все заработает и вы увидите сведения о PHP.

Контейнер Podman

Теперь осталось, как говорится, применить полученные знания на практике. Как пропатчить KDE установить Podman во FreeBSD, я уже писал, поэтому переходим сразу к делу.

Образ для компиляции

Да, в подзаголовке спойлер – образов будет несколько (два или три, смотря как считать). Базовый образ freebsd-runtime, очевидно, не содержит в себе никаких компиляторов. Казалось бы, можно установить какой-нибудь gcc из пакетов, ан нет. По мнению ./configure, компилировать он не умеет даже после того, как я подкинул линковщик (ld). Кстати в этих ваших интернетах с gcc в принципе не советуют связываться. Скажу больше, в базовом образе даже sed какой-то не такой (можно было бы починить установкой gsed).

В итоге по аналогии с тюрьмами я взял и распаковал базовую ОС в образ. Разумеется, каталог rescue не нужен, а еще в процессе извлечения не удается изменить права на несколько файлов даже под root (!). Чтобы сборка из-за этого не прервалась, по сути не начавшись, применил трюк с || true.

ARG FREEBSD_VERSION
FROM docker.io/freebsd/freebsd-runtime:$FREEBSD_VERSION

ARG FREEBSD_VERSION PHP_VERSION FRANKENPHP_VERSION WATCHER_VERSION

# Base OS, including `make`, complier, linker and so on
RUN set -eux; \
        cd /; \
        fetch https://download.freebsd.org/ftp/releases/amd64/amd64/${FREEBSD_VERSION}-RELEASE/base.txz; \
        # ignore permission warnings during extraction
        tar xfm base.txz -C / --keep-old-files --exclude=rescue || true; \
        rm base.txz

Файл я назвал builder.containerfile и, как видите, сразу же параметризовал все версии. Для минимизации размера образа (и промежуточных слоев) каждый раз будем за собой прибираться. В этот раз, например, удаляем загруженный архив.

Далее я объединил в одну команду всю установку ПО из пакетов:

# Install software
RUN set -eux; \
        pkg bootstrap -y; \
        \
        # use the latest packages
        mkdir -p /usr/local/etc/pkg/repos; \
        echo 'FreeBSD: { url: "pkg+http://pkg.FreeBSD.org/${ABI}/latest" }' > /usr/local/etc/pkg/repos/FreeBSD.conf; \
        \
        pkg install -y \
            # Build tools
            autoconf \
            automake \
            getopt \
            lang/go \
            pkgconf \
            re2c \
            # Runtime dependencies
            brotli \
            capstone \
            curl \
            devel/oniguruma \
            libargon2 \
            libedit \
            libiconv \
            libsodium \
            libxml2 \
            pcre2 \
            sqlite3; \
        # cleanup to reduce image size
        pkg clean -ay; \
        rm -rf /var/db/pkg/repos

Примерно то же самое мы делали и раньше при создании образа PHP-FPM. Следующий шаг – загрузка всех исходников:

# Fetch sources
ENV PHP_VERSION=$PHP_VERSION
ENV PHP_SHA256="04cd331380a8683a5c2503938eb51764d48d507c53ad4208d2c82e0eed779a00"
RUN set -eux; \
        fetch -o /usr/src/php.tar.xz https://www.php.net/distributions/php-${PHP_VERSION}.tar.xz; \
        echo "$PHP_SHA256 /usr/src/php.tar.xz" > /usr/src/checksumfile; \
        sha256sum -c /usr/src/checksumfile; \
        rm /usr/src/checksumfile; \
        # For GitHub sources digests are not available
        fetch -o /usr/src/frankenphp.tar.gz https://github.com/php/frankenphp/archive/refs/tags/v${FRANKENPHP_VERSION}.tar.gz; \
        fetch -o /usr/src/watcher.tar.gz https://github.com/e-dant/watcher/archive/refs/tags/${WATCHER_VERSION}.tar.gz

Вроде бы из интернета нам пока больше ничего не потребуется, так что заодно эти шаги могут браться из кэша, если вдруг на последующих надо будет что-то исправить.

Копируем скрипты:

COPY --chmod=755 sbin/podman-php-source sbin/podman-php-ext-* /usr/local/sbin/

Понимаю, что расположение sbin в данном случае некоторым образом дискуссионно, но честно говоря я больше руководствовался тем, что в нем мало файлов (в отличие от /usr/local/bin).

Приступаем к сборке PHP, тут-то как раз и объявляем переменные окружения:

# PHP build options for FreeBSD
ENV PHP_CFLAGS="-O2 -pipe -fstack-protector-strong -isystem /usr/local/include -fno-strict-aliasing"
ENV PHP_LDFLAGS=" -L/usr/lib -lcrypto -lssl"
ENV PHP_CXXFLAGS="$PHP_CFLAGS -isystem /usr/local/include"
ENV PHP_OPENSSL_CFLAGS="-I/usr/include"
ENV PHP_OPENSSL_LIBS="-L/usr/lib -lssl -lcrypto"
ENV PHP_INI_DIR="/usr/local/etc/php"

RUN set -eux; \
        podman-php-source extract; \
        cd /usr/src/php; \
        mkdir -p "$PHP_INI_DIR/conf.d"; \
        \
        CPP=cpp \
        CFLAGS="$PHP_CFLAGS" \
        LDFLAGS="$PHP_LDFLAGS" \
        CXXFLAGS="$PHP_CXXFLAGS" \
        OPENSSL_CFLAGS="$PHP_OPENSSL_CFLAGS" \
        OPENSSL_LIBS="$PHP_OPENSSL_LIBS" \
        PHP_UNAME="FreeBSD - Podman" \
        ./configure \
            # FreeBSD
            --program-prefix= \
            --prefix=/usr/local \
            --with-layout=GNU \
            --infodir=/usr/local/share/info/ \
            --localstatedir=/var \
            --mandir=/usr/local/share/man \
            # Adapted from Docker
            --with-config-file-path="/usr/local/etc/php" \
            --with-config-file-scan-dir="/usr/local/etc/php/conf.d" \
            --enable-option-checking=fatal \
            --with-mhash \
            --with-pic \
            --enable-mbstring \
            --enable-mysqlnd \
            --with-password-argon2=/usr/local \
            --with-sodium=shared \
            --with-pdo-sqlite=/usr/local \
            --with-sqlite3=/usr/local \
            --with-curl \
            --with-iconv=/usr/local \
            --with-openssl \
            --with-readline \
            --with-zlib \
            --enable-phpdbg \
            --enable-phpdbg-readline \
            --with-pear \
            --enable-embed \
            # FrankenPHP
            --enable-zts \
            --disable-zend-signals; \
        \
        make -j "$(nproc)"; \
        make install; \
        strip --strip-all /usr/local/bin/perl; \
        strip --strip-all /usr/local/bin/php-cgi; \
        strip --strip-all /usr/local/bin/php; \
        strip --strip-all /usr/local/bin/phpdbg; \
        strip --strip-all /usr/local/lib/libphp.so; \
        find /usr/local/lib/php \
            -type f \
            -name '*.so' \
            -exec sh -euxc 'strip --strip-all "$@" || :' sh {} +; \
        make clean; \
        cp -v php.ini-* "$PHP_INI_DIR/"; \
        pecl update-channels; \
        rm -rf /tmp/pear /.pearrc; \
        # smoke test
        php --version; \
        podman-php-ext-enable opcache; \
        podman-php-ext-enable sodium; \
        cd /; \
        podman-php-source delete

Уфф. tired Из отличий от рассмотренного выше – извлечение исходников из архива и их удаление в конце с использованием вспомогательного скрипта podman-php-source, очистка временных файлов pear и «дымовой тест» PHP путем запроса версии.

Компилируем watcher-c:

# Compile watcher-c
# https://github.com/e-dant/watcher/tree/release/watcher-c
RUN set -eux; \
        tar xzf /usr/src/watcher.tar.gz -C /usr/src; \
        cd /usr/src/watcher-${WATCHER_VERSION}/watcher-c; \
        # unlike documented, compile possible with c++20 standard
        c++ -o libwatcher-c.so ./src/watcher-c.cpp -I ./include -I ../include -std=c++20 -fPIC -shared; \
        strip --strip-all libwatcher-c.so; \
        cp libwatcher-c.so /usr/local/lib/libwatcher-c.so; \
        cp -R include/wtr /usr/local/include; \
        cd /; \
        rm -rf /usr/src/watcher-${WATCHER_VERSION}

Компилируем FrankenPHP:

# Compile FrankenPHP without xcaddy
# https://frankenphp.dev/docs/compile/#without-xcaddy
RUN set -eux; \
        tar xzf /usr/src/frankenphp.tar.gz -C /usr/src; \
        cd /usr/src/frankenphp-${FRANKENPHP_VERSION}/caddy/frankenphp; \
        go telemetry off; \
        \
        CGO_CFLAGS="-DFRANKENPHP_VERSION=v${FRANKENPHP_VERSION} $(php-config --includes) -I/usr/local/include" \
        CGO_LDFLAGS="$(php-config --ldflags) $(php-config --libs)" \
        go build -tags=nobadger,nomysql,nopgx; \
        \
        # smoke test
        ./frankenphp version; \
        mkdir -p /usr/local/etc/frankenphp/Caddyfile.d; \
        mv frankenphp /usr/local/bin/frankenphp; \
        cp Caddyfile /usr/local/etc/frankenphp/Caddyfile; \
        go clean -cache -modcache; \
        cd /; \
        rm -rf /usr/src/frankenphp-${FRANKENPHP_VERSION}

Опять же добавил «дымовой тест», а еще очистку кэша go:

go clean -cache -modcache

Ранее мы это не обсуждали, но с помощью переменных окружения надо настроить и пути для хранения состояния Caddy (FrankenPHP). В оригинальном образе это директории в корне, но я все же решил их разместить в недрах /var/db, как это принято во FreeBSD:

# Set up directories in FreeBSD style
ENV XDG_CONFIG_HOME /var/db/frankenphp/config
ENV XDG_DATA_HOME /var/db/frankenphp/data
RUN set -eux; \
        mkdir -p $XDG_CONFIG_HOME; \
        mkdir -p $XDG_DATA_HOME; \
        mkdir -p /usr/local/www/app/public; \
        echo '<?php phpinfo(); ?>' > /usr/local/www/app/public/index.php

Наконец, настраиваем сам контейнер на автоматический запуск FrankenPHP:

COPY --chmod=755 sbin/podman-php-entrypoint /usr/local/sbin/
ENTRYPOINT ["podman-php-entrypoint"]

WORKDIR /usr/local/www/app
CMD ["--config", "/usr/local/etc/frankenphp/Caddyfile"]

Точка входа по сути объявляет команду по умолчанию frankenphp run, поэтому в директиве CMD достаточно передать лишь параметры запуска. Тем самым образ для компиляции готов, всего-то за 182 строки. Позвольте мне не вставлять всю эту простыню целиком заново… sorry

У себя в документации я оставил предупреждение о том, что этот образ нельзя использовать в боевом окружении. Причин тому как минимум две – лишние 900 метров после распаковки операционной системы и работа под root. Поэтому нужен другой…

Образ для эксплуатации

Здесь мы возьмем образ для компиляции, установим в него еще несколько расширений, а потом скопируем получившийся результат в контейнер на основе freebsd-runtime. Файл будет называться runner.containerfile.

ARG FRANKENPHP_VERSION PHP_VERSION FREEBSD_VERSION
FROM docker.io/dmkos/php-freebsd:frankenphp-${FRANKENPHP_VERSION}-builder-php${PHP_VERSION}-freebsd${FREEBSD_VERSION} AS builder

# Install dependencies
RUN set -eux; \
        pkg install -y \
            databases/postgresql17-client \
            devel/icu \
            graphics/libavif \
            graphics/png \
            libgd \
            x11/libXpm; \
        # cleanup to reduce image size
        pkg clean -ay; \
        rm -rf /var/db/pkg/repos

# Build and install additional PHP extensions
RUN set -eux; \
        podman-php-source extract; \
        podman-php-ext-configure gd --with-webp --with-jpeg --with-xpm --with-freetype --with-avif; \
        podman-php-ext-install -j"$(nproc)" \
            bcmath \
            bz2 \
            gd \
            intl \
            pcntl \
            pdo_mysql \
            pdo_pgsql \
            pgsql; \
        pecl install \
            # https://pecl.php.net/package/uploadprogress
            uploadprogress-2.0.2; \
        podman-php-ext-enable uploadprogress; \
        rm -rf /tmp/pear; \
        podman-php-source delete; \
        # smoke test
        php -m | grep -e gd -e intl -e pgsql

Пока что все примерно как я и рассказывал про установку расширений, разве что помимо gd и uploadprogress я добавил bcmath, bz2, intl, pcntl, pdo_mysql, pdo_pgsql и pgsql наряду с зависимостями времени выполнения.

Тут, конечно, всплывает проблема с обнаружением сервисов. Расширения для MySQL и PostgreSQL есть, а как к ним подключаться, непонятно. Надеюсь, в будущем эту проблему решат.

Я решил предустановить composer. Это можно сделать двумя способами: взять из готового образа или через curl. Второй способ выглядит примерно так:

curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer

К сожалению в последнее время сайт отвечает не всегда, поэтому я решил воспользоваться первым. Хотя получается, что вместо условных 2 мегабайт (composer.phar) придется скачивать 200 (его образ).

Пока что только подключим его в качестве промежуточного:

# Composer's official Docker image
FROM --platform=linux/amd64 docker.io/library/composer:2 AS composer

Образа для FreeBSD, очевидно, не существует, поэтому явно указываем платформу.

Начинаем финальную сборку. Устанавливаем все зависимости:

FROM docker.io/freebsd/freebsd-runtime:$FREEBSD_VERSION

# Install dependencies
RUN set -eux; \
        pkg bootstrap -y; \
        \
        # use the latest packages
        mkdir -p /usr/local/etc/pkg/repos; \
        echo 'FreeBSD: { url: "pkg+http://pkg.FreeBSD.org/${ABI}/latest" }' > /usr/local/etc/pkg/repos/FreeBSD.conf; \
        \
        pkg install -y \
            # basic
            brotli \
            capstone \
            curl \
            devel/oniguruma \
            libargon2 \
            libedit \
            libiconv \
            libsodium \
            libxml2 \
            pcre2 \
            sqlite3 \
            # additional
            databases/postgresql17-client \
            devel/icu \
            graphics/libavif \
            graphics/png \
            libgd \
            x11/libXpm \
            # composer
            unzip; \
        # cleanup to reduce image size
        pkg clean -ay; \
        rm -rf /var/db/pkg/repos

Подтянул unzip, иначе толку от composer будет, мягко говоря, немного.

Сейчас я хотел бы немного отвлечься и немного рассказать о команде podman image diff. Она может сравнить файловые системы двух образов или одного, но между слоями. Это оказалось очень удобным, например предыдущий слой заканчиваем на make, а следующий делаем RUN make install. Тогда можно наглядно увидеть, что произошло в результате этого действия. Кажется Docker так не умеет, а еще есть команда podman image mount, что дает возможность изучить файловую систему образа тем же Midnight Commander. Супер!

Собственно, теперь нам надо скопировать результаты компиляции. Так как в /usr/local/bin сборщика находится почти вся FreeBSD, приходится долго и упорно перечислять файлы поименно:

# Copy build
COPY --from=builder --chmod=755 \
        /usr/local/bin/frankenphp \
        /usr/local/bin/pear \
        /usr/local/bin/peardev \
        /usr/local/bin/pecl \
        /usr/local/bin/phar.phar \
        /usr/local/bin/php \
        /usr/local/bin/php-cgi \
        /usr/local/bin/php-config \
        /usr/local/bin/phpdbg \
        /usr/local/bin/phpize \
        /usr/local/bin/
COPY --from=builder /usr/local/etc/frankenphp/ /usr/local/etc/frankenphp/
COPY --from=builder /usr/local/etc/php/ /usr/local/etc/php/
COPY --from=builder --chmod=644 /usr/local/etc/pear.conf /usr/local/etc/
COPY --from=builder /usr/local/include/php/ /usr/local/include/php/
COPY --from=builder /usr/local/lib/php/ /usr/local/lib/php/
COPY --from=builder --chmod=755 /usr/local/lib/libphp.so /usr/local/lib/libwatcher-c.so /usr/local/lib/
COPY --from=builder /usr/local/share/pear/ /usr/local/share/pear/
COPY --from=builder /usr/local/www/app/public/index.php /usr/local/www/app/public/

Да уж, но зато не тащим за собой 900 мегабайт. Там еще и права могут быть какие-то не такие, по возможности указал правильные в параметрах COPY.

Подтягиваем composer:

# Install composer from image
COPY --from=composer /usr/bin/composer /usr/local/bin/

Под шумок забираем chown, его почему-то нет (хотя в целом он наверное не так уж и нужен вообще-то, но нам понадобится):

# Copy sysutils
COPY --from=builder /usr/sbin/chown /usr/sbin/

Далее я немного оптимизировал и слегка нелогично собрал в одну кучу установку разрешений, создание директорий и символических ссылок:

ENV GODEBUG cgocheck=0
ENV XDG_CONFIG_HOME /var/db/frankenphp/config
ENV XDG_DATA_HOME /var/db/frankenphp/data
RUN set -eux; \
        podman-php-fix-permissions; \
        cd /usr/local/bin; \
        ln -s phar.phar phar; \
        chmod -h 755 phar; \
        mkdir -p $XDG_CONFIG_HOME; \
        mkdir -p $XDG_DATA_HOME; \
        # smoke test
        php --version && frankenphp version

Еще один скрипт, на сей раз это моя личная инициатива, меняет права с 700 на 755 и 600 на 644 в скопированной сборке. Поскольку пришлось ставить 755 еще и на /usr/local/sbin, возможно я и впрямь неправильно расположил скрипты… scratch

Осталось подготовить контейнер для запуска под пользователем www:

ARG PHP_VERSION
ENV PHP_VERSION $PHP_VERSION
ENV PHP_INI_DIR /usr/local/etc/php

# Run as non-root user
RUN set -eux; \
        chown -R www:www $PHP_INI_DIR; \
        chown -R www:www /usr/local/etc/frankenphp; \
        chown -R www:www /usr/local/www/app; \
        chown -R www:www /var/db/frankenphp; \
        # Custom ports, should be redirected
        sed -i '' 's/{$CADDY_GLOBAL_OPTIONS}/http_port 10080\n        https_port 10443\n        {$CADDY_GLOBAL_OPTIONS}/' /usr/local/etc/frankenphp/Caddyfile

EXPOSE 10080/tcp 10443/tcp 10443/udp

ENTRYPOINT ["podman-php-entrypoint"]
WORKDIR /usr/local/www/app
CMD ["--config", "/usr/local/etc/frankenphp/Caddyfile"]
USER www

Опять немного бесхозные переменные окружения, возможно в будущем распределю их чуть иначе, а еще chown пригодился. Здесь, кстати, есть интересный момент, связанный с портами. Непривилегированный пользователь не может слушать «низкие» порты, в частности 80 и 443, поэтому перенастраиваем веб-сервер на прослушивание портов 10080 и 10443 соответственно. В результате в podman run или compose.yaml нужно пробрасывать порт 80 на 10080 и 443 на 10443.

Использование

По сути, как пользоваться образом для сборки, я показал выше. Посему коротко о втором.

Как и в прошлый раз, образы можно загрузить с моего личного GitLab, а также Docker Hub и GitHub.

  • glcr.dmkos.ru/containers/php
  • docker.io/dmkos/php-freebsd
  • ghcr.io/dmkos/php

Примеры для podman-compose. Сайт example.com:

services:
  php:
    build: .
    restart: always
    environment:
      SERVER_NAME: "example.com"
    ports:
      - "80:10080" # HTTP
      - "443:10443" # HTTPS
      - "443:10443/udp" # HTTP/3
    volumes:
      - ./app:/usr/local/www/app
      - franken_config:/var/db/frankenphp/config
      - franken_data:/var/db/frankenphp/data

volumes:
  franken_config:
  franken_data:
FROM glcr.dmkos.ru/containers/php:frankenphp-freebsd

# Use the default production configuration
RUN cp "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini"

Разработка сайта на Symfony, режим worker:

services:
  php:
    build: .
    restart: always
    environment:
      FRANKENPHP_CONFIG: |
        worker {
          file ./public/index.php
          num 1
          watch
        }
      APP_RUNTIME: Runtime\FrankenPhpSymfony\Runtime
      CADDY_GLOBAL_OPTIONS: "auto_https off"
      SERVER_NAME: "http://"
    ports:
      - "80:10080"
    volumes:
      - ./app:/usr/local/www/app
      - franken_config:/var/db/frankenphp/config
      - franken_data:/var/db/frankenphp/data

volumes:
  franken_config:
  franken_data:
FROM glcr.dmkos.ru/containers/php:frankenphp-freebsd

# Use the default development configuration
RUN cp "$PHP_INI_DIR/php.ini-development" "$PHP_INI_DIR/php.ini"

Переменные окружения:

  • CADDY_GLOBAL_OPTIONS: глобальные настройки Caddy
  • FRANKENPHP_CONFIG: настройка директивы frankenphp в глобальном блоке
  • CADDY_EXTRA_CONFIG: произвольная настройка Caddy, например фрагменты или редиректы с/на www
  • SERVER_NAME: адрес, в случае указания домена будет сформирован сертификат TLS (по умолчанию localhost)
  • SERVER_ROOT: корневой каталог сайта, по умолчанию public/ относительно рабочей директории
  • CADDY_SERVER_EXTRA_DIRECTIVES: дополнительная настройка сервера по умолчанию, например модулей Mercure и/или vulcain

Можно подмонтировать директорию дополнительных *.caddyfile к /usr/local/etc/frankenphp/Caddyfile.d или заменить конфигурацию целиком:

    volumes:
      - ./config:/usr/local/etc/frankenphp

В общем, все замечательно, кроме мелочи, не влезающей ни в какие ворота. А именно – пока совершенно неясно, как быть с обнаружением сервисов. Повторюсь, расширения подключений к БД есть, а самих серверов – нет. Печаль… cry

Заключение

Развивая предыдущую мысль, у меня двойственные впечатления от проделанной работы. С одной стороны, все компилируется и на первый взгляд даже работает. Это вроде шутка, но только лишь с долей шутки: в идеале хотелось бы как следует все протестировать, но вот только мне неизвестен способ это сделать путем развертывания некоего специального веб-приложения. Демка Symfony, скажем, запускается, наблюдатель за изменениями файлов в ней наблюдает. Достаточно ли этого для далеко идущих выводов? Вряд ли.

С другой стороны, я отдаю себе отчет в том, что установка дополнительных расширений – это боль. wacko Ради сокращения размеров образа и безопасности нужно писать огромный Containerfile (да, можно взять мой и подправить, но все же). Думаю, в данном случае jail подходит намного лучше, уж в нее-то никакой проблемы накатить PostgreSQL нет. Так что возможно я когда-нибудь напишу скрипт для CBSD (команды, наверное, в немалой степени те же).

Что ж, посмотрим, как будет развиваться Podman на FreeBSD и насколько востребованными окажутся мои наработки. И напоследок позвольте напомнить вам ссылки на репозитории:


Категория: Программирование, веб | Опубликовано 20.08.2025

Похожие материалы

Образ PHP для Podman во FreeBSD

При знакомстве с комбинацией Podman + FreeBSD я набросал Containerfile для PHP. Сейчас же я решил довести дело до ума и сделать образ, максимально приближенный к официальному в варианте FPM. Вынужден сразу предупредить, что интерпретатор, как и до этого, будет установлен с помощью пакетного менеджера, в результате невозможно будет гарантировать его точную версию.

Веб-сервер на FreeBSD с использованием клеток

Здесь вам не Докер, а клетки (jails) - будем говорить, это контейнеры FreeBSD, когда это еще не было мейнстримом (на минуточку, они появились еще во FreeBSD 4.x - 2000 год). Практический смысл в моем случае - неким образом изолированно использовать разные версии PHP, ну и чуть ближе познакомиться с технологией, с которой я уже сталкивался при обзоре TrueNAS. Основано, как говорится, на реальных событиях - я переносил сайты на Drupal 7.x и Yii с сервера на Linux.


Комментарии, обсуждение