5 Docker Best Practices for Fast Builds and Small Images

Photo by the Author
# Introduction
You've written your Dockerfile, built your image, and everything is up and running. But then you realize that the image is over a gigabyte, rebuilding takes minutes for even the smallest change, and every push or pull feels painfully slow.
This is unusual. These are the default results when you write Dockerfiles without thinking about base image selection, build context, and caching. You don't need a complete overhaul to fix it. A few focus changes can reduce your image by 60 — 80% and transform many reconstructions from minutes to seconds.
In this article, we'll walk through five practical techniques to learn how to make your Docker images smaller, faster, and more efficient.
# What is required
To follow along, you will need:
- Docker installed
- Basic familiarity
Dockerfilesas well asdocker buildcommandment - A Python project with
requirements.txtfile (the examples use Python, but the principles work in any language)
# Choosing Slim or Alpine Base Images
Every Dockerfile starts with FROM command that selects the base image. That base image is the base on which we program your application, and its size becomes your minimum image size before you add a single line of your code.
For example, an officer python:3.11 image is a full Debian-based image loaded with compilers, utilities, and packages that most applications never use.
# Full image — everything included
FROM python:3.11
# Slim image — minimal Debian base
FROM python:3.11-slim
# Alpine image — even smaller, musl-based Linux
FROM python:3.11-alpine
Now create an image from one and check the sizes:
docker images | grep python
You will see a difference of several hundred megabytes by changing a single line in your Dockerfile. So which one should you use?
- he is young is a safe default method for many Python projects. It strips out unnecessary tools but keeps the C libraries that most Python packages need to install properly.
- alpine is much smaller, but uses a different C library – musl instead of glibc – which can cause compatibility issues with certain Python packages. So you might spend more time debugging failed pip installs than you save on image size.
rule of thumb: start with python: 3.1x-slim. Switch to alpine only if you are sure your lean is compatible and you need more size reduction.
// Order Layers to Increase Cache
Docker builds images layer by layer, one instruction at a time. Once the layer is built, Docker saves it. In the next build, if nothing has changed to affect the layer, Docker reuses the cached version and skips rebuilding it.
Catch: if a layer changes, each layer after it becomes inactive and is rebuilt from scratch.
This is especially important for dependency injection. Here is a common mistake:
# Bad layer order — dependencies reinstall on every code change
FROM python:3.11-slim
WORKDIR /app
COPY . . # copies everything, including your code
RUN pip install -r requirements.txt # runs AFTER the copy, so it reruns whenever any file changes
Every time you change a single line in your script, Docker throws this error COPY . . layer, and reinstalls all of its dependencies from scratch. In a heavy duty project requirements.txtthose minutes are wasted on each building.
The fix is easy: copy the things that change the least, first.
# Good layer order — dependencies cached unless requirements.txt changes
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt . # copy only requirements first
RUN pip install --no-cache-dir -r requirements.txt # install deps — this layer is cached
COPY . . # copy your code last — only this layer reruns on code changes
CMD ["python", "app.py"]
Now if you change app.pyDocker reuses the cached pipeline layer and restarts the repository COPY . ..
rule of thumb: your order COPY again RUN instructions from least-frequently-changed to most-frequently-changed. Dependencies before code, always.
# Using Multi-Stage Builds
Some tools are only needed at build time – compilers, test runners, build dependencies – but end up in your final image anyway, bloat us with things the operating system doesn't touch.
Multi-stage construction solves this. You use one stage to build or install everything you need, and copy only the finished output to a clean, small final image. Build tools never render the image you send.
Here's a Python example where we want to include a dependency but keep the final image dependent:
# Single-stage — build tools end up in the final image
FROM python:3.11-slim
WORKDIR /app
RUN apt-get update && apt-get install -y gcc build-essential
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["python", "app.py"]
Now for the multi-stage construction:
# Multi-stage — build tools stay in the builder stage only
# Stage 1: builder — install dependencies
FROM python:3.11-slim AS builder
WORKDIR /app
RUN apt-get update && apt-get install -y gcc build-essential
COPY requirements.txt .
RUN pip install --no-cache-dir --prefix=/install -r requirements.txt
# Stage 2: runtime — clean image with only what's needed
FROM python:3.11-slim
WORKDIR /app
# Copy only the installed packages from the builder stage
COPY --from=builder /install /usr/local
COPY . .
CMD ["python", "app.py"]
gcc and key build tools – needed to compile other Python packages – are missing from the final image. The app still works because the bundled packages have been copied again. The build tools themselves are left behind in the builder phase, which is abandoned by Docker. This pattern has even more implications for Go or Node.js projects, where compiler or node modules of hundreds of megabytes can be completely removed from the shipped image.
# Cleaning Inside the Installation Layer
If you install system packages with apt-getpackage manager downloads package lists and keeps files you don't need at runtime. If you remove them separately RUN directives, are still in the middle layer, and the Docker layer system means they still contribute to the final size of the image.
To really get rid of them, cleaning should happen the same way RUN command as input.
# Cleanup in a separate layer — cached files still bloat the image
FROM python:3.11-slim
RUN apt-get update && apt-get install -y curl
RUN rm -rf /var/lib/apt/lists/* # already committed in the layer above
# Cleanup in the same layer — nothing is committed to the image
FROM python:3.11-slim
RUN apt-get update && apt-get install -y curl
&& rm -rf /var/lib/apt/lists/*
The same concept applies to other package managers and temporary files.
rule of thumb: anywhere apt-get install must be followed && rm -rf /var/lib/apt/lists/* similarly RUN commandment. Make it a habit.
# It uses .dockerignore files
If you run docker buildDocker sends everything in the build directory to the Docker daemon as the build context. This happens before any commands in your Dockerfile run, and often includes files you almost certainly don't want in your image.
Except a .dockerignore file, you export the entire folder of your project: .git history, physical locations, location data files, test setup, editor configuration, and more. This slows down the entire build and the risks of copying sensitive files to your image.
A .dockerignore the file works fine .gitignore; tells Docker which files and folders to exclude from the build context.
Here is a sample, albeit truncated, .dockerignore for a standard Python data project:
# Python
__pycache__/
*.pyc
*.pyo
*.pyd
.Python
*.egg-info/
# Virtual environments
.venv/
venv/
env/
# Data files (don't bake large datasets into images)
data/
*.csv
*.parquet
*.xlsx
# Jupyter
.ipynb_checkpoints/
*.ipynb
...
# Tests
tests/
pytest_cache/
.coverage
...
# Secrets — never let these into an image
.env
*.pem
*.key
This causes a large reduction in data sent to the Docker daemon before the build starts. For big data projects with parquet files or raw CSVs sitting in the project folder, this can be one big win in all five processes.
There is also a safety angle to be aware of. If your project folder contains .env files with API keys or database information, I forget .dockerignore means those secrets can end up being included in your photo – especially if you have a scope COPY . . discipline.
rule of thumb: Add regularly .env and any authentication files to .dockerignore in addition to data files that do not need to be baked into the image. Use again Docker secrets for sensitive data.
# Summary
None of these techniques require advanced knowledge of Docker; they are habits more than techniques. Use them consistently and your images will be smaller, your builds faster, and your deploys cleaner.
| Practice | What It Fixes |
|---|---|
| Slim/Alpine base image |
Ensures minimal images by starting with only essential OS packages. |
| Layer order |
Avoids re-installing dependencies for every code change. |
| Multi-stage construction |
Does not include build tools in final image. |
| Cleaning of the same layer |
It prevents the appropriate cache from bursting the middle layers. |
.dockerignore |
It minimizes the content of the plot and keeps the secrets from appearing in the images. |
Enjoy coding!
Count Priya C is an engineer and technical writer from India. He loves working at the intersection of mathematics, programming, data science, and content creation. His areas of interest and expertise include DevOps, data science, and natural language processing. She enjoys reading, writing, coding, and coffee! Currently, he works to learn and share his knowledge with the engineering community by authoring tutorials, how-to guides, ideas, and more. Bala also creates engaging resource overviews and code tutorials.



