Install and audit PHP extensions for a production application
Translate application requirements to distribution packages, reject duplicate SAPIs, verify extension provenance, and capture a reproducible manifest.
Provide only required PHP extensions from trusted package sources with consistent CLI and FPM loading.
- PHP 8.2, 8.3, 8.4, 8.5
- Supported host and recovery access Use a supported distribution, keep an independent root-capable session, and record the current package repositories before changing the PHP runtime.
cat /etc/os-release && uname -m - Application compatibility Confirm the application, framework, extensions, and deployment tooling explicitly support the target PHP branch.
- Configuration backup Archive the active PHP, web-server, and application configuration before packages, pools, or handlers change.
OneLiners never runs these steps or stores secrets. Review placeholders, versions, current state, and change-control requirements before using a command.
Full guide
What you will build
- An auditable mapping from production Composer/application `ext-*` requirements to signed distribution or approved PHP-repository packages for the exact PHP ABI.
- A CLI and FPM inventory that proves configuration paths, extension directory, package provenance and absence of duplicate, wrong-ABI or failed module loads.
- A reproducible runtime manifest containing PHP version, modules, package versions/repositories and platform checks, with an explicit high-friction gate for any PECL-only extension.
- Only required production extensions are installed by the explicitly selected package manager; optional modules and build toolchains are excluded unless justified.
- CLI and the intended `{{fpmService}}` load the same required modules without startup warnings, while a protected web-SAPI health check confirms the application-facing set.
- Every package/exception has an owner, source, version and removal path; application platform checks pass and rollback can restore prior packages plus INI links.
Architecture
How the parts fit together
Application and Composer metadata declare capabilities such as `ext-intl`, not operating-system package names. The operator normalizes those requirements, maps each to the approved repository package for PHP `{{phpVersion}}`, previews and installs an explicit package list, then validates both CLI and FPM. Package-manager metadata establishes origin and update ownership; PHP INI scan directories decide whether a module loads in each SAPI. PECL is an exception path because it compiles or downloads code outside the normal distribution lifecycle. A build-info manifest links the deployed application lock file to the exact runtime package/module set.
- Composer/application requirements become a reviewed normalized `ext-*` list.
- Each capability maps to one approved package candidate with PHP ABI, architecture and repository provenance.
- The chosen package manager installs only the allowlist; FPM validates/reloads and both SAPIs are checked.
- A manifest and protected application health check bind deployed code to the runtime state.
Assumptions
- The application has a committed `composer.lock` or equivalent authoritative production extension list and a staging environment for native-module testing.
- The exact PHP `{{phpVersion}}`, CLI/FPM binaries, `{{fpmService}}`, repository policy and rollback package inventory are known.
- `{{packageManager}}` is literally `apt` or `dnf`; `{{phpPackages}}` is a reviewed space-separated package allowlist, never untrusted input.
- The protected extension-health route on `{{domain}}` discloses only pass/fail capability names and is inaccessible to the public.
Key concepts
- extension ABI
- The binary interface tying a compiled extension to a compatible PHP/Zend build and architecture.
- SAPI
- A PHP execution interface such as CLI or FPM; each can scan different INI files and load different modules.
- extension requirement
- An application capability named `ext-*`, which still needs mapping to a distribution-specific package.
- provenance
- Evidence of the repository/package/version that supplied executable module code.
- PECL
- The PHP Extension Community Library and installer ecosystem; production use requires a separate source/build/update review.
- runtime manifest
- A release artifact recording exact PHP, package and loaded-module state.
Before you copy
Values used in this guide
{{appRoot}}Absolute deployed application root containing composer.lock and build-info.
Example: /srv/billing/current{{phpVersion}}Exact PHP branch used by CLI/FPM packages.
Example: 8.3{{fpmService}}Exact intended systemd FPM unit.
Example: php8.3-fpm{{packageManager}}Explicit supported package manager: `apt` or `dnf`.
Example: apt{{phpPackages}}Reviewed distribution package allowlist matching application requirements.
Example: php8.3-curl php8.3-intl php8.3-mbstring php8.3-xml{{extension}}One PECL-only extension candidate under review.
Example: redis{{domain}}Canary hostname with protected extension-health endpoint.
Example: billing.example.comSecurity and production boundaries
- Every native extension runs inside PHP workers with application data and credentials; minimize the set and patch it with the runtime.
- Do not use `pecl install latest`, random binary repositories, disabled GPG checks or a compiler on production as shortcuts.
- Package names and shell placeholders are operator-reviewed values, never values copied from a request or repository README.
- Health and manifest artifacts must not reveal secrets, full phpinfo output or private repository credentials.
Stop before continuing if
- Stop when a requirement cannot map to a supported ABI-matched package or the package transaction changes the intended PHP branch/unrelated software.
- Do not reload when CLI/FPM INI roots differ unexpectedly, startup warnings/duplicate loads exist, or package provenance is unknown.
- Do not accept until Composer platform checks, protected web-SAPI health and the runtime manifest all match the intended production set.
instruction
Extract extension requirements
Read composer.lock, framework checks, and application documentation; distinguish required extensions from optional accelerators.
Why this step matters
The package allowlist must begin with application capabilities, not an administrator's memory or a generic framework checklist.
What to understand
Use the locked production dependency graph and exclude development-only requirements; compare framework/runtime diagnostics.
Classify required versus optional extensions and document which feature/test proves each requirement.
System changes
- No persistent change; reads composer.lock and current platform requirements.
Syntax explained
composer check-platform-reqs --lock --no-dev- Checks production locked-package PHP/extension requirements.
jq ... ext-- Extracts normalized extension keys from locked package requirements.
Values stay on this page and are never sent or saved.
cd {{appRoot}} && composer check-platform-reqs --lock --no-dev 2>/dev/null || true; jq -r '.packages[].require // {} | keys[]' composer.lock 2>/dev/null | grep '^ext-' | sort -uA normalized ext-* requirement list is produced.
Checkpoint: Checkpoint: requirements
Continue whenA deduplicated production `ext-*` list identifies owner, purpose, required/optional status and functional test.
Stop whenNo authoritative lock/requirements exist, development requirements are mixed in, or an extension purpose/owner is unknown.
If this step fails
Composer check passes but extraction lists additional extensions.
Likely causeCurrent host already satisfies requirements or package metadata contains conditional/optional paths.
composer show --locked --treecomposer check-platform-reqs --lock --no-dev
ResolutionReview locked dependency metadata and application features; retain only explicit production requirements.
Security notes
- Do not execute untrusted Composer scripts during requirement inventory.
Alternatives
- Use the framework's official platform-requirement command plus a reviewed lockfile parser.
Stop conditions
- No authoritative lock/requirements exist, development requirements are mixed in, or an extension purpose/owner is unknown.
instruction
Inventory both CLI and FPM configuration
Record the exact binaries, loaded INI files, extension directories, and module lists for every SAPI.
Why this step matters
CLI and FPM can load different branches/directories; inventory prevents installing a correct package for the wrong SAPI.
What to understand
Record binary paths, PHP/Zend API, scan directories, extension_dir and loaded modules for both interfaces.
Map web handlers to the exact FPM service/socket before changing packages.
System changes
- No persistent change; records current CLI/FPM configuration and module state.
Syntax explained
php --ini- Lists CLI loaded/scanned INI files.
php -i- Shows API and extension directory details.
php-fpm... -i- Shows the FPM binary's configuration roots.
Values stay on this page and are never sent or saved.
php -v; php --ini; php -i | grep '^extension_dir'; php -m | sort; php-fpm{{phpVersion}} -i | grep -E '^(Loaded Configuration|Scan this dir|extension_dir)'CLI and FPM configuration roots are identified.
Checkpoint: Checkpoint: inventory
Continue whenExact branch, ABI, architecture, service, socket, INI roots, extension directory and baseline module list are archived.
Stop whenCLI/FPM unexpectedly differ, startup warnings already exist, or web handler/service mapping is unknown.
If this step fails
Versioned FPM binary is not found.
Likely causeEnterprise Linux uses unversioned `php-fpm` or the branch is installed in an SCL path.
command -v php-fpmsystemctl list-unit-files '*php*fpm*'rpm -qa 'php*' | sort
ResolutionIdentify the exact packaged binary/service and use its own guide; do not assume Debian naming.
Security notes
- Treat loaded module paths outside package-owned directories as supply-chain findings.
Alternatives
- Collect equivalent SAPI inventory through a signed host agent.
Stop conditions
- CLI/FPM unexpectedly differ, startup warnings already exist, or web handler/service mapping is unknown.
instruction
Map requirements to distribution packages
Use apt-cache or dnf repoquery to confirm package ownership, version, repository, and architecture before installation.
Why this step matters
An `ext-*` capability can map to different package names and dependencies; candidate version, origin and ABI must be reviewed before installation.
What to understand
Query only enabled approved repositories and record candidate/installed versions plus architecture.
Do not assume every example package is required; construct `{{phpPackages}}` from the requirement list.
System changes
- No persistent change; queries package metadata for candidate extension packages.
Syntax explained
apt-cache policy- Shows Debian-family candidate version and repository priority.
dnf repoquery --info- Shows Enterprise Linux package version, architecture and repository metadata.
Values stay on this page and are never sent or saved.
apt-cache policy php{{phpVersion}}-{curl,gd,intl,mbstring,mysql,pgsql,xml,zip} 2>/dev/null || dnf repoquery --info 'php-{curl,gd,intl,mbstring,mysqlnd,pgsql,xml,zip}'Candidate versions and repositories are recorded.
Checkpoint: Checkpoint: map packages
Continue whenEvery required extension maps to one ABI-compatible signed package; all candidate repositories are approved.
Stop whenAny mapping is ambiguous/missing, package belongs to another PHP branch, or origin is unapproved.
If this step fails
The expected package name is absent.
Likely causeDistribution bundles the module in another package, branch repository is disabled, or no compatible build exists.
apt-file search '/intl.so$' 2>/dev/null || dnf repoquery --whatprovides '*/intl.so'
ResolutionUse package ownership/repository metadata and official docs; stop if no supported build exists.
Security notes
- Never add a repository solely because its package name looks convenient.
Alternatives
- Choose a supported runtime/image that includes the needed extension.
Stop conditions
- Any mapping is ambiguous/missing, package belongs to another PHP branch, or origin is unapproved.
command
Install only the approved extension set
Prefer distribution or trusted versioned repository packages; select the reviewed package manager explicitly and avoid PECL unless no supported package exists.
Why this step matters
An explicit manager and allowlist make the state change reviewable; shell fallback could run a second manager after a genuine first-manager failure.
What to understand
Replace `{{packageManager}}` with literal `apt` or `dnf` and preview the exact transaction first.
`{{phpPackages}}` contains only reviewed package tokens; compare resulting PHP branch and provenance immediately.
System changes
- Installs the approved PHP extension package allowlist and associated INI links.
Syntax explained
case apt|dnf- Selects exactly one supported package manager and fails otherwise.
apt-get install --yes / dnf install -y- Applies the previously previewed explicit package list.
Values stay on this page and are never sent or saved.
case '{{packageManager}}' in apt) sudo apt-get install --yes {{phpPackages}};; dnf) sudo dnf install -y {{phpPackages}};; *) printf 'Choose apt or dnf\n' >&2; exit 1;; esacThe explicitly selected package manager completes without replacing the intended PHP branch.
Checkpoint: Checkpoint: install packages
Continue whenOnly allowlisted modules/dependencies install, PHP branch stays constant and package provenance is approved.
Stop whenPreview removes/replaces unrelated runtime, placeholder contains shell syntax, repository is unexpected, or a package script fails.
If this step fails
Command exits with `Choose apt or dnf`.
Likely causeThe placeholder was not replaced with an exact supported literal.
command -v apt-get dnfcat /etc/os-release
ResolutionSelect the host's actual reviewed package manager; use a separate guide for other systems.
Security notes
- Package placeholders are operator-controlled allowlists, never user input; retain signature checks.
Alternatives
- Bake the same approved packages into an immutable image.
Stop conditions
- Preview removes/replaces unrelated runtime, placeholder contains shell syntax, repository is unexpected, or a package script fails.
decision
Gate any PECL-only extension
If PECL is unavoidable, record maintainer, release, checksum, ABI, build dependencies, and removal procedure; never run an unpinned pecl install in production.
Why this step matters
PECL-only modules lack the normal distribution package lifecycle and need explicit source, checksum, ABI, build and update ownership.
What to understand
`remote-info` is discovery, not approval. Pin a release and retrieve source/checksum through an isolated build.
Document maintainer health, license, CVEs, PHP compatibility, tests, signing, repository packaging and removal.
System changes
- No persistent change; inspects PECL metadata and local phpize ABI. It intentionally does not install.
Syntax explained
pecl remote-info- Displays upstream release metadata for review.
phpize --version- Shows PHP/Zend API identifiers for build compatibility.
Values stay on this page and are never sent or saved.
pecl remote-info {{extension}} 2>/dev/null || true; phpize --version 2>/dev/null || trueThe extension is rejected or an explicit, pinned review record exists.
Checkpoint: Checkpoint: pecl gate
Continue whenCandidate is rejected or has a pinned, checksum-verified, reproducibly packaged and tested exception record.
Stop whenOnly `latest` is identified, source/checksum/maintainer/update/removal is unknown, or production compilation is proposed.
If this step fails
PECL lists a release but it does not support the active PHP API.
Likely causeExtension has not been ported/tested for this PHP branch.
pecl remote-info redisphpize --version
ResolutionStay on the supported runtime or wait/use another feature path; do not patch unknown native code on production.
Security notes
- Never pipe PECL output or untrusted build scripts into a privileged shell.
Alternatives
- Package the pinned source in CI as a signed internal DEB/RPM or avoid the extension.
Stop conditions
- Only `latest` is identified, source/checksum/maintainer/update/removal is unknown, or production compilation is proposed.
verification
Detect duplicate and failed loads
Check startup diagnostics and enabled INI links for extensions loaded more than once or against the wrong ABI without writing to a predictable shared temporary path.
Why this step matters
A package can install successfully while duplicate INIs or old manual binaries produce warnings, crashes or silently wrong module behavior.
What to understand
Print startup diagnostics/module list directly; avoid predictable shared temp paths susceptible to symlink/overwrite mistakes.
Search the exact FPM journal for failed loads, duplicate declarations and undefined symbols after installation.
System changes
- No persistent change; reads CLI startup/module output and the selected FPM journal.
Syntax explained
display_startup_errors=1- Makes module load failures visible for the diagnostic command.
journalctl -u {{fpmService}}- Reads the intended web SAPI service evidence.
grep unable/already/undefined- Selects common binary/load failure signatures.
Values stay on this page and are never sent or saved.
php -d display_startup_errors=1 -m 2>&1; sudo journalctl -u {{fpmService}} -n 50 --no-pager | grep -iE 'unable to load|already loaded|undefined symbol' || trueThe concrete module list is printed and no startup warning or duplicate load is present.
Checkpoint: Checkpoint: duplicate check
Continue whenConcrete module list contains each required module once and neither CLI nor FPM reports startup/load warnings.
Stop whenAny warning, duplicate, undefined symbol, unexpected module or manual unowned `.so` is present.
If this step fails
`already loaded` appears.
Likely causeThe module is declared in multiple INI files or compiled statically plus dynamically.
php --inigrep -R -n 'extension.*redis' /etc/php
ResolutionKeep one package-owned authoritative declaration and remove only the unintended duplicate.
Security notes
- Do not hide startup errors with logging filters; investigate binary-origin anomalies.
Alternatives
- Compare clean immutable image manifests to detect duplicate/unowned files before deployment.
Stop conditions
- Any warning, duplicate, undefined symbol, unexpected module or manual unowned `.so` is present.
command
Validate and reload FPM
Resolve the exact versioned or unversioned FPM binary, test it, and reload the inventoried service so the web SAPI receives the reviewed module set.
Why this step matters
New extension code reaches web requests only after the exact FPM configuration validates and workers reload; CLI success is insufficient.
What to understand
Resolve versioned/unversioned FPM binary, require `-tt`, then reload only `{{fpmService}}`.
Keep previous package/INI state and watch canary health/journal during worker replacement.
System changes
- Reloads the selected FPM unit so new workers load the approved extension set.
Syntax explained
command -v php-fpm...- Resolves the exact installed FPM parser.
-tt &&- Blocks reload when effective configuration is invalid.
systemctl is-active- Confirms the selected unit remains active.
Values stay on this page and are never sent or saved.
FPM_BIN="$(command -v php-fpm{{phpVersion}} || command -v php-fpm)"; sudo "$FPM_BIN" -tt && sudo systemctl reload {{fpmService}} && systemctl is-active {{fpmService}}FPM configuration succeeds and the selected service stays active.
Checkpoint: Checkpoint: reload
Continue whenFPM parser is clean, unit stays active, journal has no load errors and canary application health passes.
Stop whenParser/service/health fails, wrong unit is selected, or prior packages/INI cannot be restored.
If this step fails
Reload succeeds but app crashes on first use of an extension.
Likely causePresence test did not exercise runtime behavior, extension config/dependency/API path fails.
journalctl -u php8.3-fpm -n 150 --no-pagercurl -v https://billing.example.com/internal/extension-health
ResolutionRestore prior handler/package/INI or disable the one new module, reload and run extension-specific staging tests.
Security notes
- Canary native parsing/network/database functions with non-sensitive fixtures before broad traffic.
Alternatives
- Restart a separate canary pool/container with the new extension set.
Stop conditions
- Parser/service/health fails, wrong unit is selected, or prior packages/INI cannot be restored.
instruction
Write a reproducible runtime manifest
Record PHP version, packages with repositories, module hashes, and composer platform checks alongside deployment metadata.
Why this step matters
A release is reproducible only when its lock file can be tied to exact PHP, package and loaded-module state.
What to understand
Write build-info under an approved non-public release path with deployment identity/ownership, then checksum and version it.
Include repository provenance in production automation even though the compact quick path records package versions.
System changes
- Creates `{{appRoot}}/var/build-info/php-runtime.txt` and SHA-256 evidence.
Syntax explained
php -v / php -m- Records runtime branch and loaded CLI modules.
dpkg-query / rpm -qa- Records installed PHP package versions.
sha256sum- Provides an integrity identifier for the manifest contents.
Values stay on this page and are never sent or saved.
mkdir -p {{appRoot}}/var/build-info; { php -v; php -m | sort; dpkg-query -W 'php*' 2>/dev/null || rpm -qa 'php*' | sort; } > {{appRoot}}/var/build-info/php-runtime.txt; sha256sum {{appRoot}}/var/build-info/php-runtime.txtA versioned PHP runtime manifest and checksum are produced.
Checkpoint: Checkpoint: manifest
Continue whenManifest is non-public, tied to release/lock, contains exact runtime/package/module state and Composer platform checks pass.
Stop whenPath is publicly served/writable by attackers, output contains secrets, or manifest differs from the canary host/runtime.
If this step fails
Two supposedly identical hosts produce different manifests.
Likely causePackage repositories/versions, INI links, PHP branch or deployment drift differs.
diff -u host-a-php-runtime.txt host-b-php-runtime.txt
ResolutionReconcile through the approved package/image release; do not normalize away unexplained differences.
Security notes
- Manifest exposes versions useful to attackers; store it as internal release evidence.
Alternatives
- Attach a signed SBOM/runtime attestation produced by the immutable build pipeline.
Stop conditions
- Path is publicly served/writable by attackers, output contains secrets, or manifest differs from the canary host/runtime.
Finish line
Verification checklist
cd {{appRoot}} && composer check-platform-reqs --no-devAll production PHP and extension requirements pass.curl --fail --silent https://{{domain}}/internal/extension-healthThe protected application health check confirms the required web-SAPI extensions.Recovery guidance
Common problems and safe checks
CLI loads an extension but the application says it is missing.
Likely causeFPM scans a different INI directory/branch, service was not reloaded, or the web handler reaches another pool.
php --inisudo php-fpm8.3 -ttsystemctl status php8.3-fpm --no-pager
ResolutionMap and correct the exact FPM scan/handler/service; do not add a second blind extension declaration.
Startup reports `undefined symbol` or wrong API number.
Likely causeA binary was built for another PHP ABI/architecture or an old manual `.so` shadows the packaged module.
php -i | grep -E 'PHP API|Zend Extension Build|extension_dir'file /usr/lib/php/*/*.sodpkg -S /path/to/module.so 2>/dev/null || rpm -qf /path/to/module.so
ResolutionRemove only the unmanaged/wrong-ABI file, reinstall the matching signed package and retest both SAPIs.
Package install proposes replacing the PHP branch.
Likely causeGeneric package names resolve through another repository/stream or the allowlist mixes version families.
apt-get install --simulate php8.3-intl 2>/dev/null || dnf install --assumeno php-intlphp -v
ResolutionCancel, pin/map packages to the active approved branch and preview again.
Reference
Frequently asked questions
Is `php -m` enough?
No. It proves CLI loading only. FPM and a real application path must be checked independently.
Why not install every common extension?
Each module adds executable code, configuration, patching and potential parsing risk. Install requirements, not a generic mega-list.
Can Composer install PHP extensions?
Composer expresses/checks platform requirements but native modules are normally supplied by the OS/PHP packaging or a separately governed build.
Recovery
Rollback
Remove only newly approved extension packages and restore prior INI state if an application regression occurs.
- Restore the archived conf.d links and package list, then validate FPM.
- Remove newly installed packages with the distribution package manager; review the transaction before approval.
- Reload FPM and run platform and application smoke tests.
Evidence