
Photo by author
# Introduction
You’ve written your Dockerfile, built your image, and everything works. But then you notice that the image is over a gigabyte, even the smallest change takes minutes to render, and every push or pull feels painfully slow.
This is not unusual. These are the default results if you write Docker files without thinking about base image selection, context building, and caching. You don’t need a complete overhaul to fix it. A few focused changes can shrink your image by 60 – 80% and turn most reconstructions from minutes to seconds.
In this article, we’ll walk through five practical techniques so you can learn how to make your Docker images smaller, faster, and more efficient.
# Conditions
To follow along, you’ll need:
- Docker installed
- Basic familiarity with
DockerfilesAnddocker buildOrder - A Python project with a
requirements.txtfile (the examples use Python, but the principles apply to any language)
# Choosing Slim or Alpine base images
Each docker file starts with one. FROM The directive that selects the base image. That base image is the foundation your app sits on, and its size becomes your minimum image size before you add a single line of your code.
For example, officials python:3.11 Image is a complete 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-alpineNow create an image from each and check the size:
docker images | grep pythonYou’ll see a difference of several hundred megabytes by changing just one line in your Dockerfile. So which one should you use?
- thin Safe is the default for most Python projects. It removes unnecessary tools but keeps the C libraries that many Python packages need to install properly.
- Alpine is even smaller, but it uses a different C library. musl Instead of glibc – which can cause compatibility issues with some Python packages. So you can spend more time debugging a failed pip install than saving image size.
Rule of thumbStart with : python: 3.1x-slim. Switch to Alpine only if you’re sure your dependency is compatible and you need an extra size reduction.
// Configuring Layers to Maximize Cache
Docker builds images layer by layer with a single instruction at a time. Once a layer is created, Docker caches it. On the next build, if nothing has changed that would affect a layer, Docker reuses the cached version and skips rebuilding it.
catch: If a layer changes, then each layer is invalidated and rebuilt from scratch..
This is very important for installing dependencies. 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 changesEvery time you change a line in your script, Docker invalidates it. COPY . . layer, and then reinstalls all your dependencies from scratch. On a plan with a heavy requirements.txtthese are the minutes lost in each rebuild.
It’s easy to fix: 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 when you change. app.pyDocker reuses the cached pip layer and only replays the final. COPY . ..
Rule of thumb: Your order COPY And RUN From least frequently changed to most frequently changed instructions. Dependencies before code, always.
# Using multi-stage builds
Some tools are only needed at build time—compilers, test runners, building dependencies—but they end up in your final image anyway, bloating it with things that never touch a running application.
Creates a multi-stage solution. You use one step to build or install everything you need, then simply copy the finished output into a clean, minimal final image. Construction tools are never built into the image you send.
Here’s a Python example where we want to install dependencies but keep the final image lean:
# 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 with multistage build:
# 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")The essential tools for gcc and build—required to compile some Python packages—are missing from the final picture. The app still works because the compiled packages were copied. The build tools themselves were left behind in the builder phase, which Docker rejects. This pattern is even more effective in Go or Node.js projects, where a compiler or Node modules that are hundreds of megabytes can be completely excluded from the shipped image.
# Cleaning inside the installation layer
When you install with system packages. apt-getthe package manager downloads package lists and caches files that you don’t need at runtime. If you delete them separately RUN According to instructions, they still exist in the intermediate layer, and Docker’s layer system means they still contribute to the size of the final image.
In order to actually remove them, cleaning must be done. RUN Instructions as to install.
# 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 logic applies to other package managers and temporary files.
Rule of thumb: Anyone apt-get install should be followed && rm -rf /var/lib/apt/lists/* In the same RUN Make it a habit.
# Applying .dockerignore files
when you run docker buildsends everything in the Docker build directory to the Docker daemon as a build context. This happens before any instructions in your Dockerfile run, and often includes files you definitely don’t want in your image.
Without a .dockerignore file, you are sending your entire project folder: .git History, virtual environments, local data files, test fixtures, editor configurations, and more. This slows down every build and risks copying sensitive files into your image.
Oh .dockerignore The file works exactly the same way. .gitignore; This tells Docker which files and folders to exclude from the build context.
Here’s a sample, albeit cropped. .dockerignore For a typical 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
*.keyThis causes a significant reduction in data sent to the Docker daemon before the build even starts. On large data projects with Parquet files or raw CSVs sitting in the project folder, this can be the biggest win of the five methods.
A safety angle is also worth noting. If your project folder contains .env Files containing API keys or database credentials, by default .dockerignore That means those secrets can bake into your image — especially if you have a wide one. COPY . . instruction
Rule of thumb: Always include .env and any document files .dockerignore Except for data files that don’t need to be baked into an image. Also use Secrets of Docker For sensitive data.
# Summary
None of these techniques require advanced knowledge of Docker. They are more habits than techniques. Apply them consistently and your images will be smaller, your builds will be faster, and your deployments will be cleaner.
| Practice. | What does it fix? |
|---|---|
| Slim/alpine base image | Ensures small images by starting with only essential OS packages. |
| Layer configuration | Avoids reinstalling dependencies on every code change. |
| Creates multi-stage. | Excludes construction tools from the final image. |
| Single layer cleaning | Adequate cache prevents intermediate layers from bloating. |
.dockerignore | Reduces architectural context and keeps secrets out of images. |
Happy coding!
Bala Priya c is a developer and technical writer from India. She 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, she’s working on learning lessons and sharing her knowledge with the developer community, writing tutorials, how-to guides, opinion pieces, and more. Bala also creates engaging resource reviews and coding tutorials.