Per-vhost PHP versions in OpenLiteSpeed: keep one site on 7.4 while everything else moves to 8.2

Modernizing the PHP runtime on a multi-tenant LSWS box is rarely an “all sites at once” affair. Some sites are on a custom theme that uses PHP 4-style class constructors. Some have a plugin still calling create_function(), removed in PHP 8. Some are pinned to a specific PHP minor by a vendor’s compatibility matrix. The path forward isn’t waiting until every site is portable — it’s running PHP 8.2 by default and quietly leaving one or two stragglers on PHP 7.4 until you can patch them.

OpenLiteSpeed makes this nearly free. The mechanism is two extprocessors at the server level and a single scripthandler override in whichever vhosts need the legacy runtime. Here’s how it slots together.

Install both PHP versions

Add the LiteSpeed apt repo if you haven’t, then install the version you’re moving to. The 7.4 build that shipped with OLS is already in /usr/local/lsws/lsphp7/; we just need 8.2 alongside it:

curl -fsSL https://repo.litespeed.sh | sudo bash
sudo apt update
sudo DEBIAN_FRONTEND=noninteractive apt install -y \
  lsphp82 lsphp82-common lsphp82-mysql lsphp82-curl lsphp82-imagick \
  lsphp82-intl lsphp82-opcache lsphp82-imap

# Verify
/usr/local/lsws/lsphp82/bin/lsphp -v
# PHP 8.2.30 (litespeed) ...

The extensions mbstring, xml, zip, gd, and bcmath are bundled into the main lsphp82 package on the LiteSpeed apt repo — there’s no lsphp82-mbstring like Ubuntu’s php-* naming. Don’t go hunting for them.

Wire up the wrapper symlink

OLS’s existing extprocessor lsphp uses the path fcgi-bin/lsphp7, which is a symlink to the 7.4 binary. Mirror that pattern for 8.2 so the config reads naturally:

sudo ln -sfn /usr/local/lsws/lsphp82/bin/lsphp \
              /usr/local/lsws/fcgi-bin/lsphp82
ls -la /usr/local/lsws/fcgi-bin/lsphp82
# lsphp82 -> /usr/local/lsws/lsphp82/bin/lsphp

Default everything to 8.2, define a legacy override

In /usr/local/lsws/conf/httpd_config.conf, change the existing extprocessor’s path to point at the new wrapper, and add a second extprocessor for the 7.4 fallback. Keep the same socket pattern so worker management stays predictable:

extprocessor lsphp {
  type                    lsapi
  address                 uds://tmp/lshttpd/lsphp.sock
  maxConns                10
  env                     PHP_LSAPI_CHILDREN=10
  env                     LSAPI_AVOID_FORK=200M
  initTimeout             60
  retryTimeout            0
  persistConn             1
  respBuffer              0
  autoStart               2
  path                    fcgi-bin/lsphp82       # ← was fcgi-bin/lsphp7
  backlog                 100
  instances               1
}

extprocessor lsphp_legacy {
  type                    lsapi
  address                 uds://tmp/lshttpd/lsphp7.sock
  maxConns                5
  env                     PHP_LSAPI_CHILDREN=5
  env                     LSAPI_AVOID_FORK=200M
  initTimeout             60
  retryTimeout            0
  persistConn             1
  respBuffer              0
  autoStart               2
  path                    fcgi-bin/lsphp7
  backlog                 100
  instances               1
}

The server-level scripthandler still says add lsapi:lsphp php — every site uses the default 8.2 runtime unless its vhost says otherwise.

Pin the legacy site at the vhost level

In conf/vhosts/<legacy-site>/vhconf.conf, append a single block:

scripthandler  {
  add                     lsapi:lsphp_legacy php
}

That’s the entire override. Vhost-level scripthandler overrides server-level. Every .php request to that one vhost goes through the 7.4 socket; everything else stays on 8.2.

Restart and verify

Graceful reload does not reliably rebind extprocessors when you’ve added a new one. Use a hard restart:

# Kill any stale workers from prior masters that might still hold sockets
sudo killall -9 openlitespeed lsphp lsphp7 lsphp82 2>/dev/null
sudo rm -f /tmp/lshttpd/lsphp.sock /tmp/lshttpd/lsphp7.sock /tmp/lshttpd/*.pid
sudo systemctl start lshttpd
sleep 5

# Trigger one request per site so workers spawn
for h in main-site.com legacy-site.com; do
  curl -s -o /dev/null -k --resolve "$h:443:127.0.0.1" "https://$h/"
done

# Check which lsphp binary each worker is running
for pid in $(pgrep -f lsphp); do
  echo "  $pid → $(sudo readlink /proc/$pid/exe)"
done

You should see two distinct binaries in the output: workers for the legacy vhost executing lsphp-7.4.x, workers for everything else executing the 8.2 binary under /usr/local/lsws/lsphp82/bin/lsphp.

Two gotchas

  • LiteSpeed Cache will serve cached pages with no PHP involvement. If you check x-powered-by after the switch and see PHP/7.4.x on a site you expected to be on 8.2, purge LSCache (rm -rf /usr/local/lsws/cachedata/*) before assuming the config is wrong. The cached HTML still has the old header baked in.
  • The 8.2 build defaults to expose_php = Off, so on the upgraded sites x-powered-by is simply absent. That’s the desired behavior; don’t try to “fix” it back.

When you’re ready to retire 7.4

Patching the legacy site is usually one of two things:

  • A theme widget calling $this->WP_Widget(...) in a PHP4-style constructor — convert to __construct() + parent::__construct(...) (a few lines per widget).
  • A plugin calling create_function(), each(), or hitting one of the dropped silent-string-conversion warnings. Either patch with a small monkeypatch or replace the plugin.

Once the legacy site loads cleanly under 8.2, delete the scripthandler block from its vhconf and restart. The site falls back to the server-level default, the lsphp_legacy extprocessor goes idle, and you can drop it from httpd_config.conf on the next maintenance window.

The whole pattern is genuinely two config blocks plus one symlink. The hard part is knowing it’s possible at all — most “PHP version” howtos pretend you have to upgrade everything in lockstep. You don’t.

Leave a Comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.