Commit the Lockfile: Use composer install in CI for Deterministic PHP Deploys
Commit composer.lock for apps and run composer install in CI. Update locally to move constraints, install everywhere else to reproduce the exact dependency graph with Composer 2.
27 Mar 2026, 14:03 UTC

A build passes on your laptop, fails in CI, and the error is a missing method in a dependency you did not change. The useful takeaway is that composer.json expresses intent, composer.lock records the exact resolved graph, and the engineering decision is to update locally and install everywhere else.
The problem is non-deterministic resolution
composer.json holds constraints like "monolog/monolog": "^3.0". The caret ^ allows non-breaking minor and patch updates within a major version. That is intentional flexibility for developers.
composer.lock holds the concrete versions that were chosen for that constraint set, plus the full transitive dependency tree with hashes. With Composer 2, install reads the lockfile and reproduces the same graph on any machine where the lockfile is present.
composer install installs from the lockfile. composer update recalculates the graph against composer.json constraints and rewrites composer.lock. The two commands have different safety profiles.
Separate intent from resolution
Think of composer.json as the specification and composer.lock as the build artifact.
For applications, commit both files. For reusable libraries, the common practice is to commit only composer.json, because consumers should resolve against their own constraints.
Platform requirements declared in composer.json, such as "php": "^8.1" and extensions, are enforced on install. A mismatch between local and CI PHP extensions will cause install to fail even with a valid lockfile.
Version assumptions to verify
Run in the project root:
composer --versionThis confirms the major version. Modern workflows assume Composer 2, where install is faster and lockfile handling is stable. Behavior differs between Composer 1 and Composer 2.
Check the lockfile exists alongside composer.json:
ls composer.lock composer.jsonInspect the packages section to see pinned versions, e.g., monolog/monolog 3.5.0 rather than a range.
Worked example: update locally, install in CI
Local developer machine, user with write permission to the project and vendor directory.
composer update --no-interactionThis recalculates the graph, updates composer.lock, and writes vendor. Expected check: composer.lock modification time changes and the packages list reflects new concrete versions within the allowed ranges. Risk: an allowed minor update can still introduce behavioral changes. Review the diff before committing.
Commit the updated lockfile:
git add composer.lock
git commit -m "chore(deps): update dependencies"CI or deployment container, typically a non-interactive user with write access to the working directory.
composer install --no-dev --optimize-autoloaderWith a lockfile present, install does not recalculate constraints. It installs the exact versions recorded. --no-dev omits require-dev packages. --optimize-autoloader generates a class map for faster autoloading in production.
Expected check: vendor directory is created with the same package versions as the lockfile. Compare with:
composer show --installedBefore and after a local update you can see how a range resolves to a concrete version.
Run a clean verification by removing vendor and reinstalling:
rm -rf vendor
composer install --no-devIf the lockfile is present, the install should succeed without network changes to constraints.
Trade-offs and limitations
Autoloader optimization changes runtime class loading behavior. It should be tested after enabling, especially with code that relies on dynamic class names.
Lockfile freshness is a trade-off. Pinning gives reproducibility, but it also means security patches are not applied until someone runs composer update locally and merges the lockfile.
Platform drift is a common failure mode. If composer.json requires ext-intl and the CI image lacks it, install fails. The lockfile does not relax platform requirements.
For libraries, committing composer.lock can be misleading because consumers will resolve their own graph. Keep the lockfile for applications.
Actionable closing
Use this rule: update locally, install everywhere else.
- Developers run composer update intentionally, review the lockfile diff, and commit it.
- CI and production run composer install --no-dev --optimize-autoloader and never run update.
- Verify Composer 2 is used, the lockfile is present, and platform requirements match the target environment.
That separation gives you reproducible builds without sacrificing the ability to move constraints deliberately.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.