Monorepo Build Contexts
Summary
- A monorepo build context builds each service from the repository root so its
DockerfilecanCOPYshared packages the service depends on. - Insight: the context root, not the
Dockerfilelocation, defines whatCOPYcan reach. Move the root, not theDockerfile. - Key Concepts:
build context, root-level.dockerignore, selectiveCOPY, per-serviceDockerfile, BuildKit layer cache. - When to Use: when services share internal libraries in one repo and a per-service context would be blind to them.
- Limitations: a root context is large and cache-sensitive; poor
COPYordering invalidates the dependency cache on every source edit.
Recipe
Point the build at the repo root and select only what each service needs.
docker build -f services/api/Dockerfile -t api:dev .The -f flag names the per-service Dockerfile; the trailing . is the shared root context.
Inside the Dockerfile, COPY the shared packages and the service, in dependency order.
Add a root .dockerignore so the large tree does not flood the build.
Working Example
A repo with two services and a shared package:
repo/
services/api/Dockerfile
packages/shared/package.json
packages/shared/src/
package.json
.dockerignoreRoot .dockerignore, so node_modules, history, and other services' artifacts stay out:
.git
**/node_modules
**/dist
**/*.env
services/workerThe services/api/Dockerfile, built with context .:
FROM node:22-slim AS build
WORKDIR /repo
# 1. Manifests first - cache installs independently of source
COPY package.json package-lock.json ./
COPY packages/shared/package.json packages/shared/
COPY services/api/package.json services/api/
RUN npm ci
# 2. Source next - only the paths this service needs
COPY packages/shared packages/shared
COPY services/api services/api
RUN npm run build --workspace services/api
FROM node:22-slim
WORKDIR /app
COPY --from=build /repo/services/api/dist ./dist
COPY --from=build /repo/node_modules ./node_modules
USER node
CMD ["node", "dist/server.js"]Build and run it:
docker build -f services/api/Dockerfile -t registry.internal/api:$(git rev-parse --short HEAD) .
docker run --rm -p 8080:8080 registry.internal/api:$(git rev-parse --short HEAD)The worker service gets its own Dockerfile that COPYs only the packages it needs, from the same root context.
Deep Dive
Why the root context
COPY source paths resolve relative to the context root, never the Dockerfile.
Building services/api with context services/api cannot reach packages/shared, because that path is outside the context.
Building with context . makes the whole tree reachable, so selective COPY packages/shared works.
Ordering for cache hits
BuildKit caches each instruction by the checksum of its inputs.
Copy manifests (package.json, lockfiles) and run the install before copying source, so a code edit does not bust the dependency-install layer.
This is the single biggest lever on monorepo build time.
Keeping the context small
A root context can be gigabytes. A root .dockerignore prunes what BuildKit transfers.
Ignore all node_modules, dist, .git, and sibling services that a given image never needs.
For very large repos, BuildKit's cache mounts (RUN --mount=type=cache,target=/root/.npm) keep package caches across builds without shipping them in layers.
Selective COPY versus copying everything
COPY . . in a monorepo pulls unrelated services and invalidates the cache on any repo change.
Explicit COPY packages/shared and COPY services/api limit both the transfer and the cache blast radius.
Bake path filtering
BuildKit and docker buildx bake can define multiple targets that share the root context but differ in Dockerfile and args, which scales the pattern to many services from one build definition.
Gotchas
COPY fails with "not found" for shared code. The context is too narrow. Build with the repo root as context, not the service folder.
Every code change reinstalls dependencies. Manifests are copied after source. Reorder so COPY package*.json and RUN npm ci come before COPY of the source.
Builds are slow despite .dockerignore. Ignore rules cut the transfer, but an enormous tree still costs enumeration. Also ignore sibling services and large fixtures the image never uses.
Secrets leak into layers. A wide root context can sweep in .env files. Add them to .dockerignore and use RUN --mount=type=secret for build credentials.
Local node_modules overwrites the image's. Ignore **/node_modules so host-installed modules are not copied over the ones the build produced.
Alternatives
Per-service context (docker build services/api): simplest and smallest, correct when a service has no shared internal code. It cannot reach sibling packages.
Prebuilt shared base image: publish packages/shared as its own internal image and FROM it. Good when many services share a stable library; adds a release step for the shared package.
Buildx bake / Turborepo-style task graph: define many targets over one context and let the tool prune and cache. Best at scale; more tooling to learn and maintain.
Symlink or copy shared code into each context: avoid this - it duplicates source and breaks reproducibility. The root context exists precisely so you do not have to.
FAQs
Which folder is the build context here? The repository root (.), passed as the last argument to docker build, so COPY can reach shared packages.
Where does the Dockerfile live? Still co-located with its service (services/api/Dockerfile), selected with -f. Only the context moves to the root.
How do I stop code edits from busting the install cache? Copy manifests and run the install before copying source, so the dependency layer only rebuilds when dependencies change.
Does this work with BuildKit cache mounts? Yes. RUN --mount=type=cache persists package caches across builds and pairs well with a root context in monorepos.
Should I use COPY . .? Avoid it in monorepos. Copy only the packages and service directory each image needs to limit transfer and cache invalidation.
Related
- Where Docker Fits in a Repository
- Repo Layout Basics
- Base Image Internal Registry
- Project Setup Best Practices
Stack versions: This page was written for Kubernetes 1.36.2, Docker Engine 29.6.1 (BuildKit default), containerd (CRI runtime on nodes), Helm 3, Compose v2, Argo CD (latest - verify at build), and Gateway API (GA - verify controller support at build).