Компиляция FrankenPHP во FreeBSD. Контейнер Podman
Воодушевленный созданием контейнера 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:
- re2c>0 : devel/re2c
- pkgconf>=1.3.0_1 : devel/pkgconf
- autoconf>=2.72 : devel/autoconf
- automake>=1.17 : devel/automake
Library dependencies:
- libargon2.so : security/libargon2
- libpcre2-8.so : devel/pcre2
- 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 нет нужных зависимостей, то тогда… не будем о грустном.
На всякий случай, исходные коды моих скриптов на момент публикации статьи можно посмотреть в репозитории:
Что ж, переходим к основному блюду.
Компиляция 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
:
-I/usr/local/include
- подключает стандартный каталог FreeBSD для заголовочных файлов,-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
Уфф. Из отличий от рассмотренного выше – извлечение исходников из архива и их удаление в конце с использованием вспомогательного скрипта 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 строки. Позвольте мне не вставлять всю эту простыню целиком заново…
У себя в документации я оставил предупреждение о том, что этот образ нельзя использовать в боевом окружении. Причин тому как минимум две – лишние 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, возможно я и впрямь неправильно расположил скрипты…
Осталось подготовить контейнер для запуска под пользователем 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
: глобальные настройки CaddyFRANKENPHP_CONFIG
: настройка директивыfrankenphp
в глобальном блокеCADDY_EXTRA_CONFIG
: произвольная настройка Caddy, например фрагменты или редиректы с/на wwwSERVER_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
В общем, все замечательно, кроме мелочи, не влезающей ни в какие ворота. А именно – пока совершенно неясно, как быть с обнаружением сервисов. Повторюсь, расширения подключений к БД есть, а самих серверов – нет. Печаль…
Заключение
Развивая предыдущую мысль, у меня двойственные впечатления от проделанной работы. С одной стороны, все компилируется и на первый взгляд даже работает. Это вроде шутка, но только лишь с долей шутки: в идеале хотелось бы как следует все протестировать, но вот только мне неизвестен способ это сделать путем развертывания некоего специального веб-приложения. Демка Symfony, скажем, запускается, наблюдатель за изменениями файлов в ней наблюдает. Достаточно ли этого для далеко идущих выводов? Вряд ли.
С другой стороны, я отдаю себе отчет в том, что установка дополнительных расширений – это боль. Ради сокращения размеров образа и безопасности нужно писать огромный 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.