Back to blog

Leaving PHP, Part 7: the shared-hosting mental model still leaks into the language

Part 1 looked at hiring. Part 2 looked at the runtime. Part 3 looked at types. Part 4 looked at the ecosystem. Part 5 looked at the type seam. Part 6 looked at concurrency. Part 7 looks at where the code lives. PHP grew up on cPanel and that heritage is still encoded in the language's defaults.

2005
Roughly the deployment story the language defaults still assume: mod_php fronting Apache, configured by php.ini and .htaccess.
~6
Targets the modern PHP deployment toolbelt has to cover: bare metal, VMs, containers, Kubernetes, serverless, edge. None of them are the assumed default.
2
The most credible projects bridging PHP to modern deployment: Bref on AWS Lambda, FrankenPHP embedded in a Go binary. Both are adapter projects.

The defaults are from 2005

PHP grew up on shared hosting. Every cPanel account on a budget host ran mod_php inside Apache, configured by php.ini at the system level and .htaccess at the directory level. A request hit the web server, the web server forked or threaded into a PHP interpreter, the interpreter loaded the script, ran it top to bottom, sent the response, and dropped everything on the floor. The defaults of the language were chosen to make that one shape easy.

You can read the heritage anywhere you look. $_GET, $_POST, $_SERVER, $_SESSION are global because in a fork-per-request world there is no ambiguity about whose request they belong to. session_start() writes to a file in /tmp because the host filesystem was assumed to be the only state store. ini_set mutates process-global configuration mid-request because in 2005 the process was the request. register_shutdown_function exists because the script returning was the only termination signal anyone needed.

None of this is broken. All of it works. And every one of these defaults is now slightly off-shape for what people actually deploy in 2026.

Everything modern is an adapter

The PHP deployment story today is a stack of adapters that paper over the shared-hosting assumptions. php-fpm moved interpretation into a long-lived process pool, but the request shape stayed the same. Containers with nginx + php-fpm are now the standard production target, but the container has two processes for what other ecosystems do with one. Bref runs PHP on AWS Lambda by wrapping the runtime in a custom Lambda layer that synthesises the cPanel request shape on top of the Lambda event. FrankenPHP embeds the PHP runtime inside a Go binary so you can ship a single executable and skip the FPM step entirely. RoadRunner hosts a long-lived PHP worker that talks to a Go server over a binary protocol. Octane does similar work for Laravel specifically.

Every one of these projects is high-quality, well-engineered, and absolutely necessary. And every one of them exists because the language's defaults do not match the deployment shape. The adapter has to keep the script-per-request fiction alive while the runtime that calls it is doing something completely different. You can deploy PHP to Lambda, to Cloud Run, to a container, to Kubernetes, to the edge. You will be running an adapter every time.

The cost is rarely dramatic. It shows up as: extra processes per pod, extra moving parts in the diagram, more surface area for production incidents, an answer of "well, it depends on which SAPI you are using" to a lot of operational questions, and a steady tax on the time it takes to get a new service from a clean repo to a healthy production deployment.

JavaScript, Go, and Rust are native to this world

Node was built around an event loop and a single long-lived process. Drop the same code on a VM, in a container, on Lambda, on Cloud Run, or on a Cloudflare Worker, and the runtime model is roughly the same: an event loop accepts connections, dispatches handlers, returns responses. The deployment target changes the orchestration, not the runtime shape. Go is the same story with goroutines instead of an event loop. Rust's tokio looks similar from the outside. There is no equivalent of php-fpm and there does not need to be.

The result is that JavaScript, Go, and Rust deployments are direct. Build a binary or a bundle, ship it, run it. PHP deployments are translated. Build an artifact, pair it with a SAPI, pair the SAPI with a process supervisor, pair the supervisor with a web server, pair the web server with a container, and decide which of those layers you are willing to skip. The work is not insurmountable. It is steady, and there is more of it than there is in the alternatives.

A scorecard for the deployment surface

Defaults still assume mod_php + Apache + .htaccess
Superglobals, ini files, per-script lifecycle. None of it is wrong; all of it is shaped for 2005.
Every modern target needs an adapter
Bref, FrankenPHP, RoadRunner, Octane, php-fpm + nginx. Each one solves one shape of the problem.
Two-process containers are still common
nginx and php-fpm in the same pod, talking over a unix socket. Other ecosystems use one process.
Edge runtimes are mostly off the table
Cloudflare Workers, Deno Deploy, Vercel Edge: V8-isolate runtimes that PHP does not target.
=
The adapters themselves are good
Bref and FrankenPHP are excellent engineering. They are still adapters.

Deployment cost is the kind of thing that does not block any single decision and quietly shapes every one of them. Picking a container layout is harder. Picking a serverless platform is harder. Picking an edge story is harder. None of these are dealbreakers; all of them are friction the alternatives do not have.

Up next in the series

Three posts to go. The remaining arguments are about security defaults, the developer-tooling story (LSPs, debuggers, profilers, the shape of the IDE experience in 2026), and the actual migration playbook: what you do on day one, what you do in month six, and how Pext fits in.

If you want to start the migration before Part 10 lands, book a demo and we will walk you through what Pext does to your codebase.