Docker Build Context and .dockerignore Tips
Why your Docker builds are slow and your images are bloated, and how to fix both with a tight build context and a thoughtful .dockerignore.
What you'll learn
- ✓What the build context really is
- ✓How .dockerignore shapes builds
- ✓Layer cache and COPY ordering
- ✓Multi-stage builds for slim images
- ✓BuildKit features worth using
Prerequisites
- •Familiar with terminals and YAML
What and Why
When you run docker build ., the period is not just “current directory” - it is the build context, a tarball Docker sends to the daemon before any instruction runs. A messy context means slow uploads, broken caches, and accidentally shipping secrets into your image.
A clean context with a thoughtful .dockerignore is one of the cheapest wins in the whole container toolchain. Builds get faster, layers cache properly, and images shrink.
Mental Model
Source directory
|
v
.dockerignore filter (drops node_modules, .git, secrets...)
|
v
build context tarball ---> daemon / BuildKit
|
v
Dockerfile executes
COPY/ADD pull from context
layers are cached by content Two practical consequences. First, anything in the context counts toward upload time even if you never COPY it. Second, BuildKit hashes the content of files referenced by each COPY. Touch the wrong file and you bust the cache for every layer below it.
Hands-on Example
Start with a Node.js app. A naive Dockerfile:
FROM node:20
WORKDIR /app
COPY . .
RUN npm install
RUN npm run build
CMD ["node", "dist/server.js"]
This is slow and wrong. Every code change reinstalls dependencies because COPY . . happens before npm install. Fix the order and add a multi-stage build:
# syntax=docker/dockerfile:1.7
FROM node:20-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm \
npm ci
FROM node:20-alpine AS build
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build
FROM node:20-alpine AS runtime
WORKDIR /app
ENV NODE_ENV=production
COPY --from=deps /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist
COPY package.json ./
USER node
CMD ["node", "dist/server.js"]
Now pair it with a .dockerignore:
.git
.gitignore
node_modules
dist
coverage
.env
.env.*
*.log
.DS_Store
Dockerfile
docker-compose*.yml
.vscode
.idea
README.md
tests
This shrinks the upload from hundreds of megabytes to a few, and crucially keeps node_modules out so the cached install in the image is the one that runs.
Build and inspect:
DOCKER_BUILDKIT=1 docker build -t app:dev .
docker image ls app:dev
docker history app:dev
Common Pitfalls
- Sending
node_modulesor.gitinto the context. Slow uploads, busted caches, and.gitmay include credentials in hooks or configs. COPY . .early in the file. Any source change invalidates everything below. Copy dependency manifests first, install, then copy source.- ADD instead of COPY.
ADDhas surprising behaviors (URL fetch, tar extraction). UseCOPYunless you specifically want those. - Building on
latestbase. Non-reproducible builds. Pin to a digest or at least a specific minor version. - Ignoring secrets risk. A
.envin the context can land in your image if youCOPY . .. Use BuildKit secrets:--mount=type=secret,id=npm,target=/root/.npmrc.
Production Tips
- Always enable BuildKit (
DOCKER_BUILDKIT=1or modern Docker default). It gives you secret mounts, cache mounts, parallelism, and better output. - Use distroless or alpine for runtime images. Strip the compiler, package manager, and shell from the final layer.
- Cache aggressively with
--cache-fromand--cache-to=type=registryin CI. Subsequent builds skip steps whose inputs did not change. - Run dive or
docker historyto find fat layers. A 200 MBRUN apt-get installoften hides a single forgotten cleanup. - Pin base images by digest and update via a bot like Renovate. You get reproducibility without falling behind on CVEs.
Wrap-up
The build context is the front door to your image. Keep it tight with .dockerignore, copy dependency manifests before source, and lean on BuildKit features like cache mounts and secret mounts. Multi-stage builds give you small runtime images without giving up developer convenience. Together these moves can turn a 5-minute, 1.2 GB image build into a 30-second, 80 MB one.
Related articles
- Docker Docker Image Layer Caching Strategies for Faster Builds
Learn how Docker's layer cache really works and the ordering tricks that turn a 5-minute build into a 20-second one without sacrificing correctness.
- Docker Writing Your First Dockerfile
Learn how to write a Dockerfile from scratch. Understand each instruction, build a real image for a small Node application, and run it as a container.
- Docker Docker Buildx and Multi-Arch Images: amd64 plus arm64
Build container images that run on both x86 and ARM with Docker Buildx, QEMU, and registry manifests, without doubling your CI complexity.
- Docker Docker Compose Network Aliases Tutorial
Network aliases let containers reach each other under multiple names. Learn how aliases work in Compose, when to use them, and the gotchas to avoid.