
September 10, 2026
12 min read
By Kokil Thapa | Last reviewed: September 2026
Build Caching: Speed Up CI Builds is the difference between a pipeline that finishes in three minutes and one that burns twelve on every push. Your CI runner re-downloads Composer packages, npm modules, and Docker base layers on each job unless you tell it what to reuse. On real client projects I maintain with Composer and npm CI caching, cache hits cut install time by 60–80%. The rest of this guide walks through cache keys, platform config, and the stale-cache traps that waste more time than they save.
What is build caching in CI and why does it matter?
Build caching stores filesystem paths or image layers from a previous CI run and restores them before expensive steps run again. Without it, every job starts cold. That means another composer install, another npm ci, and another Docker pull.
The cost adds up fast. A Laravel 13 project on PHP 8.3 with Vite 8.x frontend assets might spend four minutes on dependencies alone. Multiply that by ten pushes per day across three developers and you lose two hours daily to redundant work.
Caching is not free complexity. You trade a bit of pipeline YAML and occasional cache invalidation for predictable speed. For teams using GitLab CI with Laravel or Jenkins agents, the ROI shows up within the first week.
Think of CI cache as a shared scratch disk keyed to your lock files. Application runtime caching with Redis in Laravel solves a different problem. Build cache lives on the runner or object storage and dies when keys change.
What should you cache first?
Prioritize the slowest, most deterministic steps:
- Composer vendor/ — keyed on
composer.lock - npm node_modules/ — keyed on
package-lock.json - Docker layers — keyed on Dockerfile and context hashes
- Build artefacts — Vite
public/build/when tests need compiled assets - Test caches — PHPUnit result cache, ESLint cache, static analysis output
Do not cache secrets, .env files, or database dumps. Those belong in masked CI variables or encrypted storage.
How do you configure cache keys for Composer, npm, and Docker?
A cache key tells the runner which saved folder matches the current job. Change the lock file and the key changes. The runner misses cache, installs fresh, then writes a new entry.
A common mistake is keying only on branch name. Two developers on feature/checkout with different lock files would share a broken vendor tree. Always include a hash of the lock file.
GitLab CI cache example for Laravel
This pattern works on the sister-site pipelines I run with Deployer 7 and GitLab CI. It caches Composer 2.10 and npm 12 installs separately:
# .gitlab-ci.yml
variables:
COMPOSER_CACHE_DIR: .composer-cache
cache:
key:
files:
- composer.lock
- package-lock.json
paths:
- vendor/
- node_modules/
- .composer-cache/
policy: pull-push
stages:
- test
php-test:
stage: test
image: php:8.3-cli
before_script:
- apt-get update && apt-get install -y git unzip
- curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
- composer install --no-interaction --prefer-dist --no-progress
- corepack enable && npm ci
script:
- cp .env.testing .env
- php artisan key:generate
- php artisan test GitLab restores cache before before_script runs. If vendor/ is intact, Composer skips most downloads. See the official GitLab CI caching documentation for fallback_keys and per-job cache overrides.
GitHub Actions cache example
# .github/workflows/ci.yml
name: CI
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
tools: composer:2.10
- name: Cache Composer
uses: actions/cache@v4
with:
path: vendor
key: composer-${{ hashFiles('composer.lock') }}
restore-keys: |
composer-
- name: Install PHP deps
run: composer install --no-interaction --prefer-dist
- name: Cache npm
uses: actions/cache@v4
with:
path: node_modules
key: npm-${{ hashFiles('package-lock.json') }}
restore-keys: |
npm-
- name: Install JS deps
run: npm ci
- name: Run tests
run: php artisan test GitHub Actions uses restore-keys as prefix fallbacks. A partial hit beats a full miss when you bump one lock file but not the other. The GitHub Actions dependency caching guide covers size limits and eviction rules.
Docker layer caching
Container builds benefit from layer reuse even when application code changes. Put rarely changing instructions first in your Dockerfile:
# Dockerfile — cache-friendly order
FROM php:8.3-fpm
RUN apt-get update && apt-get install -y \
git unzip libzip-dev \
&& docker-php-ext-install zip pdo_mysql
COPY composer.json composer.lock ./
RUN composer install --no-dev --no-scripts --prefer-dist
COPY . .
RUN npm ci && npm run build
CMD ["php-fpm"] Use docker buildx build --cache-to type=registry --cache-from type=registry in CI for shared layer storage across runners. The Docker layer caching guide covers BuildKit backends in more depth. Docker's own build cache documentation explains inline and registry cache exporters.
Which CI platforms support build caching in 2026?
Every major CI vendor offers caching, but semantics differ. Pick the model that matches your runner topology and budget.
| Platform | Cache storage | Key model | Best for |
|---|---|---|---|
| GitLab CI | Runner local or S3-compatible | File hash + branch | Laravel teams already on GitLab |
| GitHub Actions | GitHub-managed blob per repo | Explicit key + restore-keys | Open-source and SaaS repos |
| Azure Pipelines | Azure Blob via Cache@2 task | Key + restore keys | .NET + mixed stacks |
| CircleCI | Dedicated cache service | Template keys per branch | High-volume parallel jobs |
| Jenkins | Plugin-dependent (S3, NFS) | Manual job config | Self-hosted bare metal |
GitLab shared runners in Nepal often have limited disk. I set cache:policy: pull on feature branches and pull-push only on main. That stops every branch from writing multi-gigabyte vendor trees.
For Jenkins setups, read the Jenkins CI/CD practical tutorial and add the Pipeline Utility Steps plugin for stash and unstash. It is less elegant than native cache but works on agents without shared storage.
Azure and Bitbucket teams should cross-read the Azure Pipelines guide and Bitbucket Pipelines guide for platform-specific YAML.
How do you avoid stale cache bugs in CI pipelines?
Stale cache is the main reason teams disable caching after one bad deploy. A restored vendor/ folder from an old lock file passes CI but fails in production with a class-not-found error.
These guardrails keep cache safe without throwing away speed gains.
Validate after restore
Never assume restore equals correct. Run install commands anyway. Composer and npm are idempotent when lock files match:
composer install --no-interaction --prefer-dist
npm ci npm ci deletes and reinstalls from lock file if node_modules drifts. Composer verifies checksums. The cache still saves download time even when validation runs.
Bust cache on demand
Add a cache version variable you bump when debugging weird failures:
cache:
key: "v2-composer-${CI_COMMIT_REF_SLUG}-${CI_COMMIT_SHA:0:8}" Or use a CACHE_BUST CI variable set manually from the GitLab UI. One click invalidates without editing lock files.
Separate pull and push policies
On merge-request pipelines, pull only. Push cache updates from main after tests pass. That reduces race conditions when five developers push within the same hour.
I've seen PHPUnit pass against stale autoload maps while production crashed. Running composer dump-autoload -o after restore closes that gap. Pair it with code coverage gates in CI so test gaps surface before merge.
What are the best practices for Laravel and PHP CI caching?
Laravel 13 on PHP 8.3 adds a few PHP-specific wrinkles. Opcache and path repositories behave differently on ephemeral runners than on your laptop.
Cache the right paths
vendor/keyed oncomposer.locknode_modules/keyed onpackage-lock.json.composer-cache/for Composer 2.10 download cachepublic/build/only if downstream jobs skipnpm run build- Never cache
bootstrap/cache/orstorage/between unrelated jobs
For Vite 8.x frontends, decide whether CI builds assets or commits them. Several projects I deploy commit compiled assets because production servers have no Node. In that case cache JS deps only for lint jobs. See Vite vs Webpack for frontend builds for the trade-off.
Parallel jobs and cache contention
Split lint, unit tests, and browser tests into parallel jobs. Each job pulls the same cache key. Only one job should push updates per pipeline to avoid last-write-wins corruption:
composer-cache:
stage: prepare
cache:
key:
files:
- composer.lock
paths:
- vendor/
policy: pull-push
script:
- composer install --no-interaction --prefer-dist
phpunit:
stage: test
needs: [composer-cache]
cache:
key:
files:
- composer.lock
paths:
- vendor/
policy: pull
script:
- php artisan test This prepare-job pattern mirrors what I use on legal-tech portals like the Adventure Third Pole Trek booking platform. One install, many consumers.
Frontend and asset pipeline caching
When CI runs npm run build, cache node_modules/.cache if Vite writes there. ESLint and Prettier benefit from --cache flags stored under .eslintcache. The npm scripts for build automation article lists script patterns that play well with cached runners.
WordPress and WooCommerce 11.1 projects on shared hosting follow a different path. CI might only run PHPCS and PHPUnit without a full asset pipeline. The WordPress development service page covers hosting constraints that affect what you cache locally versus in CI.
Measure before and after
Log install duration in every pipeline. GitLab exposes job duration in the UI. Compare median times over two weeks before declaring victory.
A rough budget for a mid-size Laravel app on shared runners:
- Uncached pipeline: 8–14 minutes
- Cached pipeline (hit): 3–6 minutes
- Cache miss after lock bump: 7–10 minutes once, then back to baseline
That time savings matters for small teams in Kathmandu billing hourly. Faster feedback also means fewer context switches. Use the JSON formatter tool to inspect CI API responses when you automate metrics collection.
For broader pipeline design, read build pipeline automation best practices and build automation complete guide. Teams needing hands-on help can review testing and optimization services or custom software development for audit work.
Self-hosted runners on Ubuntu benefit from the same disk hygiene as production servers. The Linux system administration service covers disk quotas and cleanup cron jobs that keep cache directories from filling root partitions.
If you maintain long-lived Jenkins agents, schedule weekly cache pruning. Orphaned vendor trees from deleted branches consume tens of gigabytes silently.
Key Takeaways
- Key caches on lock file hashes, not branch names alone, to prevent stale vendor restores.
- Run
composer installandnpm ciafter every cache restore for safe validation. - Use pull-only cache on feature branches and pull-push on main to cut write contention.
- Split a dedicated prepare job that installs deps once for parallel PHPUnit, static analysis, and lint jobs.
- Cache Docker layers by ordering Dockerfile instructions from least to most frequently changed.
- Track median pipeline duration weekly so cache regressions show up before developers complain.
People Also Ask
Does CI caching work with monorepos?
Yes, but keys must include path-specific lock files. Use separate cache entries per package with keys like hashFiles('apps/api/composer.lock'). GitLab supports multiple cache blocks per job. Without path scoping, unrelated package changes invalidate the entire cache.
How large can CI caches get?
GitHub Actions limits caches to 10 GB per repository with LRU eviction. GitLab defaults depend on runner disk, often 5–20 GB per machine. A typical Laravel vendor plus node_modules folder runs 200–600 MB compressed. Monitor size and exclude test fixtures or generated reports from cached paths.
Should you cache production vendor with dev dependencies?
Keep separate cache keys for --no-dev and full installs. Production deploy pipelines should key on composer.lock plus a prod suffix. Mixing dev and prod vendor trees causes subtle extension mismatches in CI.
Is build caching the same as a Composer mirror?
No. A mirror replicates Packagist to a local server and helps every install everywhere. CI cache only helps jobs on runners that share storage. Many teams use both: a mirror for reliability and CI cache for speed on ephemeral runners.
Ship faster pipelines with intentional cache design
Build Caching: Speed Up CI Builds is not a toggle you flip once. It is a discipline: correct keys, safe restore validation, and branch-aware push policies. Start with Composer and npm paths on your noisiest pipeline. Measure the delta. Then add Docker layer cache and parallel job splits.
If your team waits ten minutes per push and deploys multiple Laravel apps weekly, fixing cache pays for itself quickly. For pipeline audits, deployment hardening, or ongoing maintenance, reach out via contact us. You can also browse the portfolio for production CI examples or read more on the blog.
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.

