Optimizing Python Docker Images: Reducing Size by 43–57%
Multi-stage builds and Python slim images can shrink the runtime image size from 895 MB to 508 MB (-43%). Build time after code changes drops from 56 to 16 seconds. This speeds up Kubernetes deployments, saves storage, and reduces traffic.
Optimization addresses key issues: rapid pod scaling during traffic spikes and reduced user latency. Dependencies are managed with Poetry, and tests run in the builder stage.
.dockerignore and Slim Images
Exclude tests and unnecessary files via .dockerignore. Tests are only needed during the build stage.
Switch to python:3.13-slim-bookworm. The full image adds hundreds of MB.
Disable pip/Poetry cache with flags:
- pip:
--no-cache-dir - Poetry:
poetry cache clear --all pypi
Multi-Stage Build: Dockerfile Breakdown
The builder stage installs Poetry, dependencies, and runs pytest. The runtime stage copies only site-packages, binaries (gunicorn, uvicorn), and code.
# Builder stage
FROM python:3.13-slim-bookworm AS builder
ENV PYTHONUNBUFFERED=1 \
LANG=C.UTF-8 \
PYTHONDONTWRITEBYTECODE=1
RUN apt-get update -qq && \
apt-get install -y --no-install-recommends \
curl \
gcc \
libc-dev \
&& pip install --no-cache-dir 'poetry==2.3.2' \
&& apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
WORKDIR /code
COPY pyproject.toml poetry.lock* ./
RUN poetry config virtualenvs.create false \
&& poetry install --no-root --no-dev --no-interaction --no-ansi \
&& poetry cache clear --all pypi --no-interaction || true \
&& rm -rf ~/.cache/pip
COPY . .
RUN pytest
# Runtime stage
FROM python:3.13-slim-bookworm
ENV PYTHONUNBUFFERED=1 \
PYTHONDONTWRITEBYTECODE=1 \
LANG=C.UTF-8
WORKDIR /code
COPY --from=builder /usr/local/lib/python3.13/site-packages /usr/local/lib/python3.13/site-packages
COPY --from=builder /usr/local/bin/gunicorn /usr/local/bin/gunicorn
COPY --from=builder /usr/local/bin/uvicorn /usr/local/bin/uvicorn
COPY --from=builder /code /code
Basic Optimization Results:
| Metric | Before | After | Change |
|---------|------|-------|-----------|
| Image Size, MB | 894.84 | 508 | -43% |
| Full Build Time, s | 58 | 315 | +443% |
| Build Time After Edits, s | 56 | 16 | -71% |
The first build is slower due to builder layers, but caching speeds up iterations.
Advanced Methods: Chainguard and Manual Analysis
Chainguard Images
Chainguard excludes shell, apt/apk, minimizing attack surface. No CVEs in base images. Savings: 50–150 MB.
Drawbacks:
- Debugging without shell
- Free only for latest tag
- Paid versions are expensive
Suitable for high-security projects.
Manual Analysis with dive
Analyze layers:
docker history --format "{{.CreatedBy}}: {{.Size}}" image:tag
docker run -it --rm -v /var/run/docker.sock:/var/run/docker.sock wagoodman/dive:latest image:tag
Focus on large runtime layers. Collapse directories (Shift+Space), remove unnecessary libs. Requires Linux expertise, rarely pays off.
Extreme Optimization: scratch
Runtime on scratch—copy only required libc. Size drops to 384 MB (-57% from original, -24% from multi-stage).
Drawbacks:
- Debugging impossible
- New deps require manual lib addition
- Debugging time: 1+ hour
Relevant for thousands of pods, Serverless (Knative, Fargate). Harder in Python than Go.
Size Comparison:
- Without multi-stage: 894 → 384 MB (-57%)
- Multi-stage: 508 → 384 MB (-24%)
Optimizing Build Time
Copy pyproject.toml/poetry.lock BEFORE code:
- COPY pyproject.toml poetry.lock* ./
- RUN poetry install
- COPY . .
- RUN pytest
Dependency cache persists with code changes. Speedup: 58 → 14 s (x4).
Key Takeaways
- Multi-stage + slim: -43% size, -71% iteration time.
- .dockerignore + no-cache: excludes tests and cache from runtime.
- COPY order: dependencies cached separately from code.
- Scratch: -57%, but for extreme cases (thousands of pods).
- Chainguard/dive: for security and fine-tuning.
— Editorial Team
No comments yet.