PHP Runtime And Server Environment

FrankenPHP Application Server

FrankenPHP is a PHP application server built on Caddy. It can serve static files, execute PHP, terminate HTTPS, and run applications either with a conventional request lifecycle or as long-running workers.

This can simplify a deployment that would otherwise combine a web server and PHP-FPM. It does not remove the need to configure document roots, PHP extensions, environment variables, timeouts, logs, health checks, and process lifecycle.

Classic Mode First

Classic mode executes PHP files in a traditional request-oriented model. It is the default when worker mode is not configured and is the safer first migration target for an existing application.

Start here when:

  • the application currently runs behind PHP-FPM or Apache
  • compatibility matters more than avoiding framework boot time
  • dependencies have not been audited for long-running execution
  • the team needs a smaller operational change

Classic mode still uses FrankenPHP's server architecture, but application state is not deliberately retained through a framework worker between requests.

A Minimal Caddyfile

FrankenPHP uses Caddy configuration. A small application with public/ as its document root might begin with:

:8080 {
    root * /app/public
    encode zstd gzip
    php_server
}

The exact address, TLS policy, headers, static-file behavior, logs, and timeouts depend on the deployment. The important boundary is the document root: private source, configuration, and dependency files should not become directly downloadable.

Treat the Caddyfile as reviewed application infrastructure. Validate it in the same image and command used in deployment.

Packaging Choices

FrankenPHP is available through official container images, standalone binaries, and platform packages. Choose a distribution method that lets the team control:

  • the FrankenPHP and PHP versions
  • required PHP extensions
  • php.ini settings
  • application files and ownership
  • certificates or upstream proxy behavior
  • reproducible builds and security updates

Do not assume a prebuilt binary contains every extension the application needs. Check the deployed runtime with normal PHP inspection commands and execute a real smoke test.

Worker Mode

Worker mode boots the application once and keeps it in memory while handling multiple requests. Avoiding repeated framework startup can reduce latency and CPU work.

The tradeoff is a different lifetime model. Static properties, global variables, mutable singletons, in-memory caches, event listeners, database connections, and modifications to long-lived state may survive into later requests.

Choose worker mode only after measuring a meaningful bottleneck and confirming that the framework and application support the lifecycle. FrankenPHP documents integrations for Symfony and Laravel Octane; use the supported integration rather than inventing a custom worker loop unless the team can own that code.

Audit State Before Enabling Workers

Start Workers Through A Supported Entry Point

Worker mode needs a worker script or framework integration, not merely the application's ordinary front controller. With the standalone server, the shape is:

frankenphp php-server --worker /app/path/to/worker.php

The path must be the worker entry point documented by the framework integration or maintained by the application. FrankenPHP provides specific guidance for Symfony and Laravel Octane. Those integrations handle framework boot, request conversion, termination, and known reset hooks; they do not automatically make every custom service safe.

A custom worker calls frankenphp_handle_request() repeatedly. Its request handler must catch and report exceptions for each request, because a process-level exception handler may not run until the worker ends. Cleanup and framework termination logic must execute after every handled request, including failed ones.

Development file watching can restart workers after code changes, but production deployments should replace immutable application artifacts through the normal rollout process. Hot reload is not a substitute for versioned deploys and rollback.

Know What FrankenPHP Resets

FrankenPHP updates common request superglobals such as $_GET, $_POST, $_COOKIE, $_FILES, $_SERVER, and $_REQUEST for each handled request. Application state outside those superglobals can still persist.

$_ENV is a notable exception: current FrankenPHP worker documentation states that it is not reset between requests. Do not write tenant, user, locale, authorization, or other request-specific values into $_ENV. Treat environment configuration as immutable process input.

A worker script can stop after a configured number of requests and be restarted. The official custom-worker example reads MAX_REQUESTS for that purpose. Framework integrations may expose their own lifecycle controls, so use the setting they document rather than assuming one variable applies everywhere.

A request limit can contain gradual memory growth and make deployments predictable. It cannot prevent data leakage on the requests handled before restart. State reset and sequential cross-user tests remain mandatory.

Search for request-specific values stored in long-lived locations:

  • authenticated users and authorization decisions
  • tenant identifiers
  • locale and timezone overrides
  • request IDs and tracing context
  • mutable static properties
  • global arrays
  • singleton services with setters
  • per-request event subscriptions
  • database transactions and session state
  • caches whose keys omit user or tenant scope

Framework reset support handles only state it knows about. Custom services may still require explicit reset behavior or redesign around request-scoped arguments.

Test Sequential Requests

A worker test must reuse the same running worker:

  1. send a request as user or tenant A
  2. send a different or anonymous request
  3. verify that identity, locale, headers, cart, permissions, and response data do not leak
  4. repeat enough requests to observe memory and connection behavior

A suite that rebuilds the application process before every request cannot prove worker isolation.

Set a bounded worker lifetime when appropriate so deployment restarts and gradual memory growth are manageable. A restart limit reduces impact; it does not make unsafe shared state correct.

Production Concerns

