|$ curl https://forge-ai.dev/api/markdown?path=docs/ci/caching
$cat docs/ci-caching.md
updated Recently·25 min read·published

CI Caching

CI/CDIntermediate🎯Free Tools
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
2name: CI
3on: push
4
5jobs:
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
2FROM node:20-alpine AS deps
3WORKDIR /app
4COPY package.json package-lock.json ./
5RUN npm ci --production=false
6
7FROM node:20-alpine AS builder
8WORKDIR /app
9COPY --from=deps /app/node_modules ./node_modules
10COPY . .
11RUN npm run build
12
13FROM node:20-alpine AS runner
14WORKDIR /app
15ENV NODE_ENV=production
16COPY --from=builder /app/dist ./dist
17COPY --from=deps /app/node_modules ./node_modules
18COPY package.json .
19
20EXPOSE 3000
21CMD ["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)
2key: deps-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}
3
4# Strategy: Cache per-branch (avoid cache pollution)
5key: deps-${{ github.ref_name }}-${{ hashFiles('**/package-lock.json') }}
6
7# Strategy: Matrix builds with separate caches
8strategy:
9 matrix:
10 node-version: [18, 20, 22]
11# Cache key includes matrix value:
12key: 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