Copy Only Needed Files
Learn how to copy only needed files into Docker images to minimize image size and improve build performance. This lesson covers best practices for selective file copying using .dockerignore and efficient COPY instructions.
Focus: copy only needed files into images
Every Docker image you build is a promise: it contains everything your application needs to run — and nothing it doesn't. Unfortunately, it's surprisingly easy to accidentally include local virtual environments, .git folders, or node_modules that bloat your image by hundreds of megabytes. That extra baggage slows down builds, increases push/pull times, and wastes disk space. Learning to ⭐copy only needed files into images is the single most impactful habit you can adopt for lean, fast Docker builds.
The problem this lesson solves
Imagine you have a Python project with 200 MB of dependencies in venv/, a hidden .env file, and developer-only scripts inside tests/. When you run COPY . /app, everything lands in the image. The resulting image is
- Ridiculously large – each rebuild sends the entire context to the Docker daemon.
- Slow to build – even trivial file changes invalidate the COPY . layer cache, forcing full rebuilds.
- Unsafe – secret .env files or SSH keys might leak into the image layer.
This lesson directly addresses the pain of bloated, unmanageable builds. By selectively copying only what your application needs at runtime, you produce smaller, faster, more secure images.
Core concept / mental model
Think of your Dockerfile as a packing list for a trip. You don't throw your entire wardrobe into a suitcase — you pick only the clothes you'll actually wear. Similarly, the COPY instruction is your suitcase: every file you add ends up in the final image.
Two strategies work together to achieve selective copying:
- The
.dockerignorefile – a gatekeeper that tells Docker which files not to send to the build context. Think of it as a "do not pack" list. - Precise
COPYpaths – instead ofCOPY . /app, you writeCOPY src/ /app/src/andCOPY requirements.txt /app/. You explicitly list the files you need.
💡 Pro tip: A
.dockerignorefile is not optional for production Dockerfiles. It's a minimum requirement.
How it works step by step
- Create a
.dockerignorefile in your project root – same directory as your Dockerfile. - List patterns for every file or directory that must not enter the build context:
-
node_modules(npm packages) -.venvorvenv(Python virtual environment) -.git(version control history) -.env(environment variables — often contain secrets) -.log,__pycache__/,.pyc(generated artifacts) - Use explicit
COPYinstructions that target only the directories and files your application needs: -COPY requirements.txt /app/-COPY src/ /app/src/ - Build the image with
docker build -t myapp .– the build context sent to the daemon is now significantly smaller.
Hands-on walkthrough
Let's do a concrete example. Create a project structure like this:
my-python-app/
├── Dockerfile
├── .dockerignore
├── requirements.txt
├── src/
│ └── app.py
├── tests/
│ └── test_app.py
├── .env
└── .venv/
└── (hundreds of files)
Example 1: Without selective copying
# BAD – copies everything
FROM python:3.11-slim
WORKDIR /app
COPY . /app
RUN pip install -r requirements.txt
CMD ["python", "src/app.py"]
When you build:
docker build -t myapp-bloated .
# Sending build context to Docker daemon 2.1GB
# ...
Example 2: With .dockerignore and explicit COPY
# .dockerignore
.venv
.venv/**
.git
.git/**
.env
tests/
tests/**
__pycache__/
*.pyc
*.log
# GOOD – selective copy
FROM python:3.11-slim
WORKDIR /app
# Copy dependencies first (caches well)
COPY requirements.txt .
RUN pip install -r requirements.txt
# Copy application source
COPY src/ ./src/
CMD ["python", "src/app.py"]
Build now:
docker build -t myapp-lean .
# Sending build context to Docker daemon 24.5kB
# ...
The difference is dramatic: 24.5 kB vs 2.1 GB. That's a 99.998% reduction in context size.
Example 3: Fine-grained version – copy only individual files
FROM node:20-alpine
WORKDIR /app
# Copy only package files for dependency installation
COPY package.json package-lock.json ./
RUN npm install
# Copy only the source code
COPY src/ ./src/
COPY public/ ./public/
CMD ["npm", "start"]
Benefits: package.json and package-lock.json rarely change, so npm install is cached. Source code is copied separately, so changes to src/ don't invalidate the npm install layer.
Compare options / when to choose what
| Approach | When to use | Pros | Cons |
|---|---|---|---|
COPY . /app + .dockerignore |
General-purpose web apps | Simple; one-line COPY; .dockerignore acts as safety net |
Requires discipline to keep .dockerignore updated; might still copy extra files |
COPY src/ /app/src/ (specific directories) |
Applications with clear folder structure (src/, lib/, static/) | Very explicit; small context; easy to review | More Dockerfile lines; must know exact paths |
COPY requirements.txt . (single files) |
When dependencies change rarely | Maximizes layer caching | Only works when you can separate dependency files from source |
| Multi-stage builds | Production optimizations (not this lesson) | Ultimate image size control | More complex; covered in later lessons |
Rule of thumb: Start with .dockerignore and explicit directory copies. Only move to single-file COPY when you understand caching patterns.
Troubleshooting & edge cases
COPYfails with "no such file or directory" – you're trying to copy a file that isn't in the build context. Check your.dockerignore– it might be filtering it out. Rundocker build -f Dockerfile .without.dockerignoretemporarily to see what's in the context..dockerignorenot working – make sure it's in the same directory as the Dockerfile, not inside a subfolder. Docker looks for{Dockerfile-dir}/.dockerignore..envfile still ending up in the image – verify your.dockerignorepattern. For.env, the pattern**.env(double star) is safest in older Docker versions. In Docker 20.10+,.envworks.node_moduleskeeps being included – you might havenode_modules/off by one level. Ensure the patternnode_modules(without path) covers all subdirectories.- Build context is still large – run
docker build . 2>&1and look for the "Sending build context" line. If it's big, your.dockerignoreis missing key directories. - Files changed but Docker uses cached layer – if you broke dependencies from source, changes in
src/won't invalidate theRUN pip installlayer. That's by design, but if you did changerequirements.txt, that file should be copied before the RUN.
What you learned & what's next
You now understand the core idea behind copy only needed files into images: use .dockerignore as a blocker and explicit COPY paths as selective packers. You completed a practical exercise that demonstrated how to shrink a 2.1 GB build context down to 24.5 kB.
Key takeaways:
- Always include a .dockerignore in production projects.
- Copy dependency files separately from source code for better caching.
- Use explicit paths (COPY src/ /app/src/) instead of COPY ..
- Verifying context size is easy — just read the first line of the build output.
Next up: Multi-stage builds — the final step in building truly production-optimized images. You'll take what you learned here and separate build-time dependencies from runtime artifacts, creating images that are even leaner and more secure.
Practice recap
Create a new project folder with a single app.py file and a requirements.txt containing two lines. Write a Dockerfile that copies only requirements.txt first, installs dependencies, then copies app.py separately. Add a .dockerignore that ignores a dummy .venv/ directory you create. Build the image and verify that the context size drops significantly compared to a sibling Dockerfile without .dockerignore. Check the layer count with docker history.
Common mistakes
- Forgetting to include
.dockerignoreentirely — then wondering why the image is huge. - Copying
package.jsonandpackage-lock.jsonseparately but forgetting gitignore patterns, accidentally including the entire.gitdirectory. - Using
COPY . /appwithout a.dockerignore— the most common cause of 500+ MB images for simple Node or Python apps. - Assuming
.dockerignoreworks recursively — it does, but only if patterns are correctly formatted (e.g.,node_modulescatches all nesting).
Variations
- Instead of a single
.dockerignore, useCOPYwith--chownto change file ownership while copying selectively. - Instead of
.dockerignore, use multi-stage builds to copy only relevant artifacts from intermediate stages — especially for compiled languages. - Instead of copying individual files, use a build-time
.dockerignoregenerated by a CI script that knows exactly which files are needed.
Real-world use cases
- Python web application: copy
requirements.txtseparately, thensrc/only — virtualenv remains on host, reducing image size by 90%. - Node.js API: copy
package.jsonandpackage-lock.jsonfirst, runnpm ci, then copysrc/— node_modules stays inside image but excluded from context. - Static site generator: use
.dockerignoreto exclude.git,node_moduleson build, then copy onlydist/output into a minimal nginx image.
Key takeaways
- Always create a
.dockerignorefile in your project root to filter out unnecessary files from the build context. - Prefer explicit
COPYinstructions overCOPY .— they make builds faster, smaller, and more secure. - Copy dependency files (requirements.txt, package.json) before source code to maximize layer caching.
- Check the build context size in the first line of docker build output to spot bloat immediately.
- .dockerignore patterns are relative to the Dockerfile directory — use
node_modules(no slash) to match all nesting levels.
Keep learning
Related tutorials, quizzes, and articles for this topic.
More in this track
Articles
- The Rise of Python Notebooks as Operational Tools: Why Jupyter Is Moving Beyond Data Science Into DevOps and Monitoring
- How Python's Garbage Collection Works: A Deep Dive Into Memory Management for Better Code Performance
- From Notebook to Production: Reproducible Python Environments with Docker and Poetry
Discussion
Questions, corrections, and tips help everyone reading this page.
0 comments
Add a comment
No comments yet — start the thread.