Fix: Docker build sending large build context / slow Docker build
Part of: Docker, DevOps & Infrastructure
Quick Answer
How to fix Docker build sending large build context caused by missing .dockerignore, node_modules in context, large files, and inefficient Dockerfile layers.
The Error
You run docker build and see:
Sending build context to Docker daemon 2.5GBThe build takes forever, uses excessive disk space, or fails with:
Error response from daemon: Error processing tar file: exit status 1COPY failed: file not found in build context or excluded by .dockerignoreno space left on deviceDocker sends the entire build context (the directory you specify) to the Docker daemon before starting the build. If that directory contains large files, node_modules, .git, or data files, the build context is unnecessarily huge.
Why This Happens
When you run docker build ., Docker tars up the entire current directory and sends it to the daemon. This is the “build context.” Every file in that directory is included unless excluded by .dockerignore.
The reason the transfer feels disproportionately slow is that Docker has to tar, hash, and stream every file before any RUN instruction starts. With a few thousand small files (typical of node_modules), the constant-cost overhead per file dominates. Multi-gigabyte trees take minutes just to upload to the daemon, even on the same machine, because the userspace tar pipe is single-threaded and serializes through Sending build context....
The second reason is that the daemon may live across a network boundary. On Linux the daemon is usually a local Unix socket, so the transfer is fast in-memory. On Docker Desktop for macOS and Windows, the daemon runs inside a Linux VM and the build context is shipped over a virtualized filesystem (VirtioFS, gRPC FUSE, or 9P depending on the version). What looks like a “slow build” is often the file transfer to the VM, not the build itself. BuildKit’s --progress=plain makes the transfer phase visible.
Common causes:
- Missing
.dockerignorefile. Everything is included by default. node_modules/in the context. Can be hundreds of megabytes..git/directory. Repository history can be very large.- Data files, logs, or databases. SQLite databases, CSV files, logs.
- Build artifacts.
dist/,build/,target/,__pycache__/. - Large binary files. Videos, images, trained ML models.
- Wrong build context path. Running
docker build /instead ofdocker build ..
Platform and Environment Differences
The same “context too large” problem behaves differently depending on host OS, Docker engine flavor, and target platform.
Line endings on Windows. .dockerignore and .gitignore use newline-separated patterns. If you create or edit .dockerignore on Windows in an editor that defaults to CRLF, some Docker versions treat the trailing \r as part of the pattern. node_modules\r does not match node_modules, and your ignore rules silently do nothing. The file looks correct in the editor but the context still includes everything. Fix the editor to use LF (.editorconfig with end_of_line = lf) or run dos2unix .dockerignore after editing.
Case-insensitive macOS vs case-sensitive Linux. macOS’s default APFS volume is case-insensitive. A pattern like Node_Modules/ in .dockerignore will exclude node_modules/ locally on macOS but not in CI on Linux. Docker BuildKit on Linux applies the pattern literally and case-sensitively. A Dockerfile that builds clean on a developer Mac can ship a 2GB context to CI. Always lowercase ignore patterns to match the actual directory names.
BuildKit availability. BuildKit is the default on Docker Desktop and Docker Engine 23.0+. On older Linux installs (Ubuntu 20.04 LTS with the distro-packaged docker.io) the legacy builder is still the default and BuildKit is only used when you set DOCKER_BUILDKIT=1 or pass --builder. The legacy builder sends the entire context up front; BuildKit can transfer files lazily for some operations. If a build is unexpectedly slow on a server, check docker info | grep -i buildkit to confirm the active builder.
Docker Desktop default context. Docker Desktop exposes the host project directory to the Linux VM via a virtual filesystem. On VirtioFS (macOS 12.5+, opt-in on Windows) the transfer is fast. On the older gRPC FUSE backend, transfer of node_modules can be five to ten times slower than the same operation native on Linux. Switching to VirtioFS in Docker Desktop > Settings > General usually halves build context upload time without touching .dockerignore.
Multi-platform builds double or triple the context cost. When you run docker buildx build --platform linux/amd64,linux/arm64 ., BuildKit builds once per platform. With a single builder node, the context is sent once and reused. With remote builder nodes (BuildKit running on separate ARM and AMD machines for native cross-builds), the same context is transferred separately to each node. A 500MB context becomes 1.5GB of network traffic for three platforms. Trim aggressively before going multi-platform.
WSL2 vs Hyper-V backend. On Windows, Docker Desktop can use WSL2 or Hyper-V. The WSL2 backend reads files through \\wsl$\ if the project is in the Windows filesystem, which is dramatically slower than keeping the project inside the WSL2 distribution (~/projects/...). A 30-second context transfer on Windows-side becomes 3 seconds on WSL2-side for the same project.
Fix 1: Create a .dockerignore File
The most important fix. Create .dockerignore in your project root:
# Dependencies
node_modules
.npm
bower_components
vendor
# Version control
.git
.gitignore
.svn
# Build artifacts
dist
build
target
__pycache__
*.pyc
*.pyo
# IDE and editor files
.idea
.vscode
*.swp
*.swo
*~
# OS files
.DS_Store
Thumbs.db
# Docker files (not needed in the image)
Dockerfile
docker-compose.yml
docker-compose*.yml
.dockerignore
# Documentation
README.md
CHANGELOG.md
LICENSE
docs
# Test files
coverage
.nyc_output
*.test.js
*.spec.js
__tests__
# Environment and secrets
.env
.env.*
*.pem
*.key
# Logs
*.log
logs
# Data files
*.sql
*.sqlite
*.csv
dataVerify what is included in the context:
# See what Docker would include (without actually building)
# Create a tar and check its size
tar -cf - --exclude='.git' . | wc -c
# Or use docker build with progress
docker build --progress=plain .Pro Tip: A good
.dockerignoreis as important as a good.gitignore. Start with everything excluded and only include what the build actually needs. This can reduce build context from gigabytes to megabytes and speed up builds dramatically.
Fix 2: Use Multi-Stage Builds
Multi-stage builds keep the final image small:
Before — everything in one stage:
FROM node:22
WORKDIR /app
COPY . .
RUN npm ci
RUN npm run build
# Final image includes source code, node_modules, AND build outputAfter — multi-stage:
# Build stage
FROM node:22 AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Production stage
FROM node:22-alpine
WORKDIR /app
COPY --from=build /app/dist ./dist
COPY --from=build /app/node_modules ./node_modules
COPY package*.json ./
EXPOSE 3000
CMD ["node", "dist/index.js"]For Go applications:
FROM golang:1.23 AS build
WORKDIR /app
COPY go.* ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o /server
FROM scratch
COPY --from=build /server /server
ENTRYPOINT ["/server"]The final image only contains the compiled binary — no source code, no build tools.
For stage selection failures specifically, see Fix: Docker multi-stage build failed.
Fix 3: Optimize COPY Order for Layer Caching
Docker caches layers. If a layer has not changed, Docker reuses the cache. Order your COPY instructions to maximize cache hits:
Bad — COPY everything first (cache invalidated on any file change):
COPY . .
RUN npm ci
RUN npm run buildGood — copy dependency files first, then source:
# Layer 1: Copy only dependency files (rarely change)
COPY package.json package-lock.json ./
RUN npm ci
# Layer 2: Copy source (changes frequently)
COPY . .
RUN npm run buildNow npm ci is cached unless package.json or package-lock.json changes. Source code changes only invalidate the last two layers.
For Python:
COPY requirements.txt ./
RUN pip install -r requirements.txt
COPY . .For Go:
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN go build -o /appCommon Mistake: Putting
COPY . .beforeRUN npm ci. This means any source code change invalidates the npm install cache, forcing a full reinstall every build. Always copy dependency manifests separately.
Fix 4: Use Specific COPY Paths
Instead of COPY . ., copy only what you need:
# Instead of COPY . .
COPY src/ ./src/
COPY public/ ./public/
COPY package.json package-lock.json tsconfig.json ./This reduces the context that matters and makes the build more predictable.
For large monorepos:
# Only copy the relevant workspace
COPY packages/my-service/package.json ./
COPY packages/my-service/src/ ./src/
COPY packages/shared/src/ ./shared/src/Fix 5: Use BuildKit for Faster Builds
Docker BuildKit is faster and more efficient:
# Enable BuildKit
DOCKER_BUILDKIT=1 docker build .
# Or set it permanently
export DOCKER_BUILDKIT=1
# In Docker Desktop, BuildKit is enabled by defaultBuildKit advantages:
- Parallel layer building
- Better caching
- Secret mounts (no secrets in layers)
- SSH forwarding
Mount secrets without storing in the image:
# syntax=docker/dockerfile:1
RUN --mount=type=secret,id=npm_token \
NPM_TOKEN=$(cat /run/secrets/npm_token) npm cidocker build --secret id=npm_token,src=.npmrc .Fix 6: Check Build Context Size
Measure your build context:
# Quick check — how big is the directory?
du -sh --exclude=.git .
# What's taking the most space?
du -h --max-depth=1 . | sort -rh | head -20
# Build with verbose output to see context size
docker build --progress=plain . 2>&1 | head -5
# Sending build context to Docker daemon 15.2MBTarget: Under 50MB. If your context is over 100MB, you almost certainly need a better .dockerignore.
Fix 7: Use Remote Build Context
Build from a Git URL instead of a local directory:
# Build from a Git repository
docker build https://github.com/user/repo.git#main
# Build from a specific directory in the repo
docker build https://github.com/user/repo.git#main:docker/
# Build from a tarball URL
docker build https://example.com/context.tar.gzUse stdin for the Dockerfile:
docker build -f - . << 'EOF'
FROM alpine
RUN echo "hello"
EOFFix 8: Use .dockerignore Patterns Effectively
.dockerignore supports the same pattern syntax as .gitignore:
# Exclude everything
*
# Include only what's needed
!src/
!package.json
!package-lock.json
!tsconfig.json
!.env.example
# Exclude within included directories
src/**/*.test.ts
src/**/*.spec.tsThe “exclude everything, include selectively” pattern is the most effective approach for large projects. It guarantees only the minimum files are in the context.
Test your .dockerignore:
# List files that would be included in the build context
# (No built-in Docker command, but you can simulate it)
rsync -avn --exclude-from=.dockerignore . /dev/nullStill Not Working?
Check for symlinks. Symlinked directories can include unexpected large directories in the context.
Check for Docker disk space:
docker system df
# Shows space used by images, containers, volumes, and cache
docker system prune
# Removes unused dataUse --no-cache for debugging:
docker build --no-cache .Check that BuildKit cache is healthy. A corrupted BuildKit cache makes every build behave as if --no-cache was passed, which feels like “the context is too large again.” Run docker buildx prune to reset the cache. If layer caching has stopped working entirely, see Fix: Docker layer cache invalidated for diagnosis steps that go beyond prune.
Check for COPY --link to skip context overhead. BuildKit’s COPY --link materializes the copy by hardlinking from the cache rather than re-reading the source. For large monorepos this can cut several seconds per build, especially when combined with --mount=type=bind.
Check that file watchers are not adding noise. IDEs like JetBrains and VS Code create .idea/workspace.xml, .vscode/.cache, and language-server index directories that change on every save. These flips invalidate caches and bloat context. Add them to .dockerignore early.
Check for .git/lfs blobs. A repo using Git LFS keeps large binary blobs under .git/lfs/objects. Even if .git is excluded, certain workflows expand LFS objects into the working tree as full files. Confirm with du -sh .git and du -sh .git/lfs.
Consider using Kaniko, Buildpacks, or Nix for alternative build strategies that do not require a build context.
For Docker permission issues, see Fix: Docker permission denied socket. For Docker container name conflicts, see Fix: Docker container name already in use.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: Coolify Not Working — Deployment Failing, SSL Not Working, or Containers Not Starting
How to fix Coolify self-hosted PaaS issues — server setup, application deployment, Docker and Nixpacks builds, environment variables, SSL certificates, database provisioning, and GitHub integration.
Fix: Docker Secrets Not Working — BuildKit --secret Not Mounting, Compose Secrets Undefined, or Secret Leaking into Image
How to fix Docker secrets — BuildKit secret mounts in Dockerfile, docker-compose secrets config, runtime vs build-time secrets, environment variable alternatives, and verifying secrets don't leak into image layers.
Fix: Docker Compose Healthcheck Not Working — depends_on Not Waiting or Always Unhealthy
How to fix Docker Compose healthcheck issues — depends_on condition service_healthy, healthcheck command syntax, start_period, custom health scripts, and debugging unhealthy containers.
Fix: Docker Multi-Platform Build Not Working — buildx Fails, Wrong Architecture, or QEMU Error
How to fix Docker multi-platform build issues — buildx setup, QEMU registration, --platform flag usage, architecture-specific dependencies, and pushing multi-arch manifests to a registry.