Composer Lock Files for Small PHP Applications - Deploy the Reviewed Dependencies
A PHP application may deploy successfully on Monday and fail on Friday even when its own source code has not changed. One possible cause is easy to overlook: the two deployments did not receive the same dependency versions. A version constraint such as ^3.0 describes a range, not one immutable package release.
Composer addresses this problem with composer.lock. The lock file is simple enough to commit and forget, but that simplicity can hide two opposite mistakes: ignoring it entirely, or treating it as proof that the whole production environment is reproducible. Neither view is quite right.
This article asks a narrower question: how can a small PHP application deploy the dependency set that was actually reviewed, while remaining honest about what the lock file does not control?
Think of the manifest as policy and the lock as a snapshot
composer.json declares what the project is allowed to use. It can require PHP, extensions, application packages, development tools, and version ranges. Composer then resolves those constraints together with every transitive dependency.
composer.lock records the result of that resolution. For an application, a useful mental model is:
composer.jsonis the dependency policy.composer.lockis the reviewed snapshot selected under that policy.vendor/is an installation of that snapshot for a particular release.
This is an explanatory model rather than Composer terminology, but it captures an important distinction. According to Composer's basic usage documentation, update resolves dependencies and writes their exact versions to the lock file. When a lock file already exists, install uses the versions recorded there.
For an application, the lock file should normally be committed with the source. That lets a developer machine, CI job, and deployment process request the same package versions. Reusable libraries have different considerations because their consumers perform dependency resolution, but that is outside this article's scope.
update makes a decision; install applies it
The command names sound similar, yet they belong in different stages.
composer update asks Composer to solve the dependency graph again. A constraint may now select a newer package, and transitive packages may also change. The resulting lock-file diff is therefore part of the change being proposed. It deserves review and testing just like an application-code diff.
composer install, when given a lock file, applies the recorded selection. Running update directly during deployment combines two events that are safer to separate: choosing new third-party code and releasing the application. A failed deployment then becomes harder to explain because the server made a dependency decision that was absent from the reviewed commit.
A modest workflow is:
- Run a deliberate dependency update in a development branch or maintenance change.
- Review both
composer.jsonandcomposer.lock. - Run the application's tests and relevant smoke checks.
- Commit the lock file with the change.
- Deploy that commit with
composer install, not an unreviewedcomposer update.
This does not make updates risk-free. It makes the decision visible, testable, and easier to reverse.
A small deployment sequence
The exact release process depends on the application, but the following commands show the responsibilities that should be represented somewhere in the pipeline. They are separate commands intentionally; a deployment system should stop when any required gate fails.
1. Validate the manifest and lock
composer validate --strict
The Composer CLI reference says validate checks composer.json and, when a lock file exists, whether it is up to date with the manifest. --strict also gives warnings a non-zero exit status. This is a useful consistency gate, not a substitute for application tests.
2. Install the reviewed production set
composer install --no-dev --optimize-autoloader --no-interaction
--no-dev skips packages in require-dev and development autoload rules. That reduces production-only dependencies, but it also exposes a configuration mistake if runtime code secretly relies on a development package. Such a failure should be fixed in the dependency declarations rather than hidden by installing every development tool in production.
--optimize-autoloader converts PSR-0 and PSR-4 rules into a class map. Composer's autoloader optimization guide recommends this first optimization level for production. The more aggressive --classmap-authoritative option is not a universal upgrade: the same guide warns that it can break software that generates classes at runtime.
--no-interaction prevents a deployment from waiting indefinitely for terminal input. It does not answer every operational question automatically; plugin permissions and authentication still need explicit, preconfigured decisions.
3. Check the real runtime platform
composer check-platform-reqs --no-dev
Composer represents PHP, extensions, and some system libraries as platform packages. A lock file can preserve package versions while the destination server still lacks ext-intl, runs an incompatible PHP version, or differs in another required platform capability.
The check-platform-reqs documentation states that this command checks the real PHP and extension environment and ignores any simulated config.platform values. It should run on the deployment target or on the final runtime image, not only on a developer laptop.
4. Run application-specific checks before switching traffic
No generic Composer command can prove that routes render, database access works, configuration is present, or a queue consumer can start. A release still needs checks appropriate to the application. For a small site, that might mean a health endpoint, one read-only database operation, and requests to a few critical pages.
If possible, build the new release in a separate directory and switch to it only after these checks pass. Retaining the previous release makes rollback a directory or symlink change rather than an attempt to reconstruct yesterday's dependency graph during an incident.
The platform setting is a target, not evidence
Composer's config.platform option can emulate a target PHP or extension version during dependency resolution. This is useful when development runs a newer PHP version than production: it can stop the resolver from selecting packages that require more than production provides.
However, an emulated platform can also conceal reality. If the configuration claims PHP 8.x while a command actually runs on an older interpreter, resolution may succeed and runtime may fail. Composer therefore recommends pairing this setting with check-platform-reqs.
There are two separate questions:
- Was the dependency set resolved for the intended target?
- Does the machine that will run it actually satisfy that target?
config.platform helps with the first. A check against the real runtime helps with the second.
Scripts and plugins make installation more than file copying
It is tempting to describe composer install as merely downloading archives. That is incomplete. Composer supports scripts and plugins, and its security FAQ notes that they can execute third-party code with the privileges of the account running Composer.
This leads to two practical cautions. First, avoid running Composer as root merely because deployment needs to write files; give a dedicated deployment account the narrow permissions it needs. Second, know which scripts and plugins the application expects. The flags --no-scripts and --no-plugins reduce code execution, but enabling them blindly may skip required application setup. They are policy choices, not decorative hardening switches.
The lock file makes the selected package set inspectable. It does not make every selected package trustworthy, nor does it neutralize code that legitimately runs during installation.
What the lock file does not lock
Composer documents that repeated installation of the same lock can produce identical vendor/ contents apart from timestamps. That is a valuable and fairly specific guarantee. It should not be stretched into “the servers are identical.”
The lock file alone does not freeze:
- the PHP interpreter and its configuration;
- installed PHP extensions and linked system libraries;
- the Composer executable used by the pipeline;
- operating-system packages, environment variables, secrets, and service configuration;
- database schema and persistent data;
- external services called by the application;
- the effects of Composer scripts, plugins, or application build steps.
A stronger deployment records or controls these layers separately: for example, a pinned container base or documented server package set, a known Composer release, schema migrations, configuration validation, and a release artifact that can be retained. Not every personal site needs a sophisticated artifact platform, but every site benefits from knowing which assumptions remain outside Composer.
Locked does not mean maintained
A lock file intentionally prevents surprise upgrades. The same stability can preserve an old vulnerable or abandoned dependency if nobody reviews updates. Repeatability and maintenance are different goals.
Dependency updates should be deliberate rather than permanently avoided. Composer provides outdated and audit commands to support review, but their output still requires context. An advisory may or may not affect the way an application uses a package, while an apparently minor update may still deserve testing. The useful habit is a recurring maintenance window: inspect available changes, update a bounded set, test, review the lock diff, and then release.
Rollback also needs a boundary. Restoring an earlier lock file and reinstalling during an outage still depends on repositories and the environment being available. Keeping the previously working release artifact is usually a calmer option. If a release includes a non-reversible database migration, rolling back PHP files alone may be unsafe, so dependency rollback must fit the application's broader migration plan.
A narrow promise is a useful promise
composer.lock cannot make a complete PHP deployment reproducible by itself. It can do something smaller and essential: preserve the dependency decision that was reviewed and tested.
The operational pattern is straightforward. Resolve updates away from production, commit and review the lock file, validate it, install rather than update during deployment, check the real runtime platform, run application-specific smoke tests, and retain a rollback path. Then document the remaining variables instead of pretending they disappeared.
That narrower promise is not a weakness. It gives a deployment one fewer question to answer at the worst possible time: “Which dependency versions did the server choose today?”
