Pular para o conteúdo principal

Getting started

WarmPHP keeps your PHP application booted in memory and serves HTTP with it, using the plain PHP CLI. You put it behind your usual web server, as you would PHP-FPM.

You need a PHP 8.1 or newer CLI to run it, from your distribution or any image. Install WarmPHP from the ByJG package repository, use Homebrew, or build from source as described below. Only the source build needs Go installed manually. The binary carries its PHP files, so it runs from any folder, on its own.

Install PHP​

WarmPHP uses the PHP you already have: nothing is built for it. -mode prefork also needs the pcntl and posix extensions. Without them the gateway refuses -mode prefork when it starts and names what is missing, for example:

-mode prefork needs PHP's pcntl and posix extensions, and this PHP lacks posix_kill: install them (see Requirements in the README), or use -mode worker

-mode worker and -mode legacy need neither.

Ubuntu (apt)​

sudo apt install php-cli # pcntl and posix are included

Your application's extensions are packages too: php-mysql, php-mbstring, php-xml... Tested on Ubuntu 26.04, PHP 8.5.4.

Fedora (dnf)​

sudo dnf install php-cli php-process # php-process brings posix; pcntl is in php-cli

Without php-process, -mode prefork is refused for posix_kill. Tested on Fedora 44, PHP 8.5.12.

Docker's official image​

php:8.5-cli has posix but not pcntl. As it is, it runs the plain workers; for -mode prefork, add pcntl with the image's own tool:

FROM php:8.5-cli
RUN docker-php-ext-install pcntl

Then run PHP through it, as in PHP in Docker. Tested with PHP 8.5.12.

byjg/php images​

byjg/php:8.5-cli and byjg/php:8.5-fpm-nginx have pcntl and posix. They load Xdebug in develop mode, which slows every request, so turn it off:

./warmphp \
-php "docker run --rm -i --init -v $PWD:$PWD -v /tmp:/tmp -u $(id -u):$(id -g) byjg/php:8.5-cli php -d xdebug.mode=off" \
examples/hello/app.php

make test runs on byjg/php:8.5-fpm-nginx by default, and the benchmark uses it. Tested with PHP 8.5.12.

1. Install or build​

Install from ByJG packages​

For Debian/Ubuntu, Fedora/RHEL or Alpine, first follow the ByJG repository setup instructions for your distribution. Then install WarmPHP:

sudo apt install warmphp # Debian/Ubuntu
sudo dnf install warmphp # Fedora/RHEL
sudo apk add warmphp # Alpine

On Alpine, php84 alone runs WarmPHP; add the extensions your application needs beside it (php84-session for sessions, php84-pdo_mysql...).

For macOS or Linux with Homebrew:

brew install byjg/tap/warmphp

Homebrew installs Go as a build dependency automatically.

After package or Homebrew installation, run warmphp from your PATH instead of ./warmphp. You can run your own application directly; the example below uses files from a source checkout.

Build from source​

From a source checkout, with Go installed:

make build

This gives ./warmphp, a static binary for this machine. For another one: make build GOOS=linux GOARCH=arm64.

2. Run the example​

./warmphp -workers 4 examples/hello/app.php
curl 'localhost:8080/?name=ana'

Call it a few times: requests_served grows, because the application is not booted again.

3. Run your application​

The gateway runs an entry file: it boots once per worker (once for all of them with -mode prefork), then handles every request.

3.1 Your own code​

The entry boots once and returns the function that handles each request:

<?php
// app.php: runs once per worker
require __DIR__ . '/vendor/autoload.php';
$app = buildMyApplication(); // container, routes, configuration...

return static function () use ($app): void {
$app->handle(); // runs on every request, with $_GET, $_POST, $_SERVER...
};
./warmphp app.php

An old-style index.php that does everything at the top level can run as it is, with -mode legacy. It is included again on every request, but the worker process stays: the classes, functions and require_once files it loaded stay loaded, and an edit to them is not seen until the worker is replaced. It gains less than -mode worker; read the rules in -mode legacy first:

./warmphp -mode legacy public/index.php

Which mode for which case: When to use each mode.

3.2 Existing frameworks​

A framework's own index.php boots it on every request, and most break when it is included twice. The binary has an entry for each framework instead, from compat/: it boots the framework once and handles each request the way the framework's own worker runtime does. Your application is not changed; -framework picks the entry and -project-root says where your application is:

./warmphp -framework symfony -project-root /path/to/my-symfony-app

The PROJECT_ROOT variable does the same, if you prefer it; -project-root wins over it.

