|$ curl https://forge-ai.dev/api/markdown?path=docs/ci/caching
$cat docs/ci-caching.md
updated Recently·25 min read·published
CI Caching
Introduction
CI caching stores dependencies, build artifacts, and intermediate outputs between workflow runs. A well-cached CI pipeline can reduce build times from 15 minutes to 2 minutes — saving developer time and CI costs.
GitHub Actions Cache
basic-cache.yml
YAML
| 1 | # Basic dependency caching |
| 2 | name: CI |
| 3 | on: push |
| 4 | |
| 5 | jobs: |
| 6 | build: |
| 7 | runs-on: ubuntu-latest |
| 8 | steps: |
| 9 | - uses: actions/checkout@v4 |
| 10 | |
| 11 | # Cache node_modules (keyed on lockfile hash) |
| 12 | - uses: actions/cache@v4 |
| 13 | with: |
| 14 | path: node_modules |
| 15 | key: npm-${{ hashFiles('package-lock.json') }} |
| 16 | restore-keys: npm- |
| 17 | |
| 18 | # Or use the dedicated setup-node cache |
| 19 | - uses: actions/setup-node@v4 |
| 20 | with: |
| 21 | node-version: 20 |
| 22 | cache: "npm" # Automatically caches ~/.npm |
| 23 | |
| 24 | - run: npm ci |
| 25 | - run: npm run build |
| 26 | - run: npm test |
advanced-cache.yml
YAML
| 1 | # Advanced caching with multiple layers |
| 2 | - uses: actions/cache@v4 |
| 3 | with: |
| 4 | path: | |
| 5 | ~/.npm |
| 6 | node_modules |
| 7 | .next/cache |
| 8 | dist |
| 9 | key: full-${{ runner.os }}-${{ hashFiles('package-lock.json') }}-${{ github.sha }} |
| 10 | restore-keys: | |
| 11 | full-${{ runner.os }}-${{ hashFiles('package-lock.json') }}- |
| 12 | full-${{ runner.os }}- |
| 13 | |
| 14 | # TypeScript build cache |
| 15 | - uses: actions/cache@v4 |
| 16 | with: |
| 17 | path: .tsbuildinfo |
| 18 | key: ts-${{ hashFiles('tsconfig.json') }}-${{ github.sha }} |
| 19 | restore-keys: ts-${{ hashFiles('tsconfig.json') }}- |
ℹ
info
Always include github.sha in the cache key for the primary key, and use prefix-only keys in restore-keys. This ensures you get the latest cache while still falling back to older versions.
Docker Layer Caching
Dockerfile
Dockerfile
| 1 | # Multi-stage Dockerfile optimized for caching |
| 2 | FROM node:20-alpine AS deps |
| 3 | WORKDIR /app |
| 4 | COPY package.json package-lock.json ./ |
| 5 | RUN npm ci --production=false |
| 6 | |
| 7 | FROM node:20-alpine AS builder |
| 8 | WORKDIR /app |
| 9 | COPY --from=deps /app/node_modules ./node_modules |
| 10 | COPY . . |
| 11 | RUN npm run build |
| 12 | |
| 13 | FROM node:20-alpine AS runner |
| 14 | WORKDIR /app |
| 15 | ENV NODE_ENV=production |
| 16 | COPY --from=builder /app/dist ./dist |
| 17 | COPY --from=deps /app/node_modules ./node_modules |
| 18 | COPY package.json . |
| 19 | |
| 20 | EXPOSE 3000 |
| 21 | CMD ["node", "dist/server.js"] |
docker-cache.yml
YAML
| 1 | # GitHub Actions Docker caching |
| 2 | - uses: docker/setup-buildx-action@v3 |
| 3 | |
| 4 | - uses: docker/build-push-action@v5 |
| 5 | with: |
| 6 | context: . |
| 7 | push: true |
| 8 | tags: app:latest |
| 9 | cache-from: type=gha |
| 10 | cache-to: type=gha,mode=max |
| 11 | |
| 12 | # This uses GitHub Actions cache backend for Docker layers |
| 13 | # First build: ~5 min, subsequent: ~30 seconds |
✓
best practice
Copy package.json and package-lock.json first, then run npm ci. This ensures the dependency layer is cached and only invalidated when dependencies change.
Cache Strategies
cache-strategies.yml
YAML
| 1 | # Strategy: Cache by OS + lockfile hash (most common) |
| 2 | key: deps-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }} |
| 3 | |
| 4 | # Strategy: Cache per-branch (avoid cache pollution) |
| 5 | key: deps-${{ github.ref_name }}-${{ hashFiles('**/package-lock.json') }} |
| 6 | |
| 7 | # Strategy: Matrix builds with separate caches |
| 8 | strategy: |
| 9 | matrix: |
| 10 | node-version: [18, 20, 22] |
| 11 | # Cache key includes matrix value: |
| 12 | key: deps-${{ matrix.node-version }}-${{ hashFiles('package-lock.json') }} |
| 13 | |
| 14 | # Cleanup old caches (runs weekly) |
| 15 | - uses: actions/github-script@v7 |
| 16 | with: |
| 17 | script: | |
| 18 | const caches = await github.rest.actions.getActionsCacheList({ |
| 19 | owner: context.repo.owner, |
| 20 | repo: context.repo.repo, |
| 21 | sort: 'last_accessed_at', |
| 22 | direction: 'asc', |
| 23 | }); |
| 24 | for (const cache of caches.data.actions_caches.slice(0, 10)) { |
| 25 | await github.rest.actions.deleteActionsCacheById({ |
| 26 | owner: context.repo.owner, |
| 27 | repo: context.repo.repo, |
| 28 | cache_id: cache.id, |
| 29 | }); |
| 30 | } |
ℹ
info
Cache size limits: GitHub Actions provides 10 GB per repository. Monitor usage and clean up stale caches. Use the GitHub API to delete old caches programmatically.
$Blueprint — Engineering Documentation·Section ID: CI-CACHE-01·Revision: 1.0