GitLab CI cache for Node monorepos should restore your package manager store and lockfile-keyed dependencies before every install, then reuse Docker BuildKit layers when jobs build images. Without that, a fifteen-package Turborepo or Nx workspace pays full npm ci or pnpm install on every push—often eight to twenty minutes of download time that caching can cut to under two minutes on warm runners.
DarDev runs GitLab pipelines for internal Node services and client monorepos on self-hosted runners beside Kubernetes clusters. The pattern below matches what we ship in gitlab-ci-kubernetes-pipeline: fast test jobs on merge requests, image builds with cache mounts, and deploy stages that assume artifacts already proved green.
What to cache in a Node monorepo
- pnpm store at ~/.local/share/pnpm/store or PNPM_HOME—content-addressed and safe across branches
- npm cache at ~/.npm when using npm ci—never cache node_modules alone without a lockfile key
- node_modules keyed on pnpm-lock.yaml, package-lock.json, or yarn.lock hash
- Turborepo.turbo or Nx.nx/cache when remote cache is not configured yet
- Docker BuildKit cache mounts for apt and package installs inside multi-stage Dockerfiles
Caching the wrong layer is worse than no cache: stale node_modules from an old lockfile produces green CI and broken production. GitLab cache keys must include the lockfile fingerprint and the Node major version. Bump either and you want a cold restore, not a silent mix of old binaries.
pnpm on GitLab CI
pnpm is the default we recommend for monorepos: strict dependency hoisting, fast installs, and a global store that deduplicates tarballs across workspaces. In.gitlab-ci.yml, define a hidden template job that restores the store and per-job node_modules paths.
.node_cache:
image: node:22-bookworm
variables:
PNPM_HOME: /pnpm
PATH: /pnpm:$PATH
cache:
key:
files:
- pnpm-lock.yaml
prefix: pnpm-${CI_JOB_IMAGE}
paths:
-.pnpm-store
- node_modules
- apps/*/node_modules
- packages/*/node_modules
before_script:
- corepack enable && corepack prepare pnpm@9 --activate
- pnpm config set store-dir.pnpm-store
- pnpm install --frozen-lockfile --prefer-offlineUse rules:changes so a README edit in docs/ does not fan out fifteen workspace installs. Point changes at apps/api/** or packages/shared/** and only run the jobs that own those paths. kubernetes-native-product-pipeline describes how we split build and deploy when services share one git repo.
npm ci and Yarn Berry
Teams on npm should cache ~/.npm and optionally node_modules with a key derived from package-lock.json. Run npm ci—not npm install—in CI so the lockfile is authoritative. For Yarn 2+, cache.yarn/cache and commit.yarn/releases; set YARN_CACHE_FOLDER to a project-relative path GitLab can archive.
Turborepo and Nx task caching
Package manager cache speeds install; task runners speed build and test. Turborepo hashes inputs per package and skips work when outputs already exist. Add.turbo to GitLab cache paths keyed on turbo.json plus lockfile, or configure TURBO_TOKEN remote cache on a small S3-compatible bucket when runners are ephemeral and local.turbo never warms.
Nx offers nx affected and distributed cache via Nx Cloud or self-hosted runners. Even without paid remote cache, caching.nx/cache between pipeline jobs on the same runner cuts repeat typecheck and lint time sharply. Keep cache:policy pull-push on branch pipelines and pull-only on fork merge requests if you worry about cache poisoning.
Docker layer caching in the same pipeline
Install cache does not help docker build if every RUN npm ci re-downloads inside the Dockerfile. Enable BuildKit on GitLab Docker executors and use cache mounts in the build stage:
# syntax=docker/dockerfile:1
FROM node:22-bookworm AS deps
WORKDIR /app
COPY pnpm-lock.yaml pnpm-workspace.yaml package.json./
RUN --mount=type=cache,target=/pnpm/store \
corepack enable pnpm && pnpm fetch
COPY..
RUN --mount=type=cache,target=/pnpm/store \
pnpm install --frozen-lockfile --offlinePair registry layer cache with GitLab dependency cache: CI job restores pnpm store for unit tests; Docker build reuses the same store mount for image assembly. Tag images with $CI_COMMIT_SHA as documented in gitlab-ci-kubernetes-pipeline so deploy jobs never guess which layers shipped.
Speed wins you can measure this week
- Baseline: record install job duration on main without cache for three pipelines
- Add lockfile-keyed GitLab cache; expect fifty to eighty percent install time drop on warm runners
- Introduce rules:changes path filters; docs-only MRs should finish under ninety seconds
- Enable Turborepo --filter or nx affected for test jobs; skip untouched packages
- Add BuildKit cache mounts to Dockerfile; compare image build stage before and after
Export pipeline duration as a Prometheus metric or at minimum log $CI_JOB_DURATION in a post-job script. Tie regressions to cache key changes—teams often break cache by moving lockfiles without updating cache:key:files. staging-environments-match-prod reminds you that fast CI is useless if staging never receives the same image digest production will run.
Mistakes that erase your gains
- Caching node_modules with a static key across Node 20 and Node 22 upgrades
- Running npm install instead of ci and wondering why cache restores wrong trees
- One global cache path on shared runners without lockfile key—cross-project pollution
- Skipping --frozen-lockfile in CI when a developer bumped package.json locally
- Rebuilding every microservice when only packages/ui changed—missing path rules

How DarDev helps
DarDevOps engagements audit pipeline latency before cluster sizing: we profile install, test, and image stages, then apply GitLab cache templates sized to your monorepo layout. Request a scoped CI review at dardev.net/products when every merge request waits twenty minutes for dependency download.
Start tomorrow: add cache:key:files on your root lockfile and measure three pipelines. Next sprint: path-based rules:changes and Turborepo filter on test. BuildKit mounts belong in the same quarter you containerize for Kubernetes deploy—not after the team disables CI from boredom.
Should I cache node_modules or only the package manager store?
Prefer store plus frozen install when disk is tight; cache node_modules too when runners are persistent and lockfile keys are strict. Never cache node_modules without a lockfile hash in the cache key.
pnpm versus npm for monorepo CI speed?
pnpm wins on disk and install time for most workspaces we operate. npm ci is fine for single-package repos; migrate when you add packages/* workspaces and install time crosses five minutes uncached.
Do GitLab shared runners share cache between projects?
Cache keys are project-scoped. Collisions happen only if you reuse identical custom keys across jobs inside one project. Self-hosted runners on your VPS keep cache on local SSD—often faster than shared runner cold starts.
When do I need Turborepo remote cache?
When runners are ephemeral containers and local.turbo never survives between jobs. A small object store plus TURBO_TOKEN pays off above ten packages or when CI runs more than twenty minutes despite GitLab dependency cache.
DarDev CI setup help?
DarDevOps covers GitLab pipeline design, cache keys, and Kubernetes deploy wiring—the same stack we run for DarDevLab and internal services on dardev-vps.