A production plan should define:

  • how configuration and secrets enter the process
  • who terminates public TLS when a load balancer or proxy is present
  • trusted-proxy and forwarded-header behavior
  • health and readiness endpoints
  • access and application log destinations
  • request, write, and upstream timeouts
  • resource limits and concurrency settings
  • graceful shutdown and worker restart behavior
  • rollback to the previous image or classic mode

Automatic HTTPS is useful when FrankenPHP is directly responsible for public certificates. Behind another proxy, configure the ownership of TLS and forwarding explicitly rather than running two accidental sources of truth.

Deployment Verification

Verify the built artifact, not only a developer command:

php -v
php -m
php --ini

Then check:

  • static assets and front-controller routes
  • uploads and request-size limits
  • error responses without exposed stack traces
  • database and cache connectivity
  • scheduled jobs and queue workers where relevant
  • logs and metrics
  • graceful restart during traffic
  • repeated requests under the configured runtime mode

Compare performance under representative traffic. A faster trivial benchmark does not prove a framework application is faster, stable, or correctly isolated.

When FrankenPHP Fits

FrankenPHP can fit teams that want Caddy's web-server capabilities and PHP execution in one deployable server, need modern protocol support, or can benefit from a supported worker integration.

PHP-FPM remains a valid choice when it is already reliable, operational tooling is built around it, or the application cannot safely use workers. Adopting FrankenPHP only to follow a runtime trend adds migration and support cost without a defined benefit.

What To Remember

FrankenPHP combines Caddy and PHP application serving. Classic mode is the lower-risk entry point; worker mode can reduce repeated boot work but requires lifecycle-safe code and sequential-request testing. Package required extensions explicitly and treat server configuration, observability, restart behavior, and rollback as part of the application design.

Before moving on, make sure you can explain why changing from PHP-FPM to FrankenPHP classic mode is a different decision from enabling FrankenPHP worker mode.

Practice

Task: Choose A FrankenPHP Runtime Mode

For each application, recommend FrankenPHP classic mode, FrankenPHP worker mode, or no migration yet.

Application A

A Symfony application runs reliably on PHP-FPM. The team wants to simplify its web-server container but has not measured framework boot cost. Several custom singleton services store the current tenant and locale.

Application B

A Laravel API has measured significant framework boot overhead. It has an existing Octane compatibility test suite, no request data in static properties, and tests that send requests from two tenants through the same worker.

For each recommendation, include:

  • the reason for the runtime choice;
  • the supported framework integration or entry point that would be used;
  • state and compatibility work required before worker mode;
  • treatment of mutable singletons, $_ENV, per-request exceptions, and database connections;
  • a sequential cross-user test that reuses one worker process;
  • whether a request-count restart limit helps and what it cannot fix;
  • deployment checks for document root, extensions, health, logs, graceful restart, and rollback;
  • one measurement that would justify revisiting or retaining the choice.

Do not recommend worker mode solely because a synthetic empty-page benchmark is faster.

Show solution

Application A

Use FrankenPHP classic mode only if combining Caddy and PHP serving has a demonstrated operational benefit. Otherwise keep PHP-FPM. Do not enable worker mode yet: mutable tenant and locale singletons can leak state, and no measured framework boot bottleneck justifies accepting that risk.

Classic mode does not require a Symfony worker entry point. If worker mode is later justified, use FrankenPHP's supported Symfony integration and redesign custom services around request arguments or reset contracts. Do not write request data into $_ENV, and ensure request exceptions are logged and converted into responses without terminating an uncontrolled worker loop.

A valid isolation test sends a request as tenant A, then an anonymous or tenant B request through the same worker process. It verifies identity, locale, authorization, headers, and response data after both requests. Rebooting the process between requests would not test worker safety.

A maximum request count may limit gradual memory growth, but it would still allow leakage between requests handled before restart. It is not a substitute for fixing the singletons.

Build the deployment artifact with required extensions, use /app/public as the document root, test static and front-controller routes, expose meaningful readiness, collect access and application logs, verify graceful replacement, and retain the PHP-FPM image or FrankenPHP classic configuration as rollback. Measure framework boot time and representative request CPU usage before revisiting workers.

Application B

Trial FrankenPHP worker mode through the supported Laravel Octane integration, using the entry point and lifecycle controls documented for the installed Octane and FrankenPHP versions. The measured boot overhead provides a reason for the trial, while the existing compatibility and cross-tenant tests reduce risk.

Extend the audit to mutable container singletons, event listeners, authentication and locale state, database and cache connection recovery, in-memory caches, and any writes to $_ENV. Confirm request failures are logged and cleaned up without contaminating the next request.

Run sequential requests from tenant A, tenant B, and an anonymous caller through one live worker. Verify that identity, permissions, locale, cache results, and response headers do not cross boundaries. Repeat the sequence under load while monitoring memory. A bounded worker lifetime can manage gradual growth and simplify replacement, but any observed cross-tenant state requires a code fix rather than a lower request limit.

Verify PHP extensions and php.ini in the built image, restrict the web root to public/, test health and readiness semantics, collect server and application logs, and prove graceful rollout and rollback to the previous image or classic mode.

Compare latency percentiles, CPU, throughput, memory growth, restart behaviour, and error rate under representative API traffic. Retain worker mode only if the measured gain is material and repeated isolation tests remain deterministic.