При знакомстве с комбинацией Podman + FreeBSD я набросал Containerfile для PHP. Сейчас же я решил довести дело до ума и сделать образ, максимально приближенный к официальному в варианте FPM. Вынужден сразу предупредить, что интерпретатор, как и до этого, будет установлен с помощью пакетного менеджера, в результате невозможно будет гарантировать его точную версию.
В предыдущей серии
В виртуальной машине VirtualBox установил FreeBSD 14.3 и Podman по официальной инструкции. Выяснилось, что на UFS держать это дело совершенно непрактично, ибо жрет место как не в себя, поэтому для /var/db/containers создал датасет ZFS. Еще, помимо всего прочего, у меня не заработало «обнаружение сервисов», т.е. разыменовывание хостов. Жаль, ведь с помощью эмуляции Linux можно запустить Caddy в контейнере.
Containerfile (он же Dockerfile, только для Podman) у меня получился вот такой:
FROM docker.io/freebsd/freebsd-runtime:14.3
# Install software
ENV ASSUME_ALWAYS_YES=yes
RUN set -eux; \
pkg bootstrap; \
pkg install \
php84 \
php84-composer \
php84-dom \
php84-gd \
php84-iconv \
php84-opcache \
php84-pdo_sqlite \
php84-session \
php84-simplexml \
php84-tokenizer \
php84-xml \
unzip; \
pkg clean -a
# Application directory
RUN set -eux; \
[ ! -d /usr/local/www/html ]; \
mkdir -p /usr/local/www/html; \
chmod 755 /usr/local/www; \
chmod 1777 /usr/local/www/html
WORKDIR /usr/local/www/html
# Use the default production configuration
RUN set -eux; \
cp /usr/local/etc/php.ini-production /usr/local/etc/php.ini; \
chmod 644 /usr/local/etc/php.ini
CMD ["php", "-a"]
Ну такое, согласитесь. Сейчас мы будем это безобразие приводить в порядок.
Проектирование
Итак, я хочу максимально близко повторить официальный образ php:8.4-fpm по концепции в целом и по составу расширений в частности, но при этом и учесть специфику FreeBSD. Поэтому первым делом выкидываем из Containerfile переменную окружения ASSUME_ALWAYS_YES
и установку composer с unzip.
Расширения PHP
Согласуем их. Честно говоря, мне практически всегда официальных не хватает и приходится устанавливать, к примеру, gd с intl, но в сторону личные предпочтения. Там, где есть Docker, например в Arch Linux, смотрим:
docker run --rm php:8.4-fpm php -m
[PHP Modules]
Core
ctype
curl
date
dom
fileinfo
filter
hash
iconv
json
libxml
mbstring
mysqlnd
openssl
pcre
PDO
pdo_sqlite
Phar
posix
random
readline
Reflection
session
SimpleXML
sodium
SPL
sqlite3
standard
tokenizer
xml
xmlreader
xmlwriter
Zend OPcache
zlib
[Zend Modules]
Zend OPcache
Практически все из них соответствуют отдельным пакетам во FreeBSD. mysqlnd, забегая вперед, входит в базовую поставку. Следовательно, команда установки пакетов будет такой:
pkg install -y \
php84 \
php84-ctype \
php84-curl \
php84-dom \
php84-fileinfo \
php84-filter \
php84-iconv \
php84-mbstring \
php84-opcache \
php84-pdo_sqlite \
php84-phar \
php84-posix \
php84-readline \
php84-session \
php84-simplexml \
php84-sodium \
php84-sqlite3 \
php84-tokenizer \
php84-xml \
php84-xmlreader \
php84-xmlwriter \
php84-zlib
Теоретически php84 как таковой в любом случае установится как зависимость, но лучше пусть будет указан явно.
Установка в образе
Внесем еще несколько модификаций. Во-первых, предлагаю сделать возможность передавать версии FreeBSD и PHP через аргументы. Во-вторых, вместо ежеквартального репозитория (по умолчанию) скорее всего будет лучше использовать Latest, чтобы все было максимально актуальным. Наконец, помимо очистки кэша пакетов, можно смело удалить базы репозиториев – это еще сэкономит порядка 70-80 мегабайт в конечном образе.
ARG FREEBSD=14.3
FROM docker.io/freebsd/freebsd-runtime:$FREEBSD
# Install software
ARG PHP=php84
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 \
${PHP} \
${PHP}-ctype \
${PHP}-curl \
${PHP}-dom \
${PHP}-fileinfo \
${PHP}-filter \
${PHP}-iconv \
${PHP}-mbstring \
${PHP}-opcache \
${PHP}-pdo_sqlite \
${PHP}-phar \
${PHP}-posix \
${PHP}-readline \
${PHP}-session \
${PHP}-simplexml \
${PHP}-sodium \
${PHP}-sqlite3 \
${PHP}-tokenizer \
${PHP}-xml \
${PHP}-xmlreader \
${PHP}-xmlwriter \
${PHP}-zlib; \
# cleanup to reduce image size
pkg clean -ay; \
rm -rf /var/db/pkg/repos
Настройка PHP
Как и до этого, создадим директорию приложения. С учетом специфики FreeBSD пусть будет /usr/local/www/html:
# Application directory
RUN set -eux; \
[ ! -d /usr/local/www/html ]; \
mkdir -p /usr/local/www/html; \
chmod 755 /usr/local/www; \
chmod 1777 /usr/local/www/html
COPY --chmod=644 --chown=80:80 html/index.php /usr/local/www/html/
Я решил подобно образу FrankenPHP сразу включить тестовый скрипт, выводящий информацию о PHP – файл html/index.php:
<?php
phpinfo();
Напоминаю, что chown в базовом образе почему-то отсутствует, поэтому владельца явно указал в параметрах COPY
.
Теперь настраиваем PHP-FPM. Здесь я тоже решил файлы предсоздать, а не возиться с echo | tee
.
# Configure
ENV PHP_INI_DIR /usr/local/etc
COPY etc/php-fpm.d/* ${PHP_INI_DIR}/php-fpm.d/
COPY etc/php/* ${PHP_INI_DIR}/php/
Полезность переменной окружения PHP_INI_DIR
в данном случае немного сомнительна, ведь во FreeBSD php.ini при установке из пакетов находится просто в /usr/local/etc. Но пусть живет, тем более я ей воспользовался в последующих командах, да и потом можно ничего не придумывать, а вставить RUN cp "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini"
.
Что же мы там такое копируем? etc/php-fpm.d/podman.conf:
[global]
error_log = /dev/stderr
log_limit = 8192
[www]
; php-fpm closes STDOUT on startup,
; so sending logs to it does not work.
access.log = /dev/stderr
clear_env = no
; Ensure worker stdout and stderr are sent to the main error log.
catch_workers_output = yes
decorate_workers_output = no
В основном мы здесь включаем и перенаправляем логи, их можно будет видеть прямо на экране в случае podman-compose up
или через podman logs
.
etc/php-fpm.d/zz-podman.conf:
[global]
daemonize = no
[www]
listen = 0.0.0.0:9000
Отключаем «демонизацию», т.е. запуск процесса в фоне, и включаем прослушивание на всех адресах (IP-адрес хоста с точки зрения контейнера заранее неизвестен). Так как PHP обрабатывает конфигурационные файлы в алфавитном порядке, эти два файла обрамляют стандартный www.conf, тем самым дополняя и переопределяя его настройки соответственно.
Также отключаем логирование fastcgi, файл etc/php/podman-fpm.ini:
fastcgi.logging = Off
Если что, все это не я сам придумал, а авторы официального образа. Я только файлы назвал более подходящим образом и чутка подправил (например во FreeBSD нет /proc/self/fd/2, это всего лишь /dev/stderr или /dev/fd/2).
Настройка контейнера
Точку входа в контейнер я вновь взял из официального образа и переименовал:
RUN chmod 755 /usr/local/sbin
COPY --chmod=755 podman-php-entrypoint /usr/local/sbin/
ENTRYPOINT ["podman-php-entrypoint"]
WORKDIR /usr/local/www/html
Файл podman-php-entrypoint:
#!/bin/sh
set -e
# first arg is `-f` or `--some-option`
if [ "${1#-}" != "$1" ]; then
set -- php-fpm "$@"
fi
exec "$@"
Рабочая директория у нас чуть отличается от Linux, а остальные настройки образа идентичны натуральному официальному:
# Override stop signal to stop process gracefully
STOPSIGNAL SIGQUIT
EXPOSE 9000
CMD ["php-fpm"]
Вроде готово.
Итого
Мастер-процесс php-fpm запустится под root, а дочерние, благодаря конфигурации по умолчанию, под www. Опять же, все практически как в официальном образе. Если же делать потом некий rootless-вариант, то как минимум нужно будет поменять права доступа к /var/run, а как максимум – еще и к файлам конфигурации.
Containerfile:
ARG FREEBSD=14.3
FROM docker.io/freebsd/freebsd-runtime:$FREEBSD
# Install software
ARG PHP=php84
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 \
${PHP} \
${PHP}-ctype \
${PHP}-curl \
${PHP}-dom \
${PHP}-fileinfo \
${PHP}-filter \
${PHP}-iconv \
${PHP}-mbstring \
${PHP}-opcache \
${PHP}-pdo_sqlite \
${PHP}-phar \
${PHP}-posix \
${PHP}-readline \
${PHP}-session \
${PHP}-simplexml \
${PHP}-sodium \
${PHP}-sqlite3 \
${PHP}-tokenizer \
${PHP}-xml \
${PHP}-xmlreader \
${PHP}-xmlwriter \
${PHP}-zlib; \
# cleanup to reduce image size
pkg clean -ay; \
rm -rf /var/db/pkg/repos
# Application directory
RUN set -eux; \
[ ! -d /usr/local/www/html ]; \
mkdir -p /usr/local/www/html; \
chmod 755 /usr/local/www; \
chmod 1777 /usr/local/www/html
COPY --chmod=644 --chown=80:80 html/index.php /usr/local/www/html/
# Configure
ENV PHP_INI_DIR /usr/local/etc
COPY etc/php-fpm.d/* ${PHP_INI_DIR}/php-fpm.d/
COPY etc/php/* ${PHP_INI_DIR}/php/
RUN chmod 755 /usr/local/sbin
COPY --chmod=755 podman-php-entrypoint /usr/local/sbin/
ENTRYPOINT ["podman-php-entrypoint"]
WORKDIR /usr/local/www/html
# Override stop signal to stop process gracefully
STOPSIGNAL SIGQUIT
EXPOSE 9000
CMD ["php-fpm"]
Проверяем?
Сборка и проверка
Можно (или даже скорее нужно) воспользоваться podman build
, но мне больше нравится композиция. Например:
services:
php:
build: .
restart: always
ports:
- "127.0.0.1:9000:9000"
Запускаем:
podman-compose up -d --build
Для проверки можно было бы установить на хост Caddy, но я хочу показать вам другой способ – программой cgi-fcgi:
pkg install www/fcgi
SCRIPT_NAME=/index.php \
SCRIPT_FILENAME=/usr/local/www/html/index.php \
REQUEST_METHOD=GET \
cgi-fcgi -bind -connect 127.0.0.1:9000
Команда должна вывести в консоль html-документ (можно перенаправить вывод в файл).
Публикация на hub.docker.com
Для публикации нам потребуется идентификатор образа и авторизация. Смотрим список:
podman image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
localhost/freebsd_php latest dc17e55cea50 50 seconds ago 207 MB
docker.io/freebsd/freebsd-runtime 14.3 786f2592a8e1 2 months ago 33 MB
docker.io/dougrabson/hello latest f81c971736c6 3 years ago 4.06 MB
podman-compose автоматически помечает собранный образ, например localhost/freebsd_php:latest. В моем случае идентификатор этого образа dc17e55cea50.
Авторизуем Podman в docker.io:
podman login docker.io
Допустим, в Хабе есть репозиторий username/php. «Пушим» образ:
podman push dc17e55cea50 docker://docker.io/username/php:8.4-fpm-freebsd14.3-pkg
Такой вот сложный тег я придумал примерно по аналогии с официальными Apline'овскими. Суффикс pkg означает использование пакетного менеджера для установки.
Использование
Раскроем карты – я развил бурную деятельность и действительно опубликовал образ в Docker Hub, чтобы зря добру не пропадать, а еще в GitHub и даже на собственном GitLab.
- https://git.dmkos.ru/containers/php
- https://hub.docker.com/r/dmkos/php-freebsd
- https://github.com/dmkos/php-containers
В результате для загрузки образа можно использовать любой из реестров:
glcr.dmkos.ru/containers/php
- мойdocker.io/dmkos/php-freebsd
- Docker Hubghcr.io/dmkos/php
- GitHub
Тег 8.4-fpm-freebsd14.3-pkg
. Ниже слегка менее формальное описание, чем в GitLab, туда же ведут ссылки в примерах.
Конфигурация PHP
Это, на мой взгляд, недостаток официальных образов – нужно не забыть выбрать ту или иную конфигурацию (development, production). Я бы предустановил «продуктовую», но исходя из концепции, это следует сделать вам.
FROM glcr.dmkos.ru/containers/php:8.4-fpm-freebsd14.3-pkg
# Use the default production configuration
RUN cp "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini"
Работа под непривилегированным пользователем
Неприменима. На данный момент во FreeBSD Podman не поддерживает rootless mode. Теоретически, если настроить права доступа к файлам, можно запустить мастер-процесс под обычным пользователем, но в целом особой необходимости я в этом не вижу: пользователь дочерних процессов www
, а они-то и выполняют всю работу.
Запуск одиночных скриптов
Не думаю, что это основное предназначение контейнера, но почему бы и нет.
podman run -it --rm -v "$PWD":/usr/local/www/html glcr.dmkos.ru/containers/php:8.4-fpm-freebsd14.3-pkg php your-script.php
Например, hello.php:
<?php
echo 'Hello world!' . PHP_EOL;
podman run -it --rm -v "$PWD":/usr/local/www/html glcr.dmkos.ru/containers/php:8.4-fpm-freebsd14.3-pkg php hello.php
Hello world!
Установка расширений
Как и теперь уже в базовом образе, это делается через pkg. Проблема (или наоборот, преимущество) пакетного менеджера в том, что при установке расширения он скорее всего обновит весь PHP. С другой стороны, никаких проблем с зависимостями, если таковые нужны для расширений.
Допустим, нужно установить вышеупомянутые gd и intl:
FROM glcr.dmkos.ru/containers/php:8.4-fpm-freebsd14.3-pkg
# Install PHP extensions
RUN set -eux; \
pkg install -y \
php84-gd \
php84-intl; \
pkg clean -ay; \
rm -rf /var/db/pkg/repos
Установка composer
В данном случае его скорее всего тоже стоит устанавливать через менеджер пакетов, поскольку можно заодно подтянуть unzip в помощь:
FROM glcr.dmkos.ru/containers/php:8.4-fpm-freebsd14.3-pkg
# Install composer and unzip
RUN set -eux; \
pkg install -y \
php84-composer \
unzip; \
pkg clean -ay; \
rm -rf /var/db/pkg/repos
Веб-сервер
Пока не решена проблема с обнаружением сервисов, придется устанавливать сервер на хост. 9000-й порт нельзя открывать наружу. Пример compose.yaml для PHP-FPM:
services:
php:
image: glcr.dmkos.ru/containers/php:8.4-fpm-freebsd14.3-pkg
restart: always
ports:
- "127.0.0.1:9000:9000"
volumes:
- ./myapp:/usr/local/www/html
Или соответствующая команда:
podman run -d -p "127.0.0.1:9000":9000 --name php -v "$PWD/myapp":/usr/local/www/html --restart always glcr.dmkos.ru/containers/php:8.4-fpm-freebsd14.3-pkg
В качестве веб-сервера я рекомендую Caddy. Установка:
pkg install caddy security/portacl-rc
sysrc portacl_users+=www
sysrc portacl_user_www_tcp="http https"
sysrc portacl_user_www_udp="https"
service portacl enable
service portacl start
sysrc caddy_user=www caddy_group=www
service caddy enable
Базовая конфигурация:
{
email admin@example.com
}
example.com {
root * /usr/local/www/myapp/public
encode zstd br gzip
php_fastcgi 127.0.0.1:9000 {
root /usr/local/www/html/public
}
file_server
}
Запуск:
service caddy start
Выявленные проблемы
Не работает обновление пакетов в контейнере - pkg upgrade -y
, ошибка вида:
Предположительно это связано с тем, что /etc/hosts находится в слое образа freebsd-runtime, а команда обновления работает в своем, и то ли Podman, то ли ZFS тупит.
Еще в репозитории есть пример композиции с Caddy, иллюстрирующий проблему с обнаружением сервисов. Подробно я разбирал эту ситуацию в обзоре Podman.
Заключение
В целом, я считаю, поставленная задача достигнута. Получившийся образ очень похож на официальный, но при этом является родным для FreeBSD. Основной недостаток – из-за применения менеджера пакетов для установки PHP на практике нельзя гарантировать точную версию PHP. Я выгрузил образ с PHP 8.4.11, но в случае когда позже выйдет другая версия, а вы дополните образ другими расширениями через pkg install, версия PHP обновится, допустим, до 8.4.12. С другой стороны, это же и достоинство, поскольку не нужно заботиться о внешних зависимостях. И ждем, когда контейнеры смогут обращаться друг к другу по именам, иначе мало-мальски сложное приложение в классическом виде не развернуть.
Категория: Программирование, веб | Опубликовано 12.08.2025 | Редакция от 17.08.2025
Похожие материалы
Компиляция FrankenPHP во FreeBSD. Контейнер Podman
Воодушевленный созданием контейнера PHP для FreeBSD, я поставил перед собой более амбициозную цель: скомпилировать FrankenPHP. Напомню, что это веб-сервер на базе Caddy, который взаимодействует с PHP как с библиотекой и помимо этого предоставляет еще ряд различных оптимизаций. В целом все получилось, но, как говорится, есть нюансы.
Веб-сервер на FreeBSD с использованием клеток
Здесь вам не Докер, а клетки (jails) - будем говорить, это контейнеры FreeBSD, когда это еще не было мейнстримом (на минуточку, они появились еще во FreeBSD 4.x - 2000 год). Практический смысл в моем случае - неким образом изолированно использовать разные версии PHP, ну и чуть ближе познакомиться с технологией, с которой я уже сталкивался при обзоре TrueNAS. Основано, как говорится, на реальных событиях - я переносил сайты на Drupal 7.x и Yii с сервера на Linux.