Посібник із Docker

OxPHP працює як контейнер. У цьому посібнику описано, як збирати, налаштовувати й запускати його за допомогою Docker: від мінімального одноетапного образу до багатоетапної конфігурації з окремими цілями для розробки та продакшну.

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

Найпростіший спосіб контейнеризувати ваш застосунок:

Dockerfile
FROM ghcr.io/oxphp/oxphp:0.13.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. Типово вона використовує PHP 8.4 (ARG PHP_VERSION=8.4), тож збирайте її з --build-arg PHP_VERSION=8.5, щоб вона відповідала покроковому прикладу нижче. Скопіюйте її у свій проєкт і налаштуйте розширення під свої потреби.

Dockerfile
# ── Stage: php-base — shared PHP extensions ────────────────── FROM php:8.5-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.13.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-20250925/ 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.5-zts-alpine, що відповідає типовому тегу :0.13.0 (щоб використати тег OxPHP :*-php8.4* в етапі oxphp, замініть 8.5 на 8.4 у php-base і no-debug-zts-20250925 на no-debug-zts-20240924 у цілі prod; з тегом -trixie базуйте етапи на php:<minor>-zts-trixie і використовуйте apt-get, див. Образ Debian (glibc)), з OxPHP, скопійованим усередину, тож ви маєте повний доступ до PHP CLI і Composer. Ціль prod базується безпосередньо на образі OxPHP, завдяки чому продакшн-образ лишається компактним.

Встановлення розширень PHP у продакшні

Починаючи з OxPHP 0.3.0, продакшн-образ містить повний набір інструментів PHP (php, docker-php-ext-install, phpize), успадкований від php:8.5-zts-alpine (або php:8.4-zts-alpine для варіантів :*-php8.4*), і не задає директиву USER. Похідні Dockerfile можуть встановлювати розширення PHP напряму. Перемикати USER не потрібно.

Шаблон для швидкого старту (один етап)

Найкоротший корисний приклад:

Dockerfile
FROM ghcr.io/oxphp/oxphp:0.13.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, або 33 в образі Debian), тож після переходу на --user www-data на рівні оркестратора непривілейований процес отримує веб-корінь, який він може читати і (де потрібно) у який може записувати.

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

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

Щоб отримати якомога менший фінальний образ, компілюйте розширення в окремому етапі збирання і копіюйте в етап виконання лише скомпільовані файли .so. Покроковий приклад Багатоетапний Dockerfile вище використовує FROM ghcr.io/oxphp/oxphp:0.13.0 AS prod — це просто, переносно й рекомендовано як відправна точка.

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

Note

Покроковий приклад вище досі містить явні перемикання USER root / USER www-data для ешелонованого захисту. Починаючи з v0.3.0 вони необовʼязкові, оскільки базовий образ більше не задає USER.

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

Той самий продакшн-образ може виконувати команди php CLI для міграцій, 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.13.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.13.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.13.0

Коли ви в такий спосіб запускаєте контейнер не від root, OxPHP уже непривілейований і типове самостійне скидання привілеїв нічого не робить, але тоді процес не може привʼязатися до портів нижче 1024 (див. Запуск без root на порту 80, щоб зберегти привілейовану привʼязку і обслуговування не від root). Щоб свідомо лишити обслуговування від root, передайте oxphp serve --user=root.

Користувача www-data (uid/gid 82; 33 в образі Debian, де runAsUser і runAsGroup вище стають 33) заздалегідь створено в базовому образі, а власником /var/www/html його призначено під час збирання, тож будь-який із цих шляхів скидання привілеїв — зокрема й типове самостійне скидання — отримує доступний для читання веб-корінь.

Note

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

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

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

OxPHP зводить усе це до одного процесу: він привʼязує слухачі як root, а потім остаточно скидає привілеї до того, як буде прийнято будь-яке зʼєднання чи запущено будь-який PHP-воркер. Ви отримуєте привілейований порт і обробку запитів не від 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.13.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. (Типове скидання натомість виконується за можливості: якщо процес запущено не від 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) Доступний для читання користувачу після скидання привілеїв — для групи або для всіх, а не 0600 лише для root. Ключ читається після скидання, тож ключ, доступний лише 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_BROTLI_LEVEL=5 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 підхоплював зміни у файлах без перезапуску контейнера.

Рекомендовані налаштування та конфігурацію JIT див. у розділі OPcache.

Перевірки працездатності

Додайте перевірку працездатності 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 натомість використовуйте окремі ендпоінти для проб (/health/liveness, /health/readiness і /health/startup), кожен з яких розрахований на свій тип проби; див. Перевірки працездатності.

Промпти для вашого агента

Ці промпти розраховані на агента з підключеним MCP-сервером документації OxPHP — потрібні сторінки він прочитає сам.

prompt
Напиши мені Dockerfile і docker-compose.yml, які запускають цей проєкт на OxPHP. Прочитай сторінку `getting-started/docker` через MCP-сервер документації OxPHP. Збережи сервіси, від яких уже залежить мій застосунок, встанови ті розширення PHP, які йому справді потрібні, і поясни кожен специфічний для OxPHP рядок, який додаси.
prompt
Переглянь мій Dockerfile для запуску на OxPHP. Прочитай сторінку `getting-started/docker` через MCP-сервер документації OxPHP. Скажи, що зламається або марно займатиме місце в продакшні, і покажи виправлений файл.

Що далі