
September 09, 2026
12 min read
By Kokil Thapa | Last reviewed: September 2026
CI CD caching for fast Composer and NPM installs is the single cheapest speed win most PHP and JavaScript pipelines still miss. Every push that runs composer install and npm ci from a cold disk burns two to eight minutes downloading packages your last green build already fetched. On a real GitLab CI pipeline for Laravel, that wasted time stacks across lint, test, and deploy jobs. This guide shows exactly what to cache, where to store it, and how to wire it in GitLab CI, GitHub Actions, and Jenkins without stale-lock surprises.
What should you cache for Composer and NPM in CI CD pipelines?
Cache the directories dependency managers read and write, not random project folders. Composer downloads into a global cache, then copies into vendor/. NPM writes tarballs into its cache before extracting into node_modules/.
Your CI job still needs a correct install step. Caching replaces repeated network fetches; it does not replace lock-file discipline.
Composer cache paths
Composer 2.10 keeps downloaded packages under a home-relative cache directory. On Linux runners that is typically ~/.composer/cache or ~/.cache/composer depending on XDG settings.
Many teams cache vendor/ directly when the job runs composer install --prefer-dist --no-progress. That works on small Laravel 13 apps. Large monorepos with shifting dev dependencies often prefer caching the Composer cache dir and rebuilding vendor each run.
# Show Composer cache location on the runner
composer config cache-dir
# Typical output on Ubuntu GitLab runner:
# /home/gitlab-runner/.cache/composer NPM cache paths
NPM 12 stores tarballs under ~/.npm. With npm ci, the cache speeds tarball retrieval while node_modules/ is rebuilt cleanly from package-lock.json.
Do not cache node_modules/ across major Node version changes. Native modules compiled for Node.js 24 LTS will break on a 26 LTS runner.
What not to cache
.envand secrets — never belong in a cache artifact.- Build output —
public/build/from Vite 8.x should be a job artifact, not a long-lived cache. - Database dumps — use job artifacts with short TTL instead.
- Platform-specific native modules — rebuild when the runner image changes.
For deeper Composer tuning beyond caching, see the guide on Composer autoloader vs classmap optimization.
How do you configure GitLab CI cache for Composer and NPM?
GitLab CI has first-class cache support in .gitlab-ci.yml. I've used this pattern on sister legal-tech sites that share a Deployer 7 GitLab CI deploy pipeline — same runner, same cache policy, predictable install times.
Single-job Laravel example
stages:
- test
variables:
COMPOSER_CACHE_DIR: "$CI_PROJECT_DIR/.composer-cache"
cache:
key:
files:
- composer.lock
- package-lock.json
prefix: php8.5-node26
paths:
- .composer-cache/
- vendor/
- .npm/
before_script:
- composer install --prefer-dist --no-interaction --no-progress
- npm ci --cache .npm --prefer-offline
phpunit:
stage: test
image: php:8.5-cli
script:
- php artisan test The prefix includes your PHP and Node versions. Bump it when you upgrade from PHP 8.4 to 8.5 or from Node 24 LTS to 26 LTS. Old cache entries then expire naturally.
Per-job cache with fallback key
GitLab supports a fallback key when the exact lock hash misses. That helps fresh branches that have not run yet.
cache:
- key:
files:
- composer.lock
paths:
- vendor/
- .composer-cache/
policy: pull-push
- key: composer-fallback
paths:
- .composer-cache/
policy: pull Read the official GitLab CI caching documentation for cache key limits and runner storage quotas.
Separate frontend and backend jobs
Split jobs when your pipeline runs PHP tests and a Vite 8.x asset build in parallel. Each job pulls only what it needs.
- Backend job — cache
vendor/keyed tocomposer.lock. - Frontend job — cache
.npm/keyed topackage-lock.json. - Deploy job — receive
vendor/andpublic/build/as artifacts, not cache.
For a full pipeline skeleton, start with the GitLab CI YAML deep dive for PHP projects.
How do you set up GitHub Actions cache for npm and Composer?
GitHub Actions uses the actions/cache@v4 action or built-in cache hooks in setup actions. The cache key must change when lock files change.
Composer with setup-php
name: CI
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: shivammathur/setup-php@v2
with:
php-version: '8.5'
tools: composer:2.10
coverage: none
- name: Get Composer cache directory
id: composer-cache
run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
- uses: actions/cache@v4
with:
path: |
${{ steps.composer-cache.outputs.dir }}
vendor
key: composer-${{ hashFiles('composer.lock') }}-php85
restore-keys: |
composer-
- run: composer install --prefer-dist --no-interaction --no-progress
- run: php artisan test NPM with setup-node
- uses: actions/setup-node@v4
with:
node-version: '26'
cache: 'npm'
- run: npm ci
- run: npm run build The cache: 'npm' option in setup-node handles cache paths automatically. It keys off package-lock.json in the repo root. See the GitHub Actions dependency caching guide for eviction rules — unused caches expire after seven days on free tiers.
Choosing between platforms? The GitHub Actions vs GitLab CI comparison for 2026 covers cache limits and pricing trade-offs.
Which CI CD cache strategy works best for Laravel and Node monorepos?
One flat cache block feels simple but hurts monorepos. A Laravel 13 API plus a separate Vue admin panel share a repo but have independent lock files.
Cache key design rules
Build keys from inputs that actually change dependencies:
- Hash of
composer.lockfor PHP jobs. - Hash of
package-lock.jsonorpnpm-lock.yamlfor JS jobs. - Runner OS image tag (
ubuntu-24.04vsubuntu-22.04). - PHP version and Node major version in the prefix.
Do not key on branch name alone. Two branches with the same lock file should share cache entries.
Policy: pull-push vs pull-only
Use pull-push on the job that installs dependencies first — usually lint or test. Use pull on downstream jobs that only consume vendor.
On GitLab CI, the default is pull-push for all jobs. That causes race conditions when five parallel jobs all try to write cache at once. Pin push to one job.
| Strategy | Best for | Typical install time | Stale risk |
|---|---|---|---|
Cache vendor/ directly | Single-app Laravel 12/13 repos | 15–45 seconds on hit | Medium if lock changes mid-pipeline |
| Cache Composer cache dir only | Large apps, frequent dev-dep changes | 45–90 seconds on hit | Low |
npm ci + NPM cache dir | All production JS builds | 20–60 seconds on hit | Low |
Cache node_modules/ directly | Small frontends, pinned Node version | 10–30 seconds on hit | High on Node upgrade |
| No cache (cold install) | Security-audited one-off builds | 3–8 minutes | None |
On a production Laravel eCommerce build, I combine Composer cache-dir caching with a committed Vite build artifact. That mirrors patterns in building fast Laravel eCommerce platforms where deploy speed affects release cadence.
What are the common CI CD cache mistakes with Composer and NPM?
Caches fail quietly. The job still passes; it just runs slow. These are the patterns I fix most often on client pipelines.
Stale vendor after lock file change
Symptom: composer install reports success but tests fail with missing classes. Cause: cache key did not include composer.lock hash, so an old vendor/ restored.
Fix: always hash the lock file. Run composer validate --strict in CI to catch lock drift early.
Cached credentials or auth tokens
Never bake COMPOSER_AUTH or .npmrc tokens into cache paths. Use CI masked variables and ephemeral composer config in before_script.
For private packages, see Composer private packages via Satis and Repman.
Runner disk exhaustion
Self-hosted runners on a Linux VPS can fill disk when cache never expires. Schedule weekly gitlab-runner cache prune or cap GitHub Actions cache at 5 GB on small instances.
Skipping npm ci in favour of npm install
npm install mutates package-lock.json and hides drift. Always use npm ci in CI. Cache the NPM cache directory, not a half-updated node_modules/ tree.
PHP extension mismatch
Caching vendor/ built on PHP 8.3 and restoring on PHP 8.5 usually works for pure PHP packages. Extensions like imagick or custom ext-* deps may need a full rebuild. Include PHP version in your cache prefix.
How do Jenkins and Bitbucket Pipelines handle dependency caching?
Not every team runs GitLab or GitHub. Jenkins and Bitbucket have workable options with slightly more wiring.
Jenkins Pipeline
Use the Job Cacher plugin or a manual tarball step keyed to lock file checksum.
stage('Install') {
steps {
script {
def lockHash = sh(script: 'md5sum composer.lock | cut -d" " -f1', returnStdout: true).trim()
cache(maxCacheSize: 500, caches: [
[$class: 'ArbitraryFileCache', path: 'vendor', cacheName: "vendor-${lockHash}"]
]) {
sh 'composer install --prefer-dist --no-interaction'
}
}
}
} See the Jenkins CI/CD practical tutorial for full pipeline context.
Bitbucket Pipelines
Bitbucket uses definitions.caches in bitbucket-pipelines.yml:
definitions:
caches:
composer: ~/.composer/cache
npm: ~/.npm
pipelines:
default:
- step:
caches:
- composer
- npm
script:
- composer install --prefer-dist --no-interaction
- npm ci The Bitbucket Pipelines CI/CD guide covers parallel steps and cache scope across branches.
Self-hosted runner tips
Self-hosted runners on Ubuntu 24 keep cache between jobs by design. That is good for speed but bad if a compromised job poisons cache. Restrict who can push to protected branches. Read self-hosted CI runner security setup before enabling shared cache on a bare VPS.
Projects like Translation Nepal and other sister sites on shared EC2 benefit from one runner pool with a consistent cache policy — less disk churn, faster deploys.
Key Takeaways
- Key CI CD caches to
composer.lockandpackage-lock.jsonhashes plus PHP and Node version prefixes. - Cache Composer’s cache directory or
vendor/; pairnpm ciwith the NPM cache dir, not a stalenode_modules/. - Let one install job push cache; make parallel test jobs pull-only to avoid write races.
- Bump the cache prefix when upgrading PHP 8.5, Node 26 LTS, or the runner OS image.
- Pass build output as short-lived artifacts; keep long-lived cache for dependency downloads only.
- Monitor runner disk usage on self-hosted setups and prune caches on a weekly schedule.
People Also Ask
Does CI CD caching work with composer install and npm ci together?
Yes. Define separate cache paths and keys for each lock file in the same pipeline. A Laravel 13 job can restore vendor/ and .npm/ in one before_script block, then run composer install and npm ci sequentially or in parallel jobs.
How often should you invalidate CI dependency cache?
Invalidate automatically when lock files change — that is what hash-based keys do. Manually purge cache when upgrading PHP, Node, or the runner image even if locks are unchanged. Native modules and platform packages can otherwise slip through.
Is caching node_modules safe in production CI pipelines?
Generally no. npm ci with a warm NPM cache dir is safer. It guarantees a clean tree from package-lock.json while still skipping redundant tarball downloads. Direct node_modules/ caching risks partial corruption across Node version bumps.
Which is faster: GitLab CI cache or GitHub Actions cache?
Speed depends on runner proximity and cache size, not the platform alone. GitLab cache lives on runner or cloud storage tied to the project. GitHub Actions cache is per-repository with a 10 GB default limit. Both beat cold installs by a wide margin when keys are correct.
Ship faster pipelines with dependency caching done right
CI CD caching for fast Composer and NPM installs is low effort and high return. Add lock-file hash keys today and your next ten pushes on unchanged dependencies will prove the gain. If your pipeline still cold-installs everything, you are paying minutes per push that caching hands back for free.
Need help wiring cache policy into a Laravel deploy pipeline, tuning a slow GitLab runner, or auditing a VPS that runs out of disk? Review the testing and optimization service or ongoing support and maintenance options. For a greenfield setup, see custom software development. Validate your pipeline YAML syntax with the free JSON formatter when converting API responses or cache reports. Browse the Adventure Third Pole Trek portfolio for a live Laravel + Livewire project deployed through GitLab CI.
Contact us to audit your current CI/CD pipeline and cut install times on your next release.
Frequently Asked Questions
0 Comments
Leave a comment
Your email is not published. Comments appear once they have been read. Sign in to have your details filled in.