-frameworkStatus
symfony✅ one kernel, reset between requests
cakephp✅ one application, a new container per request
codeigniter✅ its own worker mode
gluo✅ one Application (byjg/gluo-core 7.0; older Gluo projects are not supported)
laravel-shared✅ one application, its session, login and array cache reset between requests; for state your application changes during a request (config, locale), see compatibility.md
laravel-fresh⚠️ builds a new application per request: little gain, and it needs -max-requests (e.g. 100)

Each entry was tested on a real application of that framework, without changing it: compatibility.md has the results and what each entry works around.

Another framework, or a change to an entry? Write the PHP files out and pass the entry you changed, keeping it beside the entries it requires:

./warmphp -extract ./warmphp-php
./warmphp -project-root /path/to/app ./warmphp-php/compat/my-framework.php

An entry has to do three things: boot the framework once, return a function that builds the framework's request from the globals (PhpWorker::input() for the body) and sends its response, and reset what the framework keeps per request. Look for the framework's FrankenPHP or RoadRunner integration: it does the same.

3.3 While you develop: -watch​

A worker keeps the code it booted with. With -watch, the gateway replaces the workers when a file of the application changes, and the next request runs the new code:

./warmphp -watch -workers 1 app.php

It watches the application's folder (-project-root, or the nearest folder with a composer.json up from the entry), skips vendor/ and the folders frameworks write to (var/, storage/, cache/...), and does not work with -mode prefork. The log names the files that changed, then says reloaded: every worker runs the new code once the new code is up. Details: Development: -watch.

4. Reading request bodies​

php://input is empty, because PHP runs as a CLI. Read the raw request body with PhpWorker::input() instead:

$data = json_decode(PhpWorker::input(), true);

The worker provides $_GET, $_POST, $_FILES, $_COOKIE, response headers, cookies, sessions and output. See the supported behavior and limitations. Framework boot files handle request-body adaptation for the supported frameworks. Your application must also reset request-specific state and, with prefork, open connections lazily; see What to watch for.

5. Put nginx in front​

nginx serves your static files and passes the rest to the gateway:

upstream php {
server 127.0.0.1:8080;
keepalive 16;
}

server {
listen 80;
root /path/to/my-app/public;

location / {
try_files $uri @php;
}

location @php {
proxy_pass http://php;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host $http_host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}

With Caddy it is one line: reverse_proxy localhost:8080.

6. Production settings​

These values are starting points, not measured capacity limits. More workers can increase memory use and database connections; choose worker and recycling counts for your application.

./warmphp -listen 127.0.0.1:8080 -workers 4 -mode prefork -max-requests 1000 -timeout 30s app.php
FlagWhat it is for
-workers 4Up to four requests in PHP at once; one per CPU core is a starting point, then measure your workload
-mode preforkBoots once and forks the workers: they share OPcache and memory, so they use much less of it
-max-requests 1000Replaces a worker after that many requests, in case your application leaks memory
-timeout 30sUp to 30 seconds to wait for a worker and process the request; timeout returns 504 and an active timed-out worker is replaced

Turn OPcache on for the CLI (opcache.enable_cli=1, in php.ini or with -php "php -d opcache.enable_cli=1").

The gateway logs JSON on stderr: a record per request with its time, CPU and memory in PHP, and PHP's errors, warnings and error_log(). An OpenTelemetry Collector can read them; see Logs.

7. PHP in Docker​

-php is the command that runs PHP. It must keep stdin open (-i), and --init lets PHP stop cleanly:

./warmphp \
-php "docker run --rm -i --init -v $PWD:$PWD -v /tmp:/tmp -u $(id -u):$(id -g) php:8.5-cli php" \
examples/hello/app.php

PHP must see -upload-dir (the system's temp folder by default, hence -v /tmp:/tmp): the gateway writes its PHP files there when it starts. Without that, PHP stops with "Could not open input file". Environment variables do not reach the container unless you pass them with -e; -project-root does, as it is an argument.

The gateway checks the command before it starts any worker, and stops with what is wrong: not found, failed (with PHP's or Docker's error), or not passing stdin, as a wrapper script that adds -i only when stdin is a terminal:

the PHP command "php" does not pass its stdin through to PHP, and the gateway sends every request on it: a wrapper script or container must keep stdin open (docker run -i)

What to watch for​

The application stays in memory between requests, so:

  • Do not keep request data in static properties or global variables. The next request, maybe another user's, sees it.
  • With -mode prefork, open database and cache connections on the first request, not at boot: a connection opened at boot would be shared by every worker.
  • A fatal error or exit() is fine: the request is answered and the worker is replaced.

Next​

  • README: every flag, the protocol, how the web functions are redefined
  • compatibility.md: how each framework runs, and what was found
  • benchmark.md: FPM, the gateway and FrankenPHP compared on a real journey
  • analysis.md: what those numbers mean, and which server to choose