Skip to content

PHP versions

The versions this build pins. A patch can move without a rexenv release — see Patch updates — but only upwards, and only when you press the button.

Version Xdebug PostgreSQL Built by Status
8.5.8 ✅ 3.5.3 ✅ rexenv Current
8.4.23 ✅ 3.5.3 ✅ rexenv Current
8.3.32 ✅ 3.5.3 ✅ rexenv Current
8.2.32 ✅ 3.5.3 ✅ rexenv Current
8.1.34 ✅ 3.5.3 ✅ rexenv Past its php.net security-end date
8.0.30 ❌ ❌ static-php.dev Past its php.net security-end date
7.4.33 ❌ ❌ rexenv Past its php.net security-end date

The PostgreSQL column is about the pinned build, not the minor: it means a working pdo_pgsql, which only rexenv’s own 8.x builds carry. See rexenv builds its own PHP.

Install and switch:

Terminal window
rex php list # installed, default, pool port, and the NOTE column
rex php install 8.1
rex php default 8.3 # for new sites
rex site php myshop.rex 8.1 # for one existing site

Patch updates are a button, never a background download

Section titled “Patch updates are a button, never a background download”

8.3.32 is what this build pins. When PHP ships 8.3.33, rexenv can install it without waiting for a rexenv release — from Settings → PHP versions, one button, one minor at a time.

What the row can say, and each is a different fact:

The row says Means
8.3.32 The patch this minor is set to — the pin, or a newer one you chose
serving 8.3.31 The running pool is still on the old build. It moves on the next start
8.3.34 exists php.net lists it. rexenv may have no verified build of it yet
[Update to 8.3.33] A signed manifest offers that patch, and this minor is installed
costs PostgreSQL The offered build has no PostgreSQL driver — see below

8.3.34 exists and Update to 8.3.33 are not the same claim, and the gap between them is real. The first comes from php.net; the second needs a build rexenv can verify, and in the app’s own words those “are published some weeks after php.net announces a release — so a version can exist upstream with nothing here to install yet.” So the row says a version exists where it cannot honestly say update available, and says nothing at all about being “up to date”, which is unprovable before the first successful check.

The php.net check is read-only by construction. It fetches one document and derives one string per minor. It never reads a URL and never reads a digest, so nothing it says can make the app run anything.

Structurally, nothing — with the one exception in the next section. A site stores a minor — 8.3 — never a patch. No config is regenerated, no certificate is touched, no edge reload happens.

What does happen is a pool restart for that one minor, and the button says so before you press it. Then:

  • The download runs first, before anything is written. A failed or cancelled download leaves the minor exactly where it was.
  • If the pool does not come back, the selection returns to the previous patch and the pool is restarted onto it — a bad patch costs a restart, not a broken stack.
  • If the revert also fails, the message says so plainly, because that is the state where PHP is down: “…and putting it back on 8.3.32 ALSO failed — the pool is DOWN.”
  • If nothing was running for that minor, the row says PHP 8.3 will use 8.3.33 — nothing was running to restart rather than naming a process that does not exist.

The downloaded tree is kept either way. It is verified and valid, and deleting it would make a retry re-fetch ~100MB.

A signed manifest can offer rexenv’s own build of a patch, or upstream’s static-php.dev build. Only rexenv’s carry a working pdo_pgsql. When the offered build would take the driver away, the row carries a costs PostgreSQL chip, and the button’s explanation opens with what it costs — naming the sites:

PHP 8.3.33 is upstream’s build and cannot reach PostgreSQL. api.rex uses a PostgreSQL database and would stop reaching it.

With no PostgreSQL site on that minor, the sentence says instead that PostgreSQL will stop being offered for new sites on that version.

Every binary rexenv runs is checksum-pinned into the app, and that pin is what makes compromising a download host unable to reach an installed user. An in-app update needs a digest for bytes the app was built before, so the anchor moves — and it moves exactly one step: from a constant in the binary to a signed document whose public key is a constant in the binary.

Five limits, each a construction rather than a note:

  • Only php, php-fpm, php-licenses and adminer may be named. Never caddy — that one is a root LaunchDaemon. An entry naming anything else is dropped, not fatal, so one bad row cannot deny you every other update. php-licenses is on the list because the manifest can carry rexenv’s own builds, and a build rexenv distributes ships with its licences — one whose licences cannot be named is refused rather than installed without them.
  • https:// only, from hosts rexenv already downloads from — dl.static-php.dev, github.com/rexenv/, and Adminer’s own release path.
  • Re-verified on every read. The cached document lives in a file you can write; a “verified” flag in it would not be verification, so the signature is checked again each time, never remembered.
  • A patch of a minor this build already ships. Not new minors: eol_since and Xdebug support are compile-time facts, so a runtime-delivered 8.6 would render with no EOL date and Xdebug silently missing.
  • The pin is the floor. A manifest can only move you above the version your app ships, never below it — a comparison, not a flag, so with no manifest the answer is today’s pin byte for byte.

