## 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

```bash
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

```bash
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

```bash
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

```bash
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

```bash
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

```bash
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:

```json
{
  "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

```bash
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

```bash
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](https://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:

```bash
composer install \
  --no-dev \
  --no-interaction \
  --prefer-dist \
  --optimize-autoloader \
  --classmap-authoritative \
  --no-progress
```

What each flag actually buys you in production:

- **`--no-dev`** skips `require-dev` (PHPUnit, Faker, Xdebug bridges) — typically 30-40% of the `vendor/` size on a Laravel project, and the difference between shipping `mockery/mockery` to production or not.
- **`--no-interaction`** disables every prompt (auth fallback, plugin trust, mirror selection). A CI build must never block on stdin.
- **`--prefer-dist`** downloads zipped releases from Packagist's CDN instead of `git clone`-ing each repo. On a typical Laravel deploy this is ~6× faster than `--prefer-source`.
- **`--optimize-autoloader`** generates a flat classmap.
- **`--classmap-authoritative`** locks 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-progress`** is the difference between a clean CI log and 20 lines of `\r\033[K` escape 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:

```bash
# 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:

```bash
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:

```yaml
# 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](https://www.deployhq.com/features/build-pipelines) 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:

```bash
#!/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:

```bash
# 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:

```bash
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](https://www.deployhq.com/features/atomic-deployments) (a `current/` symlink pointing at the active release directory), `vendor/` belongs *inside* the release directory — not shared between releases. Three reasons:

1. Each release has its own `composer.lock`, so each release needs its own resolved `vendor/`. A shared `vendor/` will be wrong for every release that didn't write it.
2. Rollback becomes a symlink swap (`ln -sfn /var/www/releases/prev /var/www/current`). With a shared `vendor/`, rollback needs a re-install — losing the "instant rollback" property.
3. Concurrent deploys can't race over the same `vendor/` mid-install.

Pattern that holds up under load:

```bash
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](https://www.deployhq.com/features/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](https://www.deployhq.com/deploy-from-github), and [zero-downtime deployments](https://www.deployhq.com/features/zero-downtime-deployments) covers the symlink-swap mechanics that pair with the `--classmap-authoritative` autoloader pattern above.

[Start a free DeployHQ trial](https://www.deployhq.com/signup) to wire a production-grade `composer install` into a build pipeline in minutes.

---

## Related cheatsheets

- [Laravel Artisan cheatsheet](https://www.deployhq.com/cheatsheets/laravel-artisan) — for the `migrate --force` / `config:cache` warm-up that runs after `composer install`.
- [WP-CLI cheatsheet](https://www.deployhq.com/cheatsheets/wp-cli) — for WordPress deploys that wrap `composer install` with `wp search-replace`.
- [Bash cheatsheet](https://www.deployhq.com/cheatsheets/bash) — for the `set -euo pipefail` deploy script that orchestrates the install.
- [SSH cheatsheet](https://www.deployhq.com/cheatsheets/ssh) — for the deploy keys and `~/.ssh/config` patterns Composer uses to pull private VCS repos.
- [Bun cheatsheet](https://www.deployhq.com/cheatsheets/bun) — for projects mixing a Bun-based frontend with a PHP/Composer backend in the same release.
- [Cheatsheets hub](https://www.deployhq.com/cheatsheets) — every DeployHQ cheatsheet in one place.

---

Need help? Email [support@deployhq.com](mailto:support@deployhq.com) or follow [@deployhq on X](https://x.com/deployhq).