Migration
This guide moves an existing PHP application to OxPHP. Start with the section for what you run today, nginx + PHP-FPM or FrankenPHP and RoadRunner, and read What does not carry over before you commit to the move.
From nginx + PHP-FPM
Most applications move without code changes. The default mode runs each request from a clean PHP state, the same lifecycle PHP-FPM gives you, so the work is translating configuration. The example deployments are complete recipes for Laravel, Symfony, WordPress, Drupal, Magento and others.
1. Pick the routing mode that matches your try_files
| nginx | OxPHP |
|---|---|
try_files $uri /index.php?$query_string (Laravel, Symfony, Yii) |
ENTRY_FILE=index.php (Framework) |
try_files $uri $uri/ /index.php with several PHP entry points (WordPress, OpenCart) |
ENTRY_FILE unset (Traditional) |
try_files $uri /index.html for a single-page app |
ENTRY_FILE=index.html (SPA) |
2. Build the image
Start from the two-line Dockerfile in Quick Start. Extensions must be built for thread-safe PHP, in a php:*-zts-alpine stage with the same PHP minor as the OxPHP image; see Installing PHP extensions in production.
Check that every PECL extension you use supports ZTS. OxPHP runs thread-safe PHP only, so an extension without ZTS support cannot be loaded.
3. Translate the configuration
The image loads no php.ini. Keep your PHP settings (memory_limit, upload_max_filesize, OPcache, …) in a file of your own anywhere in the project, for example docker/php.ini, and copy it into the image's PHP config directory. That directory is a path inside the container, not in your project:
FROM ghcr.io/oxphp/oxphp:0.13.0
COPY docker/php.ini /usr/local/etc/php/conf.d/zz-app.ini
COPY --chown=www-data:www-data . /var/www/html/publicPHP reads every .ini file in that directory in alphabetical order and the last value wins, so the zz- prefix makes yours apply after the image's own files. Mounting the file as a volume works too; see PHP configuration.
The rest maps onto environment variables:
| nginx / PHP-FPM | OxPHP |
|---|---|
listen 443 ssl, ssl_certificate |
TLS_CERT, TLS_KEY, TLS_MIN_VERSION; see TLS |
pm = static / pm = dynamic, pm.max_children |
PHP_WORKERS=N / PHP_WORKERS=MIN:MAX |
php_admin_value[...], php.ini |
your own .ini file copied to /usr/local/etc/php/conf.d/ in the image (see above) |
gzip on, brotli on |
on by default, configured with COMPRESSION_ENCODINGS; see Compression |
expires 30d for static files |
STATIC_MAX_AGE (default 30d) |
limit_req |
RATE_LIMIT, RATE_WINDOW_SECONDS (fixed window per IP); see Rate limiting |
set_real_ip_from, real_ip_header |
TRUSTED_PROXIES; see Trusted proxies |
error_page 404 /404.html |
ERROR_PAGES_DIR with {status}.html; see Error pages |
access_log |
ACCESS_LOG=all or error (JSON lines); see Access logging |
location ~ ^/uploads/.*\.php$ { deny all; } |
PHP_DENY_PATHS=/uploads/**; see PHP deny-list |
pm.status_path, ping.path |
INTERNAL_ADDR with /health and /metrics; see Health checks |
fastcgi_finish_request() |
oxphp_finish_request(); see Early response |
4. Check the behaviour differences
max_execution_timecounts wall-clock time. Under PHP-FPM, time spent waiting on a database or an HTTP call is not charged; under thread-safe PHP it is. A slow endpoint that never hit the limit before can start returning504. See Timeouts.php_sapi_name()returnscli-server. Code that branches onfpm-fcgineeds another check:function_exists('oxphp_request_id').- A few
$_SERVERkeys differ.SERVER_ADDR,PATH_TRANSLATEDandREDIRECT_STATUSare not set, andPHP_AUTH_USER/PHP_AUTH_PWare not extracted; readHTTP_AUTHORIZATIONinstead. See Differences from PHP-FPM. - Framing is restricted by default. Responses carry
X-Frame-Options: SAMEORIGINand a matchingframe-ancestorspolicy. SetFRAME_OPTIONS=offif you manage framing elsewhere or embed the app in another origin; see Clickjacking protection.
5. Roll out beside the old stack
Run OxPHP next to nginx + PHP-FPM, point a share of traffic at it through your existing load balancer, use /health/readiness for probes, and compare error rates and latency on /metrics before moving the rest.
6. Turn on worker mode last
Migrate in the default mode first. Then set WORKER_MODE_ENABLED=true and ENTRY_FILE to a worker script. State that lives in static properties and long-lived connections now survives between requests, so read What resets and what persists before enabling it.
From FrankenPHP or RoadRunner
Worker scripts keep their shape: bootstrap once, then serve requests. Replace the frankenphp_handle_request() loop with a single oxphp_worker($handler) call. OxPHP runs the loop and calls the handler for each request; inside it, read the request from the superglobals or oxphp_http_request(). RoadRunner's PSR-7 waitRequest() / respond() loop has no counterpart: move the application's handler into the same callback. See oxphp_worker() and Worker mode.
Drop the MAX_REQUESTS counter
There is no equivalent, and you do not need one: a worker is not meant to be restarted every N requests, and doing so only hides a leak. If you suspect one, track it instead:
- Chart
oxphp_worker_memory_bytesper worker and watch for a steady climb. - Set
WORKER_MAX_MEMORY_MIBas a safety net. A worker past the limit is replaced before the container runs out of memory, and each such recycle is counted inoxphp_worker_recycles_by_reason_total{reason="max_memory"}. - That limit measures only PHP's own heap. With extension-heavy stacks, compare
Worker::current()->rss()against your own threshold and callWorker::scheduleExit().
What does not carry over
- OxPHP has no reverse proxy or upstream support: there is no
proxy_passand no WebSocket server. If nginx also fronts other services, keep a proxy in front of OxPHP for those routes. - An instance has one port and one protocol. It serves plain HTTP or HTTPS, not both, so the port-80 redirect to HTTPS needs a proxy or a second instance. The certificate is read at startup, so a renewal needs a restart; see TLS.
- HTTP/3 and
103 Early Hintsare not available yet; see the Roadmap. - OxPHP runs on Linux only and is distributed as a Docker image. There are no apt or brew packages. PHP 8.4 and 8.5 are supported.
Prompts for your agent
These prompts assume an agent with the OxPHP documentation MCP server connected. The agent reads the pages it needs itself.
Move this application from nginx and PHP-FPM to OxPHP. Read the `getting-started/migration` page through the OxPHP documentation MCP server.
Translate the nginx server blocks, PHP-FPM pool settings and php.ini overrides in this repository into an OxPHP Dockerfile and environment variables, choose the routing mode from the `try_files` rules, and list every behaviour difference and unsupported feature that applies to this code, with the file it affects.Port the FrankenPHP or RoadRunner worker in this repository to OxPHP. Read the `getting-started/migration` page through the OxPHP documentation MCP server.
Rewrite the worker script around a single `oxphp_worker()` call, remove any counter that restarts the worker every N requests, and tell me which metric and memory limit to set up in its place.What's next
- Routing: Traditional, Framework, SPA, and Worker routing modes in detail
- Docker Guide: development and production Dockerfiles, Compose configuration, PHP ini mounts
- Configuration: full environment variable reference
- Worker Mode: persistent PHP workers that bootstrap once and handle multiple requests
- Example Deployments: complete recipes for Laravel, Symfony, WordPress, Drupal, Magento and others