При знакомстве с комбинацией 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.

В результате для загрузки образа можно использовать любой из реестров:

  • glcr.dmkos.ru/containers/php - мой
  • docker.io/dmkos/php-freebsd - Docker Hub
  • ghcr.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, ошибка вида:

pkg: Fail to rename /etc/.pkgtemp.hosts.Y25WIgCpWhrB -> /etc/hosts:Cross-device link

Предположительно это связано с тем, что /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.


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