In Composer, what is the difference between composer install and composer update, and which one should a deploy run?
answer
- replay vs re-resolve
- only update writes composer.lock
- no lock file means install updates
- commit the lock in applications
- deploy: install --no-dev
basics
~10 scomposer install installs the exact versions in composer.lock; composer update re-resolves composer.json's constraints, rewrites composer.lock with the newest allowed versions, then installs. Deploys and CI run install, production with --no-dev; developers run update deliberately.
solid answer
~40 s`composer update` is the **resolver**: it reads the constraints in `composer.json`, finds the newest versions that satisfy all of them, writes the exact result to `composer.lock` and then installs it. `composer install` is the **replayer**: when a lock file exists it installs exactly the versions it records, without choosing anything, so every machine gets the same `vendor/`. If no lock file exists, `install` warns and falls back to an update, creating one. That is why an application commits `composer.lock` and why deploys and CI run `composer install`, typically `composer install --no-dev` in production to skip `require-dev`. `update` is a deliberate developer action, usually scoped to named packages, whose result is reviewed and committed like code. Running `update` on a server means production gets whatever was newest that minute, not what was tested.
code
bash · 9 lines# developer, on purpose
composer update monolog/monolog
git add composer.json composer.lock
# CI and teammates
composer install
# production build
composer install --no-dev --no-interactiongo deeper
Remember that update chooses versions and writes composer.lock while install replays the lock, and that deploys run install.
Explain the no-lock fallback, the stale-lock warning, and why --no-dev installs still get the versions CI tested.
Make the pipeline enforce it: commit the lock, install with --no-dev in the build, and fail the build rather than let a deploy resolve fresh versions.
Decide how dependency updates flow through the organisation, as reviewed lock-file changes on a cadence, instead of as a side effect of whoever ran update last.
## Two commands, two jobs Composer keeps two files at the root of a project: - **`composer.json`**: what you *want*, as version constraints such as `^3.0`. - **`composer.lock`**: what you *got*, the exact version of every package in the dependency graph, direct and transitive, with the source and dist references each was fetched from. The two everyday commands move information in opposite directions. | | `composer update` | `composer install` | |---|---|---| | Reads | `composer.json` (and the old lock for partial updates) | `composer.lock` | | Resolves versions | yes, newest allowed | no, uses the recorded ones | | Writes `composer.lock` | yes | only when none exists | | Installs into `vendor/` | yes, after writing the lock | yes | | Who runs it | a developer, on purpose | everyone else: teammates, CI, deploys | ## What `composer update` does `update` runs the **dependency solver** against every constraint in the root `composer.json` and in every package's own requirements, picks the newest versions that satisfy all of them, writes the result to `composer.lock`, and then performs an install of that result. Named arguments narrow it: `composer update monolog/monolog` re-resolves only that package and leaves everything else at its locked version. The alias `upgrade` does the same thing. ## What `composer install` does With a lock file present, `install` does **not** choose versions. It checks that the locked set can be installed on the current PHP and extensions, then makes `vendor/` match the lock: installing, updating or removing packages as needed. Run it after every `git pull`, in every CI job and in every deploy. Composer's documentation notes the result is reproducible: the same command produces a `vendor/` with identical files apart from timestamps. Two edge cases matter: 1. **No lock file.** `install` prints `No composer.lock file present. Updating dependencies to latest instead of installing from lock file.` and behaves like `update`, creating a lock. A deploy that hits this has just resolved fresh versions nobody tested. 2. **A lock that lags `composer.json`.** If someone edited `composer.json` without updating, `install` warns that the lock file is not up to date and still installs the locked versions; if a required package is missing from the lock or its locked version no longer satisfies the constraint, the install stops with an error instead. ## Reading the output The console output tells you which of the two actually happened, which is worth knowing when a CI or deploy log looks suspicious: - An **update** prints `Updating dependencies`, then `Lock file operations: N installs, N updates, N removals`, then `Writing lock file`, and only after that the install phase. - An **install** from a lock prints `Installing dependencies from lock file`, with `(including require-dev)` appended in dev mode, and never writes the lock. If a deploy log contains `Writing lock file`, the deploy resolved versions on its own. ## `--no-dev` in production `install --no-dev` skips every package that was locked only because of `require-dev`, and the generated autoloader skips `autoload-dev` rules. Because Composer 2 resolves `require` and `require-dev` together during `update`, the production packages get **the same versions** that CI tested with dev tools present; `--no-dev` only leaves the dev-only ones out. The same flag on `update` does not make the solver ignore `require-dev`: the dev requirements are still resolved and locked, just not installed. ## Applications versus libraries - An **application** commits `composer.lock`. That is what makes CI, staging and production run the same code. - A **library** may commit it, to keep its own contributors on the same versions, but the lock has **no effect** on projects that install the library: only the root project's lock counts. Many libraries leave it out so CI tests against the newest versions consumers will actually get. ## The workflow in one list 1. A developer runs `composer update vendor/pkg` (or `composer require`), runs the tests, and commits `composer.json` and `composer.lock` together. 2. Teammates pull and run `composer install`. 3. CI runs `composer install` and the test suite. 4. The deploy runs `composer install --no-dev`, never `update`.
- A teammate says 'I ran composer install and it changed composer.lock'. How is that possible?Most likely there was no lock file, so `install` fell back to an update and wrote a new one. The other case is a lock file that exists but is not committed. `install` never re-resolves an existing lock; the fix is to commit `composer.lock` so later installs replay it.
- Does composer update --no-dev produce a lock file without the dev packages?No. Composer still resolves `require-dev` during the update and records those packages in the lock; `--no-dev` only stops them from being installed. If a dev requirement blocks resolution, Composer says so explicitly: running update with `--no-dev` does not mean `require-dev` is ignored.
composer.json is a shopping list with 'any brand of pasta, at least this good'; composer update goes to the shop and writes down the exact brands it bought on the receipt, composer.lock; composer install hands that receipt to everyone else so they buy the identical items.
saying these in an interview costs you the question
- composer install always fetches the newest versions allowed by composer.json
- Running composer update on the production server keeps it current and is best practice
- composer.lock should be in .gitignore in an application
- A library's committed composer.lock pins the versions its consumers get
- install --no-dev resolves different versions for production packages than a normal install