An older manifest is refused as a replay, because holding a machine on a superseded patch is what a compromised host would try. Offline, everything the row says about your installed patch is local and still true.

7.4, 8.0 and 8.1 are past their upstream security-end dates. rexenv ships them anyway — plenty of real work happens on legacy codebases, and a local environment that refuses to run what your client runs is not useful.

What it does instead is badge them: in Settings, in the create dialog, and on the site’s own Environment card. Choosing one stays a decision rather than becoming an accident.

The badge is computed against today’s date and each version’s published php.net security-end date, not read from a list somebody remembered to update. A version crossing its date starts saying so on its own, which is the only version of this feature that stays true.

Every pinned PHP except 8.0.30 is rexenv’s own build, published from the public rexenv/runtimes repository and pinned to an immutable release, so a pin can 404 but can never silently change under you.

The 8.1–8.5 builds used to be static-php.dev’s, and they had a hole rather than a gap: they ship pgsql and no pdo_pgsql, while their PDO advertises pgsql anyway. A PDO connection was accepted and then stalled for about a minute until PostgreSQL closed it with SQLSTATE[08006] — and Laravel’s pgsql driver is exactly that call. php -m and PDO::getAvailableDrivers() both reported support the build did not have, so rexenv’s builds are checked with a real connection, not a module list.

Parity with the builds they replaced is checked three ways, because each layer missed something the next one caught: module names; function names — an mbstring of the same name was 16 functions short, and every artisan command died on mb_split behind a perfect module list; and the configure flags read out of both binaries.

One difference is deliberate. static-php-cli’s Swoole PostgreSQL hook and the real pdo_pgsql are mutually exclusive — upstream’s driver only works inside a Swoole coroutine, which is why it advertises pgsql and then hangs. rexenv ships the real driver and loses Swoole’s PDO PostgreSQL and SQLite coroutine hooks.

8.0.30 is still static-php.dev’s. rexenv’s build of it passes every check on Apple Silicon and aborts on Intel inside the build toolchain’s own sanity check, reproducibly and unexplained, and 8.0 is past its security end. So 8.0 has no PostgreSQL driver.

Because rexenv builds these, it is also their distributor — so the licence texts (PHP License 3.01 and every statically linked dependency) install beside each interpreter rather than being left as somebody else’s paperwork.

7.4 was the first: static-php.dev publishes no 7.4 at all. It is a first-class version, not a compatibility shim: same picker, same pool model, its own php-fpm pool on 9774. 60 extensions, including gd, intl, mysqli and redis.

Four differences from the 8.x rows, all structural rather than oversights:

  • No Xdebug (see below).
  • No PostgreSQL. Its build has no working pdo_pgsql, so a PostgreSQL site is refused on it. See PostgreSQL.
  • No OPcache. The static-build toolchain’s OPcache patches start at 8.0, so there is nothing to enable. Stated here rather than left to be discovered from a phpinfo().
  • PCRE JIT is compiled out. The bundled PCRE2 predates Apple Silicon, and with JIT enabled Composer died on Allocation of JIT memory failed. Regex throughput on 7.4 is therefore lower than on the 8.x versions. A working 7.4 that is slower at regex beats a fast one that cannot run Composer.

A 7.4 site also cannot use the FrankenPHP override, which embeds PHP 8.5 and is refused rather than allowed to serve the site with the wrong runtime. See Web servers.

WordPress will show its own “outdated PHP” notice on a 7.4 site. rexenv warns you first — in the create dialog and on the site’s Environment card.

Xdebug is a shared object that has to dlopen into the running PHP. Those two static builds do not export the Zend symbols it needs — measured, not assumed: rexenv’s 7.4 exports around 22,400 symbols and _OnUpdateBool is not among them.

No external .so can load into them, so the per-site Xdebug toggle is not offered there rather than being offered and then failing. See Xdebug.

rexenv runs one php-fpm pool per installed PHP version, shared by every site using it — not one pool per site. Ten sites on 8.3 are ten server blocks in front of one pool.

That is why switching a site’s PHP version is instant and costs nothing: it re-points the site’s server block at a different pool. Nothing is rebuilt, and the docroot, certificate and database are untouched.