Build Smaller, Faster Django Docker Images
Shrink and speed up Django Docker images: slim base images, multi-stage builds, layer ordering, BuildKit cache mounts, .dockerignore, and measurement.
Problem statement
A typical first Dockerfile for Django is built on a full Python image, copies the whole repository before installing dependencies, and installs compilers "just in case". The result is an image of well over a gigabyte that rebuilds every dependency whenever any file changes. That costs real time and money:
- CI builds take minutes even for one-line changes
- deploys pull large images to every server or node, slowing rollouts and autoscaling
- registries store many large layers
- the image contains compilers, caches, Git history, and possibly secrets that should never ship
Image size is not just cosmetic. Pull time directly affects how fast new instances start, and fewer packages mean fewer vulnerabilities to patch. This guide reduces size and build time step by step, measuring the effect of each change.
Quick answer
- Measure:
docker images,docker history, and a tool such asdiveto see which layers are large. - Use a slim base image (
python:3.13-slim), not the full image. - Add a strict
.dockerignore(Git history, virtualenvs, node_modules, media, local databases). - Copy dependency files first and install dependencies in their own layer, so code changes do not reinstall packages.
- Use a multi-stage build: compilers and headers in the builder, only runtime libraries in the final image.
- Use BuildKit cache mounts for pip or uv caches instead of storing caches in layers.
- Clean package manager lists in the same
RUNlayer, run as a non-root user, and collect static files at build time.
Step-by-step solution
1. Measure the starting point
docker build -t myapp:before .
docker images myapp
docker history --no-trunc --format "{{.Size}}\t{{.CreatedBy}}" myapp:before | head -20
docker history lists layers with their sizes and the instruction that created them. For an interactive view of which files each layer adds, dive myapp:before is very helpful.
Also time a rebuild after changing one Python file:
touch config/urls.py
time docker build -t myapp:before .
2. Choose the base image
| Base | Typical use |
|---|---|
python:3.13 | full Debian with build tools; large |
python:3.13-slim | minimal Debian; good default for Django |
python:3.13-alpine | very small, but musl libc means many binary wheels do not apply, slower builds, and subtle compatibility differences |
For Django, slim is usually the best trade-off. Pin a specific variant such as python:3.13-slim-bookworm (or a digest) for reproducible builds.
3. Add a .dockerignore
Everything in the build context is sent to the builder, and a broad COPY . . puts it into the image.
.git
.gitignore
.venv
venv
__pycache__/
*.pyc
node_modules
media/
staticfiles/
*.sqlite3
.env
.env.*
docker-compose*.yml
compose*.yaml
tests/fixtures/large/
coverage/
.pytest_cache/
This speeds up context transfer, avoids cache invalidation from irrelevant files, and keeps secrets such as .env out of images.
4. Order layers for caching
Docker reuses a layer if its instruction and inputs are unchanged, and invalidates every layer after the first change. Put things that change rarely first:
FROM python:3.13-slim
WORKDIR /app
# Changes rarely: dependency list
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Changes often: application code
COPY . .
With this order, editing a view only rebuilds the final COPY layer.
5. Multi-stage build
Packages such as psycopg (source build), lxml, or Pillow may need compilers when wheels are unavailable. Keep build tools out of the final image:
# syntax=docker/dockerfile:1
FROM python:3.13-slim-bookworm AS builder
ENV PIP_DISABLE_PIP_VERSION_CHECK=1 \
PYTHONDONTWRITEBYTECODE=1
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential libpq-dev \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
RUN python -m venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"
COPY requirements.txt .
RUN --mount=type=cache,target=/root/.cache/pip \
pip install -r requirements.txt
FROM python:3.13-slim-bookworm AS runtime
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
PATH="/opt/venv/bin:$PATH"
RUN apt-get update && apt-get install -y --no-install-recommends \
libpq5 \
&& rm -rf /var/lib/apt/lists/*
RUN useradd --uid 10001 --create-home app
WORKDIR /app
COPY --from=builder /opt/venv /opt/venv
COPY --chown=app:app . .
RUN DJANGO_SECRET_KEY=build-only python manage.py collectstatic --noinput \
&& chown -R app:app /app/staticfiles
USER app
EXPOSE 8000
CMD ["gunicorn", "config.wsgi:application", "--bind", "0.0.0.0:8000", "--workers", "3"]
What this achieves:
- the builder has compilers and headers; the runtime has only
libpq5 - the virtualenv is created at
/opt/venvin the builder and copied to the same path, so its scripts keep working - the pip cache lives in a BuildKit cache mount, reused across builds but never stored in an image layer
rm -rf /var/lib/apt/lists/*happens in the sameRUNasapt-get install; deleting files in a later layer does not reduce image size
If all your dependencies have binary wheels (for example psycopg[binary]), you can skip compilers entirely and use a single stage.
6. Don't leave caches or build artifacts in layers
Common hidden size:
- pip cache: use
--no-cache-diror a cache mount - apt lists: remove in the same layer
.pycfiles from build steps:PYTHONDONTWRITEBYTECODE=1during build, or accept them if you precompile deliberately for faster startup- test data, docs, and frontend sources: exclude or build them in a separate stage and copy only outputs
7. Frontend assets
If your project builds JavaScript and CSS with Node, do it in a separate stage and copy only the built files:
FROM node:22-slim AS assets
WORKDIR /frontend
COPY frontend/package.json frontend/package-lock.json ./
RUN npm ci
COPY frontend/ ./
RUN npm run build
# in the runtime stage, before collectstatic:
# COPY --from=assets /frontend/dist /app/static/dist
Node and node_modules never reach the final image.
8. Measure the result
DOCKER_BUILDKIT=1 docker build -t myapp:after .
docker images myapp
touch config/urls.py && time docker build -t myapp:after .
Compare:
- total image size (
beforeversusafter) - rebuild time after a code-only change
- pull time on a fresh machine:
docker image rm myapp:after && time docker pull registry.example.com/myapp:after
It is common to go from over 1 GB to roughly 200 to 300 MB for a typical Django app with the slim base and multi-stage build, and from minutes to seconds for code-only rebuilds. Your numbers depend on dependencies; measure rather than assume.
9. Scan and keep it small
Smaller images contain fewer packages with known vulnerabilities, but not zero. Scan images in CI (for example with Trivy or your registry's scanner), rebuild regularly to pick up base image security updates, and review new system packages added to the Dockerfile.
Verification check:
- the final image has no compilers (
docker run --rm myapp:after which gccprints nothing) .env,.git, and local databases are not in the image- the container runs as a non-root user
- code-only changes rebuild in seconds using cached dependency layers
- the application passes smoke tests from the new image
Explanation
Docker images are stacks of immutable layers. Size comes from what each layer adds, including files later deleted in other layers, and build speed comes from how many layers can be reused. Ordering instructions from least to most frequently changing maximizes reuse; multi-stage builds separate what is needed to build from what is needed to run; .dockerignore and same-layer cleanup keep accidental content out. Together they produce images that are faster to build, pull, and patch.
Edge cases / notes
Distroless and minimal images
Distroless or scratch-based Python images are even smaller but lack a shell, which makes debugging harder and entrypoint scripts impossible. They suit mature teams with good observability.
Compile Python bytecode at build time
Precompiling (python -m compileall) increases image size slightly but can reduce cold start time, which matters for autoscaling and serverless-style platforms.
Multiple architectures
Building for linux/amd64 and linux/arm64 with docker buildx produces separate images per platform; make sure wheels exist for both or the builder compiles packages.
Caching builds in CI
Local builds reuse layers automatically; CI runners often start empty, rebuilding everything each time. Export and import the build cache:
# GitHub Actions
- uses: docker/setup-buildx-action@v3
- uses: docker/build-push-action@v6
with:
context: .
push: true
tags: ghcr.io/myorg/myapp:${{ github.sha }}
cache-from: type=gha
cache-to: type=gha,mode=max
The gha cache backend stores layers in the GitHub Actions cache. Registry-based caches (type=registry) work with other CI systems. mode=max caches intermediate stages too, which matters for multi-stage builds where the builder stage contains the expensive dependency installation.
Reading dive output
dive shows two useful numbers for an image:
- wasted space: bytes added in one layer and removed or overwritten in a later one. A high value points to cleanup done in separate
RUNinstructions. - efficiency score: the share of image content that is not wasted.
In the layer view, select the largest layers and inspect the file tree for unexpected directories, such as /root/.cache/pip, /var/lib/apt/lists, test fixtures, or copied .git directories. Each finding maps to a concrete fix in the Dockerfile or .dockerignore.
A size budget in CI
Prevent regressions by failing the build when the image grows beyond an agreed size. The check below needs the image in the local Docker engine, so build with load: true (or pull the pushed image) before running it:
size=$(docker image inspect --format '{{.Size}}' ghcr.io/myorg/myapp:"$GITHUB_SHA")
limit=$((400 * 1024 * 1024))
if [ "$size" -gt "$limit" ]; then
echo "Image is $((size / 1024 / 1024)) MB, above the 400 MB budget"
exit 1
fi
Adjust the budget when a legitimate dependency requires it, and review the change like any other performance regression.
Common mistakes
COPY . .before installing dependencies.- Running
apt-get cleanor deleting caches in a separateRUNinstruction. - Shipping
.git,.env, andmedia/because there is no.dockerignore. - Switching to Alpine to save space and then spending build time compiling wheels.
Next steps
- Review the complete production image in How to Dockerize a Django App for Production.
- Speed up dependency installs further with Deploy Django with uv for Reproducible Installs.
- Check the rest of the container setup with the Django Docker Production Checklist.
- Resolve driver build failures with Fix psycopg Installation Errors on Django Servers.
FAQ
What base image should I use for Django in Docker?
python:3.x-slim (Debian-based) is a good default: small, compatible with binary wheels, and easy to extend. Pin a specific version tag for reproducible builds.
Why is my Django Docker image so large?
Usually a full Python base image, compilers left in the final image, caches and apt lists stored in layers, and a build context that includes .git, virtualenvs, or node_modules.
Should I use Alpine for Django images?
Usually not. Alpine uses musl libc, so many Python binary wheels do not apply, builds compile packages from source, and the size savings are small compared to slim images with multi-stage builds.
How do I make Docker rebuilds faster for Django?
Copy dependency files and install packages before copying application code, use BuildKit cache mounts for package caches, and keep the build context small with .dockerignore.