Install Composer securely and use reproducible production dependencies
Verify the official installer checksum, install Composer in a controlled path, enforce lock files, audit dependencies, and deploy without development packages.
Avoid piping unverified installers into PHP and produce a dependency tree reproducible from composer.lock.
- Composer 2.x
- 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
- A root-owned Composer 2 executable installed only after the official installer and an independently fetched SHA-384 signature match exactly.
- A production dependency workflow that treats `composer.lock` as the resolved artifact, validates schema/lock consistency, audits advisories and previews project scripts before execution.
- A non-root, non-interactive production install using the locked package set, no development dependencies, optimized autoloading and application platform/health verification.
- No unverified network response is piped into PHP or a root shell; the installer runs unprivileged and root only copies a verified local PHAR to a fixed path/mode.
- `composer.json` and `composer.lock` agree, advisory and platform checks pass or have documented exceptions, and the production install does not resolve new versions.
- Vendor files have the deployment identity, scripts/plugins are explicitly trusted, build artifacts are reproducible, and rollback restores the previous release/Composer binary.
Architecture
How the parts fit together
The official signature endpoint supplies the expected SHA-384 digest independently from the installer URL. The installer is downloaded but not executed until the local digest matches. It creates `composer.phar` as the unprivileged operator; `sudo install` performs only a fixed file copy to root-owned `/usr/local/bin/composer`. At project level, `composer.json` defines constraints while `composer.lock` records exact resolved packages and content references. `composer install` consumes the lock; `update` changes it and belongs in reviewed development/CI. Plugins and scripts execute code with the install user's permissions, so they are inspected in a no-scripts dry run before an approved production build.
- Expected digest and installer are retrieved separately; exact SHA-384 equality gates execution.
- The installer creates a local PHAR and a fixed root-owned copy becomes the system executable.
- Project schema/lock, advisories, platform and script/plugin behavior are reviewed without mutation.
- The deployment user installs the exact production lock and application checks prove the resulting release.
Assumptions
- Trusted DNS/TLS/time/CA configuration reaches only official Composer endpoints; outbound proxy behavior is known.
- PHP CLI with OpenSSL supports Composer 2, and an existing Composer binary plus project release can be restored.
- `{{appRoot}}` is a trusted application checkout with committed composer.json/lock, and `{{appUser}}` is its managed non-root deployment identity.
- Plugins and scripts in the locked graph have been reviewed in CI; production network access, credentials and writable paths are minimized.
Key concepts
- installer signature
- The official expected SHA-384 digest used to verify installer bytes before PHP executes them.
- lock file
- The exact resolved dependency versions and references installed in production.
- install versus update
- `install` follows the lock; `update` changes dependency resolution and must not occur implicitly during deployment.
- Composer plugin
- A package that can execute code inside Composer and therefore has supply-chain privileges.
- Composer script
- Project/package-defined command triggered by lifecycle events with the installer user's permissions.
- platform requirement
- A required PHP version, extension or library that must exist outside the vendor tree.
Before you copy
Values used in this guide
{{appRoot}}Absolute trusted project/release directory.
Example: /srv/billing/releases/2026-07-25.1{{appUser}}Managed non-root deployment identity owning vendor/build output.
Example: billingSecurity and production boundaries
- A correct checksum protects against mismatched installer bytes but not a compromised official release process; retain TLS, endpoint and version evidence.
- Composer plugins/scripts execute code. Review `allow-plugins`, lock changes and lifecycle hooks; use `--no-scripts` for discovery, not as an automatic final policy.
- Never run project dependency installation as root and never expose repository tokens through command lines, logs or committed auth.json.
- An advisory-free result is time-bound; schedule repeated audits and rebuild deployed releases for security updates.
Stop before continuing if
- Stop on signature length/mismatch, TLS failure, unexpected redirect, unsupported PHP/OpenSSL or existing binary with unknown origin.
- Do not install dependencies when lock is missing/inconsistent, plugins/scripts are unreviewed, advisories/platform requirements fail without approved exception, or command runs as root.
- Do not accept until ownership, autoloader, application health, lock identity and rollback release are proven.
instruction
Inspect PHP and any existing Composer
Record executable resolution, PHP version, OpenSSL support, and the existing Composer origin before replacement.
Why this step matters
Replacing an unknown Composer binary or using an unsupported PHP/OpenSSL runtime can break builds and erase provenance.
What to understand
Record executable resolution, version/source/owner/hash and PHP/OpenSSL before download.
Check application-required Composer/PHP constraints and archive the current executable for rollback.
System changes
- No persistent change; inventories existing Composer and PHP/OpenSSL.
Syntax explained
command -v composer- Shows the executable selected by PATH.
composer --version- Reports Composer version when usable.
OPENSSL_VERSION_TEXT- Confirms PHP's TLS library is available.
command -v composer || true; composer --version 2>/dev/null || true; php -v; php -r 'echo OPENSSL_VERSION_TEXT, PHP_EOL;'Runtime and current Composer state are recorded.
Checkpoint: Checkpoint: inspect
Continue whenCurrent binary origin/hash/owner/version and compatible PHP/OpenSSL are documented with a rollback copy.
Stop whenExisting origin is unknown, PATH is shadowed, PHP unsupported, TLS trust/time broken or rollback absent.
If this step fails
Composer resolves to a project-local or user-writable path.
Likely causePATH precedence, alias/wrapper or prior manual install shadows the system binary.
type -a composerls -l "$(command -v composer)"sha256sum "$(command -v composer)"
ResolutionSelect and document one controlled binary path before replacement.
Security notes
- User-writable PATH entries can substitute executable code; use absolute `/usr/local/bin/composer` in automation.
Alternatives
- Use a pinned containerized Composer executable in CI.
Stop conditions
- Existing origin is unknown, PATH is shadowed, PHP unsupported, TLS trust/time broken or rollback absent.
command
Fetch the expected installer checksum over HTTPS
Retrieve the checksum from Composer's official installer-signature endpoint independently of the installer body.
Why this step matters
The expected digest must come from the official independent signature endpoint and have the exact SHA-384 hex shape.
What to understand
Fetch over PHP's TLS stack after validating time/CA/proxy; require exactly 96 hex characters in production automation.
Keep it only for the short verification transaction and log a redacted digest/version record.
System changes
- No filesystem change; retrieves expected digest into the current shell.
Syntax explained
composer.github.io/installer.sig- Official endpoint for the current installer SHA-384.
${#EXPECTED} -eq 96- Rejects missing/truncated/non-SHA-384-length response.
EXPECTED="$(php -r "copy('https://composer.github.io/installer.sig', 'php://stdout');")"; test "${#EXPECTED}" -eq 96; printf '%s\n' "$EXPECTED"A 96-character SHA-384 checksum is returned.
Checkpoint: Checkpoint: download signature
Continue whenA 96-character hexadecimal expected digest is received from the official endpoint over trusted TLS.
Stop whenResponse is wrong length/non-hex, TLS/proxy/redirect is unexpected, or endpoint is not official.
If this step fails
EXPECTED is empty or PHP warns `copy` failed.
Likely cause`allow_url_fopen`, CA trust, DNS, proxy or outbound policy blocks retrieval.
php -i | grep allow_url_fopenphp -r 'var_dump(openssl_get_cert_locations());'
ResolutionUse an approved TLS downloader and official endpoint with equivalent verification; do not paste a checksum from an unofficial page.
Security notes
- Do not disable TLS verification or use HTTP.
Alternatives
- Fetch the signature and installer as pinned CI inputs in a trusted build environment.
Stop conditions
- Response is wrong length/non-hex, TLS/proxy/redirect is unexpected, or endpoint is not official.
command
Download the installer without executing it
Save composer-setup.php locally so its digest can be compared before any code runs.
Why this step matters
Saving without execution separates network retrieval from the code-execution decision and permits exact local hashing.
What to understand
Write to a new protected working directory, inspect file type/size and hash it before PHP execution.
A successful download is not trust; only equality with the separately fetched expected digest is.
System changes
- Creates local `composer-setup.php` and computes its SHA-384.
Syntax explained
getcomposer.org/installer- Official Composer installer endpoint.
hash_file('sha384', ...)- Computes the local installer digest without executing it.
php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');"; ACTUAL="$(php -r "echo hash_file('sha384', 'composer-setup.php');")"; printf '%s\n' "$ACTUAL"The installer is present and its SHA-384 digest is displayed.
Checkpoint: Checkpoint: download installer
Continue whenA plausible installer file exists and its 96-character actual digest is captured; no code has run.
Stop whenDownload is empty/HTML/unexpected, permissions are broad, TLS failed or no independent expected digest exists.
If this step fails
Downloaded file contains an error page.
Likely causeProxy/captive portal/rate limit intercepted the official response.
file composer-setup.phphead -n 2 composer-setup.phpwc -c composer-setup.php
ResolutionDelete it, repair approved network path and refetch; never execute based on extension alone.
Security notes
- Use a private working directory and avoid a predictable shared `/tmp` target.
Alternatives
- Download as a pinned CI artifact with digest/provenance.
Stop conditions
- Download is empty/HTML/unexpected, permissions are broad, TLS failed or no independent expected digest exists.
verification
Require an exact checksum match
Stop immediately and delete the installer if the independent expected and actual SHA-384 values differ.
Why this step matters
Exact digest equality is the mandatory execution gate; visual similarity or HTTPS alone is insufficient.
What to understand
Refetch the official expected digest in the same controlled step or persist it securely from the prior step.
On mismatch, delete the file and investigate; never retry execution with a bypass.
System changes
- No persistent change when successful; compares strings and prints concrete verification evidence.
Syntax explained
test "$EXPECTED" = "$ACTUAL"- Requires byte-for-byte digest equality.
&& echo- Prints success only after the equality gate.
EXPECTED="$(php -r "copy('https://composer.github.io/installer.sig', 'php://stdout');")"; ACTUAL="$(php -r "echo hash_file('sha384', 'composer-setup.php');")"; test "$EXPECTED" = "$ACTUAL" && echo 'Installer verified'Installer verified
Checkpoint: Checkpoint: verify installer
Continue whenCommand prints exactly `Installer verified`; actual and expected digests are retained in change evidence.
Stop whenNo exact match, values are empty/malformed, or official endpoint identity cannot be established.
If this step fails
Digests differ between immediate refetches.
Likely causeInstaller changed upstream during the window, caching/interception or corrupted transfer.
sha384sum composer-setup.phprm -f composer-setup.php
ResolutionDelete and later refetch both values as one new transaction after validating official release state/network.
Security notes
- Never use an old copied digest to approve a newer installer without an explicit pinned-version process.
Alternatives
- Pin a specific Composer PHAR release and verify its published signature/checksum in CI.
Stop conditions
- No exact match, values are empty/malformed, or official endpoint identity cannot be established.
command
Install Composer to a controlled system path
Run the verified installer without root to create a local PHAR, then use root only for a fixed-path, fixed-mode install; never run application dependency installs as root.
Why this step matters
The installer does not need root; restricting privilege to a fixed `install` copy minimizes the code executed with administrative rights.
What to understand
Run verified PHP installer as the current controlled operator, creating local `composer.phar`.
`sudo install` copies one known file to a fixed root-owned 0755 path; remove temporary executable and installer afterward.
System changes
- Creates/replaces root-owned `/usr/local/bin/composer` and removes local installer/PHAR.
Syntax explained
--filename=composer.phar- Creates a local PHAR without privileged destination.
install -o root -g root -m 0755- Copies to fixed path with explicit metadata.
rm -f- Removes verified temporary installer artifacts after successful copy.
php composer-setup.php --filename=composer.phar; sudo install -o root -g root -m 0755 composer.phar /usr/local/bin/composer; rm -f composer-setup.php composer.phar; /usr/local/bin/composer --versionComposer 2.x reports its version from root-owned /usr/local/bin/composer.
Checkpoint: Checkpoint: install
Continue whenAbsolute path reports Composer 2.x, file is root-owned 0755, hash/version are recorded and prior binary remains recoverable.
Stop whenInstaller asks for unexpected action, PHAR differs after copy, destination is symlink/unapproved, or version is incompatible.
If this step fails
System `composer` still runs another version.
Likely causePATH resolves another earlier binary or shell command cache is stale.
type -a composerhash -r; command -v composer/usr/local/bin/composer --version
ResolutionUse the absolute controlled path in automation and reconcile PATH intentionally.
Security notes
- Do not run `composer install` as root merely because the binary is root-owned.
Alternatives
- Install the verified PHAR in a build image or use a supported distribution Composer package.
Stop conditions
- Installer asks for unexpected action, PHAR differs after copy, destination is symlink/unapproved, or version is incompatible.
verification
Validate composer.json and composer.lock
Require strict schema validity and a lock file consistent with declared requirements before production install.
Why this step matters
Production must not resolve from inconsistent manifests; strict validation and a committed lock make dependency intent reviewable.
What to understand
`composer validate --strict` checks schema and lock consistency without installing.
Review lock changes, repositories, plugins and scripts in code review; reject untrusted VCS/path repositories.
System changes
- No dependency mutation; validates project metadata and presence of composer.lock.
Syntax explained
--strict- Turns relevant warnings into a failing validation result.
--no-check-publish- Skips package-publishing checks irrelevant to an application.
test -f composer.lock- Requires a reproducible resolved dependency graph.
Values stay on this page and are never sent or saved.
cd {{appRoot}} && composer validate --strict --no-check-publish && test -f composer.lockcomposer.json is valid and composer.lock exists.
Checkpoint: Checkpoint: validate project
Continue whenSchema is valid, lock matches composer.json, source repositories/plugins/scripts are approved and lock diff is reviewed.
Stop whenLock absent/outdated/conflicted, unknown repository/plugin/script appears, or update is proposed on production.
If this step fails
Validation says lock is not up to date.
Likely causecomposer.json changed without a reviewed `composer update` in development/CI.
git diff -- composer.json composer.lockcomposer validate --strict --no-check-publish
ResolutionResolve and review lock in development/CI, commit both files and redeploy.
Security notes
- Dependency metadata is executable supply-chain policy; require trusted code review.
Alternatives
- Fail the CI build before an artifact can reach production.
Stop conditions
- Lock absent/outdated/conflicted, unknown repository/plugin/script appears, or update is proposed on production.
instruction
Audit locked dependencies and scripts
Review advisories and package scripts; use --no-scripts for inspection before allowing project-defined hooks in a trusted build.
Why this step matters
Advisories, scripts and plugins must be reviewed before the state-changing production install; a no-scripts dry run reveals the locked package transaction.
What to understand
`composer audit --locked` checks known advisories at that moment; document any explicit exception with expiry.
Dry-run/no-scripts prevents lifecycle hooks during inspection but final approved hooks still need sandboxed CI tests.
System changes
- No vendor mutation; queries advisory data and simulates the production dependency transaction.
Syntax explained
audit --locked- Checks advisories for the exact locked graph.
install --dry-run- Shows intended package operations without writing vendor.
--no-dev- Excludes development-only packages.
--no-scripts- Suppresses project/package lifecycle scripts during inspection.
Values stay on this page and are never sent or saved.
cd {{appRoot}} && composer audit --locked; composer install --dry-run --no-dev --prefer-dist --no-interaction --no-scriptsNo blocking advisories are reported and the dry run lists the locked production set.
Checkpoint: Checkpoint: audit
Continue whenNo unapproved blocking advisory, transaction matches the lock and every plugin/script is understood before final install.
Stop whenAdvisory is unassessed, network source changes, dry-run resolves new versions, or script/plugin behavior is unknown.
If this step fails
Audit cannot reach advisory service.
Likely causeOffline/network/TLS outage; absence of results is not a clean result.
composer audit --locked -vvv
ResolutionUse cached/signed advisory data or restore approved connectivity; record that audit is incomplete and stop release when policy requires it.
Security notes
- Never treat a failed audit command as zero vulnerabilities.
Alternatives
- Run advisory/SBOM scanning in CI and attach signed results to the release.
Stop conditions
- Advisory is unassessed, network source changes, dry-run resolves new versions, or script/plugin behavior is unknown.
command
Install the locked production dependency set
Run as the deployment identity, exclude development dependencies, prefer archives, optimize autoloading, and avoid interactive mutation.
Why this step matters
The final install must consume the reviewed lock as a non-root deployment identity and produce deterministic optimized autoload files.
What to understand
`--no-dev --prefer-dist --no-interaction` narrows production input; classmap-authoritative requires framework compatibility testing.
Scripts/plugins execute as `{{appUser}}`; run them only after review, with minimal credentials/network/writable paths and atomic release rollback.
System changes
- Creates/updates `vendor` and Composer-generated autoload files under `{{appRoot}}` as `{{appUser}}`.
Syntax explained
sudo -u {{appUser}}- Prevents root-owned vendor files and root script execution.
--working-dir- Selects the exact trusted release.
--no-dev --prefer-dist- Installs production archives from the lock.
--optimize-autoloader --classmap-authoritative- Builds optimized authoritative class maps; use only when compatible.
Values stay on this page and are never sent or saved.
sudo -u {{appUser}} composer install --working-dir={{appRoot}} --no-dev --prefer-dist --no-interaction --optimize-autoloader --classmap-authoritativeDependencies match composer.lock and optimized autoload files are generated.
Checkpoint: Checkpoint: production install
Continue whenInstalled packages match lock, ownership is the deployment identity, platform/health tests pass and no network resolution changed versions.
Stop whenCommand runs as root, project path/lock is untrusted, plugin/script needs broad secrets, platform fails, or application canary regresses.
If this step fails
Class not found appears only with authoritative classmap.
Likely causeApplication generates/discovers classes dynamically outside Composer's classmap.
sudo -u billing composer dump-autoload --working-dir=/srv/billing/current -ogrep -R -n 'Class not found' /srv/billing/current/var/log
ResolutionRestore prior release or remove `--classmap-authoritative` after performance/compatibility review; fix autoload metadata in development.
Security notes
- Never expose Composer auth credentials to application runtime or retain them in the release.
Alternatives
- Build vendor in isolated CI and deploy a signed immutable release without Composer on production.
Stop conditions
- Command runs as root, project path/lock is untrusted, plugin/script needs broad secrets, platform fails, or application canary regresses.
Finish line
Verification checklist
cd {{appRoot}} && composer diagnose && composer check-platform-reqs --no-devComposer configuration and production platform requirements pass.cd {{appRoot}} && composer install --dry-run --no-dev --prefer-dist --no-interactionThe dry run reports no dependency changes.Recovery guidance
Common problems and safe checks
Expected and actual installer SHA-384 differ.
Likely causePartial/cached/intercepted download, changed official installer between fetches or wrong endpoint.
rm -f composer-setup.phpphp -r "copy('https://composer.github.io/installer.sig','php://stdout');"
ResolutionDelete the installer, verify time/TLS/proxy and refetch both independently; never execute or override the mismatch.
`composer install` wants to update lock or reports lock inconsistency.
Likely causecomposer.json changed without reviewed update, merge conflict or unsupported Composer/plugin metadata.
composer validate --strict --no-check-publishgit diff -- composer.json composer.lock
ResolutionRegenerate/review the lock in development/CI, commit it, and deploy that reviewed revision; never update production ad hoc.
Production install fails platform requirements.
Likely causePHP branch/extension/lib differs from CI or lock was produced with ignored platform requirements.
composer check-platform-reqs --no-devphp -vphp -m | sort
ResolutionAlign the runtime through approved packages/image and rebuild; do not use `--ignore-platform-reqs` as deployment policy.
Reference
Frequently asked questions
Why fetch the signature separately?
A digest embedded beside the downloaded installer would not independently detect modified bytes. The official signature endpoint provides the comparison value.
Why not run the verified installer with sudo?
It is unnecessary. The installer can create a local PHAR; root only performs a fixed-path file copy with explicit ownership/mode.
Can production run `composer update`?
No. It resolves new versions and changes the lock. Resolve/review in development or CI, then production uses `install` on the committed lock.
Recovery
Rollback
Restore the prior Composer binary and vendor release artifact without editing composer.lock on the server.
- Restore the previous /usr/local/bin/composer or distribution package from the recorded source.
- Atomically switch the application symlink back to the previous release containing its own vendor tree.
- Never run composer update as a rollback mechanism on production.
Evidence