Composer Cheatsheet
What it is
Composer is the dependency manager for PHP — it resolves a project's composer.json, downloads packages into vendor/, and writes a deterministic composer.lock that pins every transitive version. For deployment workflows that lockfile is the contract: production hosts and CI runners install from composer.lock, not from composer.json, so every environment ends up with byte-identical dependencies.
This sheet covers the commands and flags you reach for when building production artifacts and running deploys — the install patterns that strip dev-only dependencies, the autoloader optimisations that knock real milliseconds off cold requests, and the CI patterns that turn a 90-second composer install into a 5-second cache restore.
Quick reference
Initialising and validating
composer init # interactive — generates composer.json
composer validate # lint composer.json (use in CI pre-deploy)
composer validate --strict # also fail on warnings (recommended)
composer check-platform-reqs # confirm host PHP version + extensions match
composer about # version, runtime, install path
composer self-update --2 # pin Composer to v2.x (avoid 1.x bugs)
composer self-update --rollback # revert if a new version misbehaves
composer validate --strict belongs in every CI pipeline — it catches a missing "license" or a composer.json that drifted from composer.lock before the deploy gate.
Installing and updating
composer install # install from composer.lock (deterministic)
composer install --no-dev # skip require-dev (production default)
composer install --no-scripts # skip post-install hooks (debug-only)
composer install --prefer-dist # download zipped releases, not git clones
composer install --prefer-source # full git clone (debug-only — slower)
composer update # resolve fresh, REWRITE composer.lock
composer update vendor/package # update one package + its deps
composer update --lock # refresh composer.lock without touching vendor/
composer update --with-all-dependencies vendor/pkg # update package + bump pinned transitives
composer update --dry-run # show what would change — no writes
The mental model that prevents most production accidents: install reads composer.lock, update writes it. A deploy script should only ever run install. Use update locally to bump versions, commit the new lockfile, then let CI run install from it.
Adding and removing packages
composer require vendor/package # add + write to require, update lock
composer require vendor/package:^2.0 # explicit constraint
composer require vendor/package --dev # add to require-dev
composer require vendor/package --no-update # write composer.json only, skip install
composer remove vendor/package # remove from require + uninstall
composer remove vendor/package --dev # remove from require-dev
Searching and inspecting
composer search query # search packagist.org
composer show # list installed packages + versions
composer show vendor/package # detail one package (description, deps, latest)
composer show --tree # full dependency tree
composer show --outdated # only packages with newer releases
composer show --direct # only top-level (no transitives)
composer depends vendor/package # WHY is this package installed?
composer prohibits vendor/package:2.0 # what prevents an upgrade to 2.0?
composer why vendor/package # alias of `depends`
composer why-not vendor/package:2.0 # alias of `prohibits`
composer why-not is the most underused command — when an upgrade is blocked, it tells you the exact transitive that's pinning you, instead of the wall-of-text default error.
Autoloading
composer dump-autoload # regenerate vendor/autoload.php
composer dump-autoload -o # optimize: classmap-style (production default)
composer dump-autoload --classmap-authoritative # -o + treat classmap as the ONLY source
composer dump-autoload --apcu # cache classmap in APCu (web-server prod boost)
composer dump-autoload --no-dev # exclude dev autoload rules
-o rewrites PSR-4 lookups as a flat classmap — one disk lookup per class instead of multiple. --classmap-authoritative goes further: classes not in the classmap throw immediately rather than falling back to filesystem search. Together they're the two flags every production install needs.
Scripts and events
composer run-script script-name # run a custom script from composer.json
composer run-script --list # list available scripts
composer run-script post-install-cmd # rerun the post-install hook manually
composer scripts # alias for --list
composer.json excerpt — typical deploy-aware scripts:
{
"scripts": {
"post-autoload-dump": [
"@php artisan package:discover --ansi"
],
"test": "phpunit --colors=always",
"lint": "phpcs --standard=PSR12 src tests",
"audit": "composer audit --no-dev --abandoned=report"
}
}
Suffix scripts with --no-scripts on the CLI to bypass them during a one-off install (e.g. when debugging a hook that crashes the build).
Authentication and private repositories
composer config --auth http-basic.repo.example.com USER TOKEN
composer config --auth github-oauth.github.com YOUR_TOKEN
composer config --auth gitlab-token.gitlab.com YOUR_TOKEN
# Or via env (CI-friendly, never lands in auth.json):
COMPOSER_AUTH='{"github-oauth":{"github.com":"YOUR_TOKEN"}}' composer install
Auth credentials live in auth.json (project-local) or ~/.composer/auth.json (global). In CI, prefer the COMPOSER_AUTH environment variable — it never hits disk, never leaks into a built layer, and rotates by simply updating the CI secret.
Cache, audit, and platform overrides
composer clear-cache # nuke local download cache
composer cache:list # list cached files
composer audit # check installed packages for known CVEs
composer audit --no-dev # production scope only
composer audit --format json # CI-parseable output
composer outdated # interactive `show --outdated`
# Platform overrides (use sparingly — they LIE to Composer)
composer config platform.php 8.2.10 # pretend the platform is PHP 8.2.10
composer config --unset platform.php # remove the override
composer audit reads CVE data from packagist.org/security-advisories and returns a non-zero exit code when vulnerabilities are found — wire it into your build pipeline as a deploy gate (see the deployment workflows section below).
Deployment workflows (the moat)
1. The production install incantation
Every production deploy should install with exactly this flag set:
composer install \
--no-dev \
--no-interaction \
--prefer-dist \
--optimize-autoloader \
--classmap-authoritative \
--no-progress
What each flag actually buys you in production:
--no-devskipsrequire-dev(PHPUnit, Faker, Xdebug bridges) — typically 30-40% of thevendor/size on a Laravel project, and the difference between shippingmockery/mockeryto production or not.--no-interactiondisables every prompt (auth fallback, plugin trust, mirror selection). A CI build must never block on stdin.--prefer-distdownloads zipped releases from Packagist's CDN instead ofgit clone-ing each repo. On a typical Laravel deploy this is ~6× faster than--prefer-source.--optimize-autoloadergenerates a flat classmap.--classmap-authoritativelocks the autoloader to only that classmap — classes not in the map throw immediately, rather than falling through to filesystem scans. Production code shouldn't be touching the disk to find a class.--no-progressis the difference between a clean CI log and 20 lines of\r\033[Kescape codes per package.
The non-obvious one is --classmap-authoritative. Without it, the autoloader still falls back to PSR-4 directory scans when a class isn't in the classmap — fine in dev, but in production it papers over a missing composer dump-autoload after a deploy and turns a hard crash into a slow request.
2. composer.lock discipline
The lockfile is the deploy contract. Three rules keep it honest:
# 1. composer.lock is ALWAYS committed.
git add composer.json composer.lock
git commit -m "chore: bump phpunit to 11.x"
# 2. Deploys NEVER run `update`.
composer install # ✓ — reads composer.lock
composer update # ✗ — rewrites composer.lock
# 3. CI fails loudly if composer.json and composer.lock drift.
composer validate --strict --no-check-publish
composer install --dry-run # exit non-zero if lock is stale
The --dry-run check on a stale lockfile is the cheapest gate you can add to your build pipeline. Composer compares the lock's content-hash against the live composer.json; a mismatch returns exit code 4 and the build fails before a single package downloads.
If your repo has multiple PHP services in a monorepo, run the gate per service:
for svc in services/*/composer.json; do
( cd "$(dirname "$svc")" && composer validate --strict ) || exit 1
done
3. Vendor caching in CI keyed by composer.lock
A cold composer install on a real Laravel project is 60-120 seconds. A warm cache restore is 3-8 seconds. The cache key must be the hash of composer.lock — never composer.json, never the branch name:
# GitHub Actions
- name: Restore Composer cache
uses: actions/cache@v4
with:
path: vendor
key: composer-${{ runner.os }}-${{ hashFiles('composer.lock') }}
restore-keys: composer-${{ runner.os }}-
- name: Install
run: composer install --no-dev --prefer-dist --optimize-autoloader --classmap-authoritative
Why hash the lockfile, not composer.json? Two pull requests can edit composer.json (re-ordering, comment, formatting) without changing what gets installed. The lockfile's content-hash is the canonical signature of the resolved dependency set — when it changes, the cache miss is real; when it doesn't, the restored vendor/ is correct by construction.
For DeployHQ's build pipelines the same principle applies: configure your build pipeline to cache vendor/ keyed off composer.lock. Subsequent deploys reuse the previous build's vendor/ directory and only re-resolve when the lockfile actually changes.
4. composer audit as a deploy gate
Adding a CVE check before code reaches production costs ~3 seconds and catches the kind of vulnerability that ends up in an incident postmortem:
#!/usr/bin/env bash
set -euo pipefail
composer audit --no-dev --format=json > .audit.json
HIGH=$(jq '[.advisories[] | select(.severity == "high" or .severity == "critical")] | length' .audit.json)
if (( HIGH > 0 )); then
echo "::error::Composer audit found $HIGH high/critical advisories" >&2
jq '.advisories[] | "\(.packageName): \(.title)"' -r .audit.json
exit 1
fi
Treat composer audit like composer install: deterministic, non-interactive, exits non-zero on failure. Don't gate on warnings (abandoned packages, soft advisories) — that's noise that trains the team to ignore the loud signal.
5. Private packages in build pipelines
Three options for pulling private packages during a deploy, in order of preference:
# Option 1 — Packagist.com / Private Packagist (org-wide credentials)
composer config --global --auth http-basic.repo.packagist.com USER TOKEN
# Then in composer.json:
# "repositories": [{ "type": "composer", "url": "https://repo.packagist.com/your-org/" }]
# Option 2 — GitHub App / OAuth token (per-repo)
composer config --global --auth github-oauth.github.com "$GITHUB_TOKEN"
# Then in composer.json:
# "repositories": [{ "type": "vcs", "url": "git@github.com:your-org/private-pkg.git" }]
# Option 3 — Path / VCS for monorepo siblings (no auth needed)
# "repositories": [{ "type": "path", "url": "../shared-lib" }]
In CI the credentials should live exclusively in the environment — never in auth.json committed to the repo, never in a CI variable that prints to logs:
export COMPOSER_AUTH='{"http-basic":{"repo.packagist.com":{"username":"'"$PACKAGIST_USER"'","password":"'"$PACKAGIST_TOKEN"'"}}}'
composer install --no-dev --prefer-dist --optimize-autoloader --classmap-authoritative
For deploys triggered through DeployHQ, set these as project-level environment variables — they survive across deploys, never land in your repo, and rotate by re-saving the project.
6. Atomic releases and the vendor/ swap
If your deploy strategy uses atomic deployments (a current/ symlink pointing at the active release directory), vendor/ belongs inside the release directory — not shared between releases. Three reasons:
- Each release has its own
composer.lock, so each release needs its own resolvedvendor/. A sharedvendor/will be wrong for every release that didn't write it. - Rollback becomes a symlink swap (
ln -sfn /var/www/releases/prev /var/www/current). With a sharedvendor/, rollback needs a re-install — losing the "instant rollback" property. - Concurrent deploys can't race over the same
vendor/mid-install.
Pattern that holds up under load:
RELEASE="/var/www/releases/$SHA"
mkdir -p "$RELEASE"
git -C "$RELEASE" archive --remote=. "$SHA" | tar -x -C "$RELEASE"
cd "$RELEASE"
composer install --no-dev --prefer-dist --optimize-autoloader --classmap-authoritative
# All ok — atomic symlink swap
ln -sfn "$RELEASE" /var/www/current
If composer install fails for the new release, the current/ symlink never moves — production keeps serving the previous release with its own intact vendor/.
Common errors and fixes
| Error / symptom | Cause | Fix |
|---|---|---|
Your lock file is out of date (exit 4) |
composer.json edited without re-running update |
Run composer update --lock locally, commit the new lock |
Your requirements could not be resolved to an installable set |
Conflict between transitive dependencies | composer why-not vendor/pkg:VER to find the blocker |
The "github.com" repository could not be retrieved |
Hit GitHub's anonymous API rate limit | Set COMPOSER_AUTH with a personal access token (5,000/hr vs 60/hr) |
The openssl extension is required |
Container image missing PHP extension | Add RUN docker-php-ext-install openssl to Dockerfile, or apt-get install php-openssl |
Class "App\Service\Foo" not found after deploy |
Autoloader cached stale classmap | composer dump-autoload --optimize in deploy hook |
cannot allocate memory / segfault on composer install |
Tiny CI runner; Composer needs ~2GB peak for big projects | Set COMPOSER_MEMORY_LIMIT=-1 (unlimited), or bump runner size |
composer install slow despite cache |
Cache key wrong (using composer.json hash, not lock) |
Switch cache key to hashFiles('composer.lock') |
Failed to download vendor/pkg from dist |
Packagist mirror flake or rate limit | --prefer-source falls back to git clone; retry with exponential backoff |
php-fpm serving stale code post-deploy |
OPcache holding the old classmap in memory | Reload php-fpm (systemctl reload php8.x-fpm) or call opcache_reset() from a deploy hook |
composer audit blocks deploy on a transitive |
Vulnerable package pinned by a parent you can't update | Add the package to replace (last-resort) or fork the parent and pin |
auth.json accidentally committed |
git add . swept it up |
git filter-repo --invert-paths --path auth.json, rotate the token, add auth.json to .gitignore |
Composer 1.x still running on a host |
Distro package never updated | php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');" && php composer-setup.php --2 |
Companion: full DeployHQ deploy workflow
A real PHP deploy is a Composer install bracketed by hooks: validate the lockfile, install with the production flag set, dump an optimised autoloader, prime caches, swap the symlink, reload php-fpm. Wire this into DeployHQ's build pipelines and the install runs on a build server with cached vendor/ directories, not on the production host — your live servers never see Composer at all, only the resolved artifact.
For a Laravel or Symfony project, the end-to-end flow is documented in the deploy from GitHub guide, and zero-downtime deployments covers the symlink-swap mechanics that pair with the --classmap-authoritative autoloader pattern above.
Start a free DeployHQ trial to wire a production-grade composer install into a build pipeline in minutes.
Related cheatsheets
- Laravel Artisan cheatsheet — for the
migrate --force/config:cachewarm-up that runs aftercomposer install. - WP-CLI cheatsheet — for WordPress deploys that wrap
composer installwithwp search-replace. - Bash cheatsheet — for the
set -euo pipefaildeploy script that orchestrates the install. - SSH cheatsheet — for the deploy keys and
~/.ssh/configpatterns Composer uses to pull private VCS repos. - Cheatsheets hub — every DeployHQ cheatsheet in one place.
Need help? Email support@deployhq.com or follow @deployhq on X.