Руководство по Docker
OxPHP работает как контейнер. Это руководство описывает сборку, настройку и запуск в Docker — от минимального одноэтапного образа до многоэтапной конфигурации с отдельными таргетами для разработки и продакшена.
Минимальный 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 и содержит только то, что нужно в продакшене.
Готовая к использованию версия этого Dockerfile лежит в репозитории по пути examples/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"]Соберите каждый таргет:
# Development image (includes PHP CLI, Composer, Xdebug)
docker build --target dev -t myapp:dev .
# Production image (minimal)
docker build --target prod -t myapp:prod .Таргет 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 не требуется.
Шаблон для быстрого старта (один этап)
Самый короткий полезный пример:
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 и других команд обслуживания, которым не место на пути обработки запросов.
Разбор выше по-прежнему показывает явные переключения USER root / USER www-data для эшелонированной защиты. Начиная с v0.3.0 они необязательны, поскольку базовый образ больше не задаёт USER.
Запуск CLI-инструментов и миграций
Тот же продакшен-образ может выполнять CLI-команды php для миграций, Composer или разовой проверки. docker run заменяет CMD по умолчанию на переданную вами команду: контейнер выполняет команду и завершается, но не запускает при этом сервер OxPHP.
# 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 migratedocker 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:
docker run --user www-data ghcr.io/oxphp/oxphp:0.10.0services:
oxphp:
image: ghcr.io/oxphp/oxphp:0.10.0
user: www-datasecurityContext:
runAsNonRoot: true
runAsUser: 82
runAsGroup: 82runAsNonRoot: true — это эшелонированная защита: если runAsUser когда-нибудь удалят или переопределят на 0, kubelet отклонит под вместо того, чтобы молча запустить его от root.
Когда вы запускаете контейнер как 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.
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 передаётся явно ради самодокументирования, но это совпадает со значением по умолчанию:
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 он просто пропускается, поскольку сбрасывать нечего.)
Выбирайте одну модель, а не обе. Используйте сброс на уровне оркестратора (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
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Смонтируйте каталог с исходным кодом как том, чтобы изменения файлов отражались без пересборки. В таргете dev включена проверка временных меток OPcache, поэтому PHP подхватывает изменения автоматически.
services:
oxphp:
build:
context: .
target: dev
ports:
- "80:80"
- "9090:9090"
volumes:
- ./src:/var/www/html:ro
- ./custom.ini:/usr/local/etc/php/conf.d/custom.ini:ro
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=debug
- ACCESS_LOG=allМонтирование томов
| Путь на хосте | Путь в контейнере | Назначение |
|---|---|---|
./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 |
Внутренний сервер по умолчанию отключён. Чтобы включить его, задайте INTERNAL_ADDR. В продакшене оставляйте внутренний порт доступным только для вашего оркестратора или системы мониторинга; не открывайте его публично.
Конфигурация PHP
Настройте параметры PHP, создав файл custom.ini и смонтировав его в контейнер. Это рекомендуемый способ конфигурации OPcache, JIT, сессий и других параметров рантайма PHP.
; 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Не добавляйте zend_extension=opcache в этот файл. OPcache уже встроен в образ PHP ZTS, используемый OxPHP. Строка zend_extension будет выдавать предупреждение при старте обработки каждого запроса.
В разработке задайте opcache.validate_timestamps = 1 и opcache.revalidate_freq = 0, чтобы PHP подхватывал изменения файлов без перезапуска контейнера.
См. OPcache для рекомендуемых настроек и конфигурации JIT.
Проверки работоспособности
Добавьте проверку работоспособности Docker, чтобы Docker или ваш оркестратор мог отслеживать состояние контейнера. Для этого требуется, чтобы был задан INTERNAL_ADDR.
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: 5sHEALTHCHECK --interval=10s --timeout=5s --retries=3 --start-period=5s \
CMD wget --quiet --tries=1 --spider http://localhost:9090/health || exit 1Эндпоинт /health возвращает 200, когда сервер здоров, и 503, когда работает с деградацией. JSON ответа включает время работы, общее число запросов и число активных соединений. Для Kubernetes используйте этот же эндпоинт и как liveness-, и как readiness-пробу.
Что дальше
- Конфигурация — полный справочник переменных окружения
- Маршрутизация — режимы маршрутизации Traditional, Framework, SPA и Worker
- Режим воркеров — постоянные PHP-процессы для фреймворк-приложений
- TLS — HTTPS со встроенной терминацией TLS
- Проверки работоспособности — детали эндпоинта работоспособности и интеграция с Kubernetes
- Корректное завершение работы — поведение слива (drain) соединений и последовательность завершения работы