Посібник із Docker
OxPHP працює як контейнер. У цьому посібнику описано, як збирати, налаштовувати й запускати його за допомогою Docker: від мінімального одноетапного образу до багатоетапної конфігурації з окремими цілями для розробки та продакшну.
Мінімальний 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 і містить лише те, що потрібно в продакшні.
Готова до використання версія цього Dockerfile лежить у репозиторії за адресою examples/dockerfile/Dockerfile. Типово вона використовує PHP 8.4 (ARG PHP_VERSION=8.4), тож збирайте її з --build-arg PHP_VERSION=8.5, щоб вона відповідала покроковому прикладу нижче. Скопіюйте її у свій проєкт і налаштуйте розширення під свої потреби.
# ── 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"]Зберіть кожну ціль:
# 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.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 не потрібно.
Шаблон для швидкого старту (один етап)
Найкоротший корисний приклад:
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 та інших службових команд, яким не місце на шляху обслуговування запитів.
Покроковий приклад вище досі містить явні перемикання USER root / USER www-data для ешелонованого захисту. Починаючи з v0.3.0 вони необовʼязкові, оскільки базовий образ більше не задає USER.
Запуск CLI-інструментів і міграцій
Той самий продакшн-образ може виконувати команди php CLI для міграцій, 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.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 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.13.0services:
oxphp:
image: ghcr.io/oxphp/oxphp:0.13.0
user: www-datasecurityContext:
runAsNonRoot: true
runAsUser: 82
runAsGroup: 82runAsNonRoot: true — це ешелонований захист: якщо runAsUser колись видалять або перевизначать на 0, kubelet відхилить под замість того, щоб мовчки запустити його від root.
Коли ви в такий спосіб запускаєте контейнер не від root, OxPHP уже непривілейований і типове самостійне скидання привілеїв нічого не робить, але тоді процес не може привʼязатися до портів нижче 1024 (див. Запуск без root на порту 80, щоб зберегти привілейовану привʼязку і обслуговування не від root). Щоб свідомо лишити обслуговування від root, передайте oxphp serve --user=root.
Користувача www-data (uid/gid 82; 33 в образі Debian, де runAsUser і runAsGroup вище стають 33) заздалегідь створено в базовому образі, а власником /var/www/html його призначено під час збирання, тож будь-який із цих шляхів скидання привілеїв — зокрема й типове самостійне скидання — отримує доступний для читання веб-корінь.
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 передано явно для наочності, але це збігається з типовою поведінкою:
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, воно просто пропускається, бо скидати нічого.)
Оберіть одну модель, а не обидві. Використовуйте скидання на рівні оркестратора (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
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Змонтуйте каталог із вихідним кодом як том, щоб зміни у файлах підхоплювалися без перезбирання. У цілі 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 підхоплював зміни у файлах без перезапуску контейнера.
Рекомендовані налаштування та конфігурацію JIT див. у розділі OPcache.
Перевірки працездатності
Додайте перевірку працездатності 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 натомість використовуйте окремі ендпоінти для проб (/health/liveness, /health/readiness і /health/startup), кожен з яких розрахований на свій тип проби; див. Перевірки працездатності.
Промпти для вашого агента
Ці промпти розраховані на агента з підключеним MCP-сервером документації OxPHP — потрібні сторінки він прочитає сам.
Напиши мені Dockerfile і docker-compose.yml, які запускають цей проєкт на OxPHP. Прочитай сторінку `getting-started/docker` через MCP-сервер документації OxPHP.
Збережи сервіси, від яких уже залежить мій застосунок, встанови ті розширення PHP, які йому справді потрібні, і поясни кожен специфічний для OxPHP рядок, який додаси.Переглянь мій Dockerfile для запуску на OxPHP. Прочитай сторінку `getting-started/docker` через MCP-сервер документації OxPHP.
Скажи, що зламається або марно займатиме місце в продакшні, і покажи виправлений файл.Що далі
- Конфігурація — повний довідник змінних середовища
- Маршрутизація — режими маршрутизації Traditional, Framework, SPA і Worker
- Режим воркерів — постійні PHP-воркери для застосунків на фреймворках
- TLS — HTTPS із вбудованим завершенням TLS
- Перевірки працездатності — подробиці про ендпоінт працездатності та інтеграцію з Kubernetes
- Коректне завершення роботи — поведінка під час дренування та послідовність завершення роботи