Skip to content
C Codeloom
Docker

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.

·4 min read · By Codeloom
Intermediate 10 min read

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
Build flow with context and ignore

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_modules or .git into the context. Slow uploads, busted caches, and .git may 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. ADD has surprising behaviors (URL fetch, tar extraction). Use COPY unless you specifically want those.
  • Building on latest base. Non-reproducible builds. Pin to a digest or at least a specific minor version.
  • Ignoring secrets risk. A .env in the context can land in your image if you COPY . .. Use BuildKit secrets: --mount=type=secret,id=npm,target=/root/.npmrc.

Production Tips

  • Always enable BuildKit (DOCKER_BUILDKIT=1 or 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-from and --cache-to=type=registry in CI. Subsequent builds skip steps whose inputs did not change.
  • Run dive or docker history to find fat layers. A 200 MB RUN apt-get install often 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.