Custom PHP-FPM 8.3 / 8.4 / 8.5 installer and updater for CWP (Control Web Panel) on EL8 / EL9 (AlmaLinux, Rocky, CloudLinux, CentOS).
Replaces the multi-step manual guide (copy selector files, fix mbstring, build curl, run builder per version, patch memcache/redis, install imagick/ioncube, reinstall ioncube after every CWP rebuild, etc.) with a single command.
A single auto-detecting script that:
- Auto-detects EL8 vs EL9 and picks the right build profile
- Refreshes
ca-certificatesand auto-disables the/etc/ld.so.conf.d/curl-local.conftrap that breaks dnf/yum after a manual curl install - Deploys CWP GUI scaffolding —
versions.ini, EL-appropriate8.3.ini/8.4.ini/8.5.ini, allexternal_modules/*andpre_run/*— into the right/usr/local/cwpsrv/htdocs/resources/conf/el${MAJOR}/php-fpm_selector/path - Seeds known-good
php{NN}.conf/_pre.conf/_external.conf(EL8 only — fixes the mbstring missing bug out of the box) - Builds PHP with EL-aware compile profile:
- EL8: isolated
curl 8.7.1under/opt/curl-8.7.1/(used only at build-time — never touches/usr/local/lib, never breaks dnf), PIE flags, OpenSSL 1.1.1k - EL9: native OpenSSL 3.x, system curl, no PIE — simpler and faster
- EL8: isolated
- Atomic-swap deploy — core PHP is built into
STAGE_DIRviaDESTDIR. Tenants keep serving on the EXISTING/opt/alt/php-fpmNNfor the entire ~10-15 min compile window. Atomic swap is ~2-5 sec. User pool configs carry over from the old install. Auto-rollback if the new install fails to start — old install restored from.rollback.<stamp>dir, service brought back online in seconds. Extensions (imagick/redis/memcache/ioncube) build AFTER swap — ~3-5 min degraded window where sites using those extensions error before they finish loading. - Builds all the PECL extensions you actually need — memcache (websupport-sk fork), memcached, redis (phpredis git), imagick, ioncube, mongodb, apcu, mailparse, xdebug, etc.
- Auto-heals ioncube after every CWP rebuild — fixes the wart where
sh /scripts/update_cwpor CWP's "Rebuild Apache + PHP-FPM" overwrites/usr/local/ioncube/with the stale bundled tarball missing 8.4/8.5 loaders - Wires systemd unit, Apache
mod_proxy_fcgi, monit watcher, CSFpignore— all auto-applied - Verifies — final table per built version shows PHP version, OpenSSL version, libcurl version,
php-fpm -tresult, service state, key extensions loaded
Works on:
- CWP / CloudLinux EL8
- CWP / CloudLinux EL9
- AlmaLinux / Rocky / CentOS 8 + 9
Idempotent — safe to re-run, safe to upgrade point releases.
# SSH to target server as root. The safe default rollout below builds PHP
# atomically and never touches CWP's system PHP, /usr/local/lib*/, or
# /usr/local/bin/* — so it's safe on any server.Run as root on each target server. Each version installs independently with atomic-swap (zero compile-time downtime, auto-rollback on failure):
for v in 8.5 8.4 8.3 8.2; do
curl -fsSL https://raw.githubusercontent.com/wpexpertinbd/cwp-custom-php/main/install.sh \
| bash -s -- --php $v=latest --force-conf
doneLogs are written automatically to /root/cwp-custom-php-<hostname>-<timestamp>.log per run. If a build fails, share that file. To disable auto-logging: append --no-log or BH_LOG_FILE=/dev/null bash ….
This is all you need on a fresh server. After it completes:
- PHP 8.2, 8.3, 8.4, 8.5 are built and visible in CWP Admin → PHP-FPM Selector
- CWP's own PHP installs (5.x, 7.x, 8.0, 8.1) are untouched
/usr/local/lib*/and/usr/local/bin/*are untouched- Existing CWP system PHP-CGI (PHP Version Switcher UI) is untouched
- ionCube auto-healed,
mongodb/sourceguardianextensions default-disabled
curl -fsSL https://raw.githubusercontent.com/wpexpertinbd/cwp-custom-php/main/install.sh \
| bash -s -- --php 8.4=latestcurl -fsSL https://raw.githubusercontent.com/wpexpertinbd/cwp-custom-php/main/install.sh \
| bash -s -- --php 8.4=8.4.21Same command — the script is idempotent. Atomic-swap means tenants serve on the existing PHP throughout the build; the swap is ~2-5 sec.
curl -fsSL https://raw.githubusercontent.com/wpexpertinbd/cwp-custom-php/main/install.sh \
| bash -s -- --php 8.4=latest--clean-shadow-libs and --system-php=X.Y exist for specific repair scenarios but caused tenant outages on s1 (2026-05-27) when used as part of the default rollout. They override CWP's behaviour for /usr/local/bin/php* and /usr/local/lib*/. Don't include them in fleet rollout commands unless you have a specific known problem and have read what they do in the Options reference below.
curl -fsSL https://raw.githubusercontent.com/wpexpertinbd/cwp-custom-php/main/install.sh \
| bash -s -- --refresh-ioncubeWhen to run it:
- After clicking Rebuild Apache + PHP in CWP Admin
- After PHP Version Switcher → Build in CWP Admin
- After PHP Selector → install/rebuild any per-user PHP-CGI version
- After PHP-FPM Selector → install/rebuild any CWP-managed (5.x–8.1) version
- After CWP's auto-update runs (often at midnight)
- Any time
/scripts/update_cwpruns (manually or via cron) - Any time
php -von a custom version sayslibzip.so.5: cannot open shared object fileorionCube loader: No such file or directory
What it does (in this exact order):
-
check_libzip— Detects whether thelibzipRPM is installed and/usr/lib64/libzip.so.5exists. CWP UI rebuilds frequently REMOVE the libzip RPM entirely (real incidents on s1 + s4 — happened twice). When this happens, every PHP binary segfaults withlibzip.so.5: cannot open shared object file. This step auto-reinstallslibzip+libzip-develviadnfand runsldconfig. -
refresh_ioncube— Downloads the latest ionCube loader tarball fromioncube.com(29 MB, ~5 sec), extracts to/usr/local/ioncube/, fixes permissions (root:root, 755 dirs / 644 files), then for every detected/opt/alt/php-fpmNN:- Writes
php.d/ioncube.inipointing at the matchingioncube_loader_lin_<X.Y>.so - Verifies the loader actually loads via
php -v(looks for "ionCube" in output) - Restarts
php-fpm<NN>service so live workers pick up the new loader - Skips PHP 8.0 (ionCube never released a loader for 8.0 — by design, not a bug)
- Skips
.rollback.*/.failed.*backup dirs from atomic-swap builds - Auto-removes
chattr +iimmutable bit on/usr/local/ioncube/if present
- Writes
-
ensure_versions_ini— Detects which/opt/alt/php-fpm{82,83,84,85}dirs exist with working binaries. Reads/usr/local/cwpsrv/htdocs/resources/conf/el${MAJOR}/php-fpm_selector/versions.ini. For each installed major:- If
[X.Y]section MISSING (CWP wiped it during their update) → appends our whole section block (all point releases, latest first) - If section EXISTS but our LATEST point release isn't listed → inserts the
version[]=X.Y.Zline right after the[X.Y]header (shows first in CWP UI dropdown) - If section exists and latest is listed → no-op
- Sections for versions we DON'T manage (5.x, 7.x, 8.0, 8.1) are never touched — CWP's updates to those flow through cleanly
- Always backs up the live file to
/root/cwp-php-backups/<stamp>/first
- If
-
Service restart loop — Iterates every
/opt/alt/php-fpm*(skipping.rollback/.failed/.bak/.olddirs), runssystemctl restart php-fpm<NN>for each one that has a systemd unit. Prints the version banner per service for confirmation.
What it does NOT do:
- Does NOT rebuild PHP from source
- Does NOT change PHP versions
- Does NOT touch
/usr/local/lib*/shadow libs - Does NOT touch
/usr/local/bin/php*system symlinks - Does NOT run
/scripts/update_cwp(used to — that was self-defeating since CWP's script wipes ionCube; removed in commit333f8ab)
Runtime: ~30-90 seconds total. Atomic — services briefly cycle but no rebuilding.
Safe to run:
- Anytime, idempotent
- Pre-flight to confirm a server is healthy after CWP-side operations
- As part of a cron / monitoring script if you want it automated
Auto-log: /root/cwp-custom-php-<host>-<stamp>.log. Share this file if anything fails.
You hit the /etc/ld.so.conf.d/curl-local.conf trap. Fix:
curl -fsSL https://raw.githubusercontent.com/wpexpertinbd/cwp-custom-php/main/install.sh \
| bash -s -- --fix-dnfSkip the GUI deploy to save 10 seconds:
curl -fsSL https://raw.githubusercontent.com/wpexpertinbd/cwp-custom-php/main/install.sh \
| bash -s -- --php 8.4 --build-onlyIf your existing /usr/local/cwp/.conf/php-fpm_conf/php84.conf is an old one missing --enable-mbstring, force-replace it with the repo's known-good copy:
curl -fsSL https://raw.githubusercontent.com/wpexpertinbd/cwp-custom-php/main/install.sh \
| bash -s -- --php 8.4 --force-confgit clone https://github.com/wpexpertinbd/cwp-custom-php.git /root/cwp-custom-php
cd /root/cwp-custom-php
bash install.sh --php 8.4# Custom PHP works?
/opt/alt/php-fpm84/usr/bin/php -v
# All expected modules loaded?
/opt/alt/php-fpm84/usr/bin/php -m
# FPM config sane?
/opt/alt/php-fpm84/usr/sbin/php-fpm -t
# Service up?
systemctl status php-fpm84
# OpenSSL + curl actually wired into PHP?
/opt/alt/php-fpm84/usr/bin/php -i | grep -iE 'SSL Version|cURL Information'
# Ioncube loaded?
/opt/alt/php-fpm84/usr/bin/php -v | grep -i ioncube| Flag | What it does |
|---|---|
--php X.Y[=VER] |
PHP majors to install/update. Accepts 8.4, 8.4=8.4.21, 8.4=latest, comma-list 8.3,8.4,8.5. |
--build-only |
Skip GUI scaffolding deploy. Use for repeat builds. |
--force-conf |
Overwrite existing /usr/local/cwp/.conf/php-fpm_conf/php{NN}*.conf (EL8 only). |
--refresh-ioncube |
Post-CWP-rebuild recovery (libzip + ioncube + versions.ini merge + restart all custom php-fpm). Also re-asserts the big-upload limit (CWP rebuilds reset it to 64 MB) — honors --big-upload / BH_BIG_UPLOAD_MB (default 2048, 0 to skip). Run after ANY CWP UI rebuild. |
--fix-dnf |
Run only the curl-trap repair and exit. |
--disable-ext=LIST |
Comma-list of extensions to disable post-build (.ini renamed to .ini.disabled, .so kept). Default: mongodb,sourceguardian — both emit noisy deprecation/version warnings every CLI invocation. Pass --disable-ext= (empty) to keep everything enabled. |
--big-upload=SIZE_MB |
After build, runs CWP's /scripts/php_big_file_upload SIZE_MB all — bumps upload_max_filesize, post_max_size, memory_limit (PHP) + client_max_body_size (Nginx) + LimitRequestBody (Apache) across all PHP versions on the box. Default: 2048 (2 GB) — high but matches BiswasHost filemanager use. Pass --big-upload=0 to skip. |
--clean-shadow-libs |
/usr/local/lib*/) and a narrow allowlist of non-PHP binaries (/usr/local/bin/curl, pcre2grep, zipcmp, etc.) to /root/cwp-php-backups/stale-libs/. Skips /usr/local/bin/{php,php-cgi,phpdbg,lsphp} because those are CWP system PHP binaries (use CWP's PHP Version Switcher UI to rebuild them, not this flag). Default is warn-only — recommended unless you have a specific known conflict. |
--system-php=X.Y |
/usr/local/bin/{php,php-cgi,phpdbg,php-config,phpize} → /opt/alt/php-fpmXY/usr/bin/. Bypasses CWP's "PHP Version Switcher" UI — the dashboard will show whatever the symlink resolves to. Replaces the manual ln -sfn ritual. Example: --system-php=8.3. |
-h, --help |
Help. |
| Variable | Effect |
|---|---|
BH_SKIP_IONCUBE=1 |
Don't auto-refresh ioncube at end of --php flow. |
BH_BIG_UPLOAD_MB |
Default size (MB) for /scripts/php_big_file_upload. Default 2048. Set 0 to skip. |
BH_REPO_URL |
Override the curl|bash clone source (defaults to this repo). |
BH_REPO_BRANCH |
Branch for curl|bash mode (default main). |
| Concern | EL8 | EL9 |
|---|---|---|
| Curl during PHP build | Isolated /opt/curl-8.7.1/ |
System curl (already 8.x) |
| PIE flags | Added | Not needed |
| OpenSSL | 1.1.1k + env-wired | 3.x native |
| Selector path | .../el8/php-fpm_selector/ |
.../el9/php-fpm_selector/ |
Seeded php-fpm_conf |
Yes (fixes mbstring) | Skipped (CWP generates) |
| Build-deps install | Looped (libavif may be absent) | Single dnf install |
8.4.ini pcre option |
(default) | --with-external-pcre |
| Building 8.3/8.4/8.5 | All supported | All supported |
Every run creates /root/cwp-php-backups/<YYYYMMDD-HHMMSS>/ with:
- Previous versions of overwritten scaffolding files
- Stashed
/opt/alt/php-fpmNN/usr/etc/php-fpm.d/users/*.confbefore rebuild - Previous
/usr/local/ioncube/if it was refreshed - Renamed
curl-local.conf.disabled.<stamp>if the trap was triggered
Safe to delete after a successful build.
cwp-custom-php/
├── install.sh # single entry point
├── lib/
│ ├── helpers.sh # logging, version compare, backup, version resolver
│ ├── preflight.sh # OS/CWP/arch checks, ca-cert, curl-trap fix, EL8/EL9 detect
│ ├── deploy-gui.sh # versions.ini, N.ini, external_modules/, pre_run/
│ ├── deploy-conf.sh # /usr/local/cwp/.conf/php-fpm_conf/ seeding (EL8 only)
│ ├── build-php.sh # unified builder with EL-aware profiles
│ ├── ioncube.sh # refresh loaders + stale-check + auto-heal
│ └── postcheck.sh # verification table
├── selector/
│ ├── versions.ini
│ ├── 8.3.el8.ini 8.3.el9.ini
│ ├── 8.4.el8.ini 8.4.el9.ini
│ ├── 8.5.el8.ini 8.5.el9.ini
│ ├── external_modules/{8.3,8.4,8.5}/*.sh # identical EL8/EL9
│ ├── pre_run/{8.3,8.4,8.5}/*.sh # identical
│ └── php-fpm_conf/php{83,84,85}{,_pre,_external}.conf # EL8 only
└── README.md
cURL error 60: SSL certificate problem — dnf reinstall -y ca-certificates && update-ca-trust force-enable && update-ca-trust extract. If dnf itself is broken: bash install.sh --fix-dnf.
mbstring not loaded after build (EL8) — your old /usr/local/cwp/.conf/php-fpm_conf/php{NN}.conf is probably missing --enable-mbstring. Re-run with --force-conf and the repo's known-good config replaces it.
ioncube missing after CWP rebuild — bash install.sh --refresh-ioncube. This is the canonical fix.
Build seems to hang — that's normal. make -j$(nproc) on PHP source takes 5-15 minutes depending on CPU.
Original manual guide: https://www.alphagnu.com/topic/614-how-to-add-custom-php-fpm-84-85-support-to-cwp-on-almalinux-9x/
- bh-server-ops — performance bootstrap, FPM/MPM tuning, monitoring, anti-bot WAF for CWP/Linux web stacks