php -m listed it. The command line ran it. WordPress Site Health, sitting in the same box, insisted it wasn’t there. One of them was lying, and it took me longer than I’d like to admit to work out which.
This is another one from the fleet migration — the ongoing job of shifting WordPress sites off Bitnami Lightsail onto AWS’s managed blueprint. A site had come across cleanly. Pages rendered, database was fine, certificates in place. Then I opened Site Health to sign it off and there it was under the critical issues: The Imagick module is not installed.
Which was strange, because I’d installed it.
The symptom
Imagick is the PHP extension WordPress leans on for image work — resizing uploads, generating those dozen thumbnail sizes, handling PDFs and the odd format GD chokes on. Without it WordPress limps along on GD and quietly does a worse job. Not the end of the world. But a red flag on a site I’m about to hand back isn’t something I’ll leave sitting there.
So I checked. On the box, from the shell:
php -m | grep -i imagick
imagick
There it is. PHP has it. To be sure I wasn’t imagining it:
php -r 'var_dump(extension_loaded("imagick"));'
bool(true)
Loaded. Confirmed twice. And yet the WordPress admin, half a metre away in the same filesystem, was adamant it couldn’t find the thing. Two surfaces of the same PHP install, flatly disagreeing about whether an extension exists.
If you’ve read the backup one from a few weeks back, you’ll know where my head went: when two views of the same tool contradict each other, don’t pick the convenient one. Work out why they differ.
The bit I’d forgotten about PHP
Here’s the thing I’d let myself forget. “PHP” on a web server isn’t one thing. It’s several, and they don’t share a config.
There’s the CLI — the php you type at the shell. There’s the Apache module, mod_php, if you’re running that. And there’s PHP-FPM, the pool of worker processes that actually serves your pages on most modern setups. Each of these is a SAPI — a Server API — and on Debian and Ubuntu each one gets its own conf.d directory of enabled extensions. Same PHP version, same extension binaries on disk, separate lists of what’s switched on.
When I ran php -m, I was asking the CLI. The CLI had imagick. WordPress doesn’t run under the CLI — it runs under FPM. And FPM, it turned out, had never been told.
The .so was built and sitting in the modules directory. The mods-available/imagick.ini file existed. The CLI’s conf.d had a symlink to it, so the CLI saw it. FPM’s conf.d had nothing. From FPM’s side of the fence, the extension may as well not have been compiled at all — and WordPress, running inside FPM, reported exactly that.
How it got that way
This is where it stops being a general PHP gotcha and becomes a specific one, the kind worth writing down so nobody re-derives it at 11pm.
The install step in my provisioning script enabled the extension like this:
phpenmod -v "$PHP_VER" imagick
phpenmod is the Debian helper that drops the symlink into the right conf.d directories. Looks complete. The trap is in what “the right directories” means: without the -s flag, phpenmod only enables the extension for the SAPIs that exist at the moment you run it.
And the ordering on this particular upgrade path was the problem. The box was originally provisioned in the mod_php era — CLI and Apache, no FPM. This script ran back then, saw two SAPIs, wired imagick into both, job done. Later a tuning stage added PHP-FPM to the same box. FPM turned up after phpenmod had already run, so nothing ever created its symlink. CLI: had it. Apache: had it. FPM, the one serving every actual page: silently missed.
No error. Nothing failed. The extension was simply enabled everywhere except the one place that mattered, and the only thing that ever noticed was a Site Health check months later.
The fix
Two changes. First, enable across every SAPI, present or not, with -s ALL:
phpenmod -v "$PHP_VER" -s ALL imagick
-s ALL tells phpenmod to symlink into every installed SAPI’s conf.d, not just whichever ones happen to be around when it runs. If FPM shows up later on this version, it inherits the symlink. It’s the difference between “enable it for what I can see” and “enable it, full stop”.
Second — enabling isn’t the same as loading. A running SAPI won’t pick up a new extension until it’s told to re-read config. Apache I already reloaded. FPM I wasn’t, because when the script was first written FPM wasn’t part of the picture. The wrinkle is that the FPM service name carries the version — php8.2-fpm, php8.3-fpm, and so on — so you can’t just hard-code it and hope. Ask the system what’s actually running:
FPM_SVC=$(systemctl list-units --type=service --state=active \
| awk '/php[0-9]+\.[0-9]+-fpm\.service/ {print $1; exit}')
if [ -n "$FPM_SVC" ]; then
systemctl reload "$FPM_SVC"
fi
Discover the service, reload it if it’s there, leave it alone if it isn’t. A box that genuinely has no FPM — a CLI-only worker, say — just skips the step instead of erroring on a service that was never installed.
I confirmed it the dull way. Ran phpenmod by hand on the affected site, reloaded FPM, refreshed Site Health. The warning went. Then I folded both changes back into the script so the next forty sites down the migration list never meet this at all.
The thing worth keeping
php -m on the command line tells you what the CLI can see. That’s it. It is not a report on what your website can see, and the two drift apart more easily than you’d think — a manual phpenmod here, a SAPI added after the fact there, and suddenly the extension list depends on which door you walked in through.
If you want to know what WordPress can see, ask WordPress. Site Health is checking the right SAPI, which is exactly why it caught something the shell was cheerfully hiding. When an extension is “definitely installed” but something still swears it’s missing, don’t re-run the install. Ask which PHP is doing the complaining — odds are it’s a different one to the PHP you keep checking.
