Руководство по Docker

OxPHP работает как контейнер. Это руководство описывает сборку, настройку и запуск в Docker — от минимального одноэтапного образа до многоэтапной конфигурации с отдельными таргетами для разработки и продакшена.

Минимальный Dockerfile

Самый простой способ упаковать приложение в контейнер:

Dockerfile
FROM ghcr.io/oxphp/oxphp:0.10.0 COPY --chown=www-data:www-data . /var/www/html/public

Так приложение копируется в контейнер. По умолчанию DOCUMENT_ROOT равен /var/www/html/public. Для Laravel, Symfony или любого проекта, который уже содержит подкаталог public/, используйте вместо этого COPY --chown=www-data:www-data . /var/www/html, чтобы собственный public/ фреймворка совпал со значением по умолчанию. По умолчанию сервер слушает порт 80.

Многоэтапный Dockerfile

Для реальных приложений используйте многоэтапный Dockerfile с отдельными таргетами dev и prod. Таргет dev включает PHP CLI, Composer и Xdebug. Таргет prod строится на минимальном образе OxPHP и содержит только то, что нужно в продакшене.

Tip

Готовая к использованию версия этого Dockerfile лежит в репозитории по пути examples/dockerfile/Dockerfile. Скопируйте её в свой проект и скорректируйте набор расширений под свои нужды.

Dockerfile
# ── Stage: php-base — shared PHP extensions ────────────────── FROM php:8.4-zts-alpine3.23 AS php-base RUN apk add --no-cache \ icu-dev \ icu-libs \ postgresql-dev \ libpq \ && docker-php-ext-install \ pdo \ pdo_mysql \ pdo_pgsql \ intl \ && apk del icu-dev postgresql-dev # ── Stage: php-dev — add Xdebug on top of base ─────────────── FROM php-base AS php-dev RUN apk add --no-cache $PHPIZE_DEPS linux-headers \ && pecl install xdebug \ && docker-php-ext-enable xdebug \ && apk del $PHPIZE_DEPS linux-headers # ── Stage: composer ─────────────────────────────────────────── FROM composer:2 AS composer # ── Stage: oxphp — pull OxPHP artifacts ────────────────────── FROM ghcr.io/oxphp/oxphp:0.10.0 AS oxphp # ── Target: dev ────────────────────────────────────────────── # Includes: PHP CLI, Composer, Xdebug, OxPHP binary + extension FROM php-dev AS dev RUN apk add --no-cache libgcc # Composer COPY --from=composer /usr/bin/composer /usr/local/bin/composer # OxPHP binary COPY --from=oxphp /usr/local/bin/oxphp /usr/local/bin/oxphp # Bridge library COPY --from=oxphp /usr/local/lib/liboxphp_bridge.so /usr/local/lib/ # OxPHP PHP extension RUN EXT_DIR=$(php -r 'echo ini_get("extension_dir");') && \ echo "$EXT_DIR" > /tmp/ext_dir COPY --from=oxphp /usr/local/lib/php/extensions/ /tmp/oxphp-ext/ RUN cp /tmp/oxphp-ext/*/oxphp_sapi.so "$(cat /tmp/ext_dir)/" && \ rm -rf /tmp/oxphp-ext /tmp/ext_dir # PHP config RUN echo "extension=oxphp_sapi.so" > /usr/local/etc/php/conf.d/oxphp-ext.ini # Dev-friendly OPcache (validates timestamps) RUN { \ echo "[opcache]"; \ echo "opcache.enable=1"; \ echo "opcache.enable_cli=1"; \ echo "opcache.validate_timestamps=1"; \ echo "opcache.revalidate_freq=0"; \ } > /usr/local/etc/php/conf.d/opcache-dev.ini # Xdebug — connect back to host RUN { \ echo "[xdebug]"; \ echo "xdebug.mode=debug"; \ echo "xdebug.start_with_request=trigger"; \ echo "xdebug.client_host=host.docker.internal"; \ echo "xdebug.client_port=9003"; \ } > /usr/local/etc/php/conf.d/xdebug-config.ini RUN adduser -D -H -u 82 -G www-data -s /sbin/nologin www-data 2>/dev/null || true RUN mkdir -p /var/www/html/public && chown -R www-data:www-data /var/www/html ENV LD_LIBRARY_PATH=/usr/local/lib # Framework layout: project ships a public/ subdir (Laravel/Symfony/Slim). # For a bare index.php at the project root, copy into /var/www/html/public instead. COPY --chown=www-data:www-data . /var/www/html EXPOSE 80 443 CMD ["oxphp"] # ── Stage: prod-extensions — compile extensions for prod ───── FROM php-base AS prod-extensions RUN EXT_DIR=$(php -r 'echo ini_get("extension_dir");') && \ mkdir -p /ext-out && \ cp "$EXT_DIR"/pdo.so \ "$EXT_DIR"/pdo_mysql.so \ "$EXT_DIR"/pdo_pgsql.so \ "$EXT_DIR"/intl.so \ /ext-out/ # ── Target: prod — minimal, based on OxPHP image ───────────── FROM oxphp AS prod USER root RUN apk add --no-cache icu-libs libpq COPY --from=prod-extensions /ext-out/*.so /usr/local/lib/php/extensions/no-debug-zts-20240924/ RUN { \ echo "extension=pdo_mysql.so"; \ echo "extension=pdo_pgsql.so"; \ echo "extension=intl.so"; \ } > /usr/local/etc/php/conf.d/app-extensions.ini # Framework layout: project ships a public/ subdir (Laravel/Symfony/Slim). # For a bare index.php at the project root, copy into /var/www/html/public instead. COPY --chown=www-data:www-data . /var/www/html USER www-data EXPOSE 80 443 CMD ["oxphp"]

Соберите каждый таргет:

bash
# Development image (includes PHP CLI, Composer, Xdebug) docker build --target dev -t myapp:dev . # Production image (minimal) docker build --target prod -t myapp:prod .
Note

Таргет dev основан на php:8.4-zts-alpine (замените 8.4 на 8.5, чтобы соответствовать тегу OxPHP :*-php8.5*) с добавленным OxPHP, поэтому вы получаете полный доступ к PHP CLI и Composer. Таргет prod основан непосредственно на образе OxPHP, что позволяет держать продакшен-образ компактным.

Установка PHP-расширений в продакшене

Начиная с OxPHP 0.3.0, продакшен-образ поставляется с полным набором инструментов PHP (php, docker-php-ext-install, phpize), унаследованным от php:8.4-zts-alpine (или php:8.5-zts-alpine для вариантов :*-php8.5*), и не задаёт директиву USER. Дочерние Dockerfile могут устанавливать PHP-расширения напрямую. Переключать USER не требуется.

Шаблон для быстрого старта (один этап)

Самый короткий полезный пример:

Dockerfile
FROM ghcr.io/oxphp/oxphp:0.10.0 RUN docker-php-ext-install mysqli pdo_mysql COPY --chown=www-data:www-data . /var/www/html/public CMD ["oxphp"]

--chown=www-data:www-data в COPY важен: внутри образа файлы принадлежат www-data (uid 82), поэтому переключение на --user www-data на уровне оркестратора приходится на webroot, который непривилегированный процесс может читать и (где нужно) записывать.

Контейнер запускается от root, а затем oxphp serve по умолчанию переключается на www-data до того, как начнёт обслуживать трафик (см. примечание о безопасности ниже). Явно закрепите идентификатор на уровне оркестратора, если вам нужен конкретный uid или пользователь, отличный от www-data.

Рекомендуемый шаблон (два этапа, меньший образ)

Для получения максимально компактного итогового образа компилируйте расширения в отдельном сборочном этапе и копируйте в рантайм-этап только скомпилированные файлы .so. Разбор многоэтапного Dockerfile выше использует FROM ghcr.io/oxphp/oxphp:0.10.0 AS prod — просто, переносимо и рекомендуется как отправная точка.

examples/dockerfile/Dockerfile в репозитории идёт дальше: его таргет prod основан на чистом alpine с явным списком зависимостей apk и копирует только бинарник oxphp, libphp.so, скомпилированные PHP-расширения и необходимые разделяемые библиотеки. Это уменьшает базовый образ с ~188 МБ до ~76 МБ (сокращение на ~60%, без учёта кода приложения) ценой отслеживания обновлений версий PHP/Alpine в списке apk. Тот же файл содержит и таргет prod-cli — недолговечный образ для php artisan migrate, Composer и других команд обслуживания, которым не место на пути обработки запросов.

Note

Разбор выше по-прежнему показывает явные переключения USER root / USER www-data для эшелонированной защиты. Начиная с v0.3.0 они необязательны, поскольку базовый образ больше не задаёт USER.

Запуск CLI-инструментов и миграций

Тот же продакшен-образ может выполнять CLI-команды php для миграций, Composer или разовой проверки. docker run заменяет CMD по умолчанию на переданную вами команду: контейнер выполняет команду и завершается, но не запускает при этом сервер OxPHP.

bash
# Run Laravel migrations against the prod image. # Container runs as root by default — the CLI has write access to # root-owned mounted volumes. docker run --rm \ -v "$(pwd):/var/www/html" \ ghcr.io/oxphp/oxphp:0.10.0 \ php artisan migrate # If the mounted volume is owned by www-data, pass Docker's --user: docker run --rm --user www-data \ -v "$(pwd):/var/www/html" \ ghcr.io/oxphp/oxphp:0.10.0 \ php artisan migrate

docker exec <container> docker-php-ext-install <ext> также работает на запущенном контейнере без каких-либо дополнительных флагов — удобно для отладки живого контейнера. Для продакшена зафиксируйте расширение в своём Dockerfile, чтобы оно сохранялось между перезапусками.

Примечание о безопасности

Продакшен-образ не содержит директивы USER, поэтому контейнер запускается от root (в соответствии с соглашениями nginx:alpine / php:*-fpm-alpine / frankenphp:alpine). Запуск от root позволяет OxPHP занимать привилегированные порты, но больше не означает, что трафик обслуживается от root: oxphp serve и oxphp run по умолчанию переключаются на www-data, привязываются от root, а затем окончательно сбрасывают привилегии до обработки любого запроса и до запуска любого PHP-воркера. В официальном образе (в котором есть учётная запись www-data) это работает из коробки, без настройки оркестратора.

Вы всё же можете явно закрепить идентификатор рантайма на уровне оркестратора — это рекомендуется, когда вам нужен конкретный uid, дополнительная эшелонированная защита или пользователь, отличный от www-data:

bash
docker run --user www-data ghcr.io/oxphp/oxphp:0.10.0

Когда вы запускаете контейнер как non-root таким образом, OxPHP уже непривилегирован и стандартный автосброс привилегий ничего не делает, но тогда процесс не может занимать порты ниже 1024 (см. Запуск от non-root на порту 80, чтобы сохранить привилегированную привязку и обслуживание от non-root). Чтобы намеренно продолжать обслуживание от root, передайте oxphp serve --user=root.

Пользователь www-data (uid 82, gid 82) заранее создан базовым образом, а /var/www/html передан ему во владение (chown) на этапе сборки, поэтому любой из этих вариантов сброса привилегий — включая стандартный автосброс — приходится на читаемый webroot.

Note

CLI-вызовы вроде docker run … php artisan migrate запускают бинарник php напрямую, а не oxphp serve/run, поэтому автосброса привилегий не происходит; они выполняются от стартового пользователя контейнера (по умолчанию root). Для них используйте --user в Docker, как показано выше.

Запуск от non-root на порту 80 (serve --user)

У сброса привилегий на уровне оркестратора (выше) есть одно ограничение: процесс, запущенный от www-data, не может занять привилегированный порт (ниже 1024). Чтобы обслуживать :80/:443, иначе понадобился бы CAP_NET_BIND_SERVICE, точка входа в стиле su-exec или высокий порт (например, :8080) за пробросом портов.

OxPHP сводит это к одному процессу: он привязывает слушатели от root, а затем окончательно сбрасывает привилегии до того, как будет принято хоть одно соединение или запущен хоть один PHP-воркер. Вы получаете привилегированный порт и обработку запросов от non-root без дополнительных capabilities. По умолчанию сброс происходит на пользователя www-data, поэтому в официальном образе достаточно запустить контейнер от root, чтобы получить привилегированную привязку, обслуживаемую от www-data, — без каких-либо флагов. Используйте --user=<spec>, только чтобы переключиться на другого пользователя; используйте --user=root, чтобы остаться root.

Запускайте контейнер от root — не задавайте при этом user:, потому что для привязки нужен root. В примере ниже --user=www-data передаётся явно ради самодокументирования, но это совпадает со значением по умолчанию:

compose.yaml
services: oxphp: image: ghcr.io/oxphp/oxphp:0.10.0 command: ["oxphp", "serve", "--user=www-data"] ports: - "80:80" - "443:443" environment: - LISTEN_ADDR=0.0.0.0:80

<spec> принимает имя пользователя, name:group, числовой uid или uid:gid. Сброс выполняет initgroups → setgid → setuid, проверяет, что root уже не вернуть, и необратим; на Linux он также устанавливает no_new_privs. Явный --user работает по принципу fail-fast: если процесс запущен не от root, serve --user завершается с ошибкой, а не продолжает молча работать от root. (А вот стандартный сброс работает по принципу best-effort — при запуске от non-root он просто пропускается, поскольку сбрасывать нечего.)

Warning

Выбирайте одну модель, а не обе. Используйте сброс на уровне оркестратора (user: / runAsUser), когда :80 терминируется высоким портом или внешним балансировщиком нагрузки. Используйте serve --user, когда хотите, чтобы привилегированную привязку держал сам OxPHP. Если задать user: и serve --user, привязка провалится: контейнер уже не root.

Чек-лист прав доступа к файлам для пользователя после сброса. После сброса всё, к чему OxPHP обращается во время работы, должно быть доступно для <spec>:

Ресурс Требование
DOCUMENT_ROOT Должен быть читаем. /var/www/html передаётся во владение www-data при сборке образа, поэтому по умолчанию это условие выполнено.
Путь сохранения сессий (session.save_path, по умолчанию /tmp) Должен быть доступен для записи.
Временный каталог загрузок (upload_tmp_dir) Должен быть доступен для записи, если используется загрузка файлов.
Файловый кеш OPcache (opcache.file_cache) Должен быть доступен для записи, если включён вторичный файловый кеш.
Файловый журнал доступа Должен быть доступен для записи, если журналирование идёт в файл, а не в stdout.
Приватный ключ TLS (TLS_KEY) Должен быть читаем пользователем после сброса — доступен на чтение группе или всем, а не только root с правами 0600. Ключ читается после сброса, поэтому доступный только root ключ приведёт к сбою запуска TLS.

Docker Compose

compose.yaml
services: oxphp: build: context: . target: prod ports: - "80:80" - "443:443" - "9090:9090" environment: - LISTEN_ADDR=0.0.0.0:80 - DOCUMENT_ROOT=/var/www/html/public - ENTRY_FILE=index.php - INTERNAL_ADDR=0.0.0.0:9090 - LOG_LEVEL=info - ACCESS_LOG=error - PHP_WORKERS=4 - DRAIN_TIMEOUT_SECONDS=25 - COMPRESSION_LEVEL=4 restart: unless-stopped

Монтирование томов

Путь на хосте Путь в контейнере Назначение
./src /var/www/html Файлы приложения (PHP-скрипты, статические ресурсы). В продакшене используйте :ro
./custom.ini /usr/local/etc/php/conf.d/custom.ini Конфигурация рантайма PHP (OPcache, сессии, JIT). Используйте :ro
./certs /etc/ssl/oxphp Сертификат TLS и приватный ключ. Используйте :ro

Справочник портов

Порт Переменная окружения Назначение
80 LISTEN_ADDR Основной HTTP-сервер
443 LISTEN_ADDR Основной HTTPS-сервер (когда настроен TLS)
9090 INTERNAL_ADDR Внутренний сервер: /health, /metrics, /config
Note

Внутренний сервер по умолчанию отключён. Чтобы включить его, задайте INTERNAL_ADDR. В продакшене оставляйте внутренний порт доступным только для вашего оркестратора или системы мониторинга; не открывайте его публично.

Конфигурация PHP

Настройте параметры PHP, создав файл custom.ini и смонтировав его в контейнер. Это рекомендуемый способ конфигурации OPcache, JIT, сессий и других параметров рантайма PHP.

custom.ini
; Do NOT add zend_extension=opcache — OPcache is already compiled into the ; PHP ZTS base image. The [opcache] section below configures it directly. [opcache] opcache.enable = 1 opcache.enable_cli = 1 opcache.memory_consumption = 128 opcache.interned_strings_buffer = 16 opcache.max_accelerated_files = 10000 opcache.validate_timestamps = 0 opcache.jit_buffer_size = 64M opcache.jit = tracing [Session] session.save_path = /tmp session.use_cookies = 1 session.use_only_cookies = 1
Note

Не добавляйте zend_extension=opcache в этот файл. OPcache уже встроен в образ PHP ZTS, используемый OxPHP. Строка zend_extension будет выдавать предупреждение при старте обработки каждого запроса.

В разработке задайте opcache.validate_timestamps = 1 и opcache.revalidate_freq = 0, чтобы PHP подхватывал изменения файлов без перезапуска контейнера.

См. OPcache для рекомендуемых настроек и конфигурации JIT.

Проверки работоспособности

Добавьте проверку работоспособности Docker, чтобы Docker или ваш оркестратор мог отслеживать состояние контейнера. Для этого требуется, чтобы был задан INTERNAL_ADDR.

compose.yaml
services: oxphp: environment: - INTERNAL_ADDR=0.0.0.0:9090 healthcheck: test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:9090/health"] interval: 10s timeout: 5s retries: 3 start_period: 5s

Эндпоинт /health возвращает 200, когда сервер здоров, и 503, когда работает с деградацией. JSON ответа включает время работы, общее число запросов и число активных соединений. Для Kubernetes используйте этот же эндпоинт и как liveness-, и как readiness-пробу.

Что дальше