10 Python Dockerfile Anti-Patterns to Stop Using
Practical fixes (with snippets) that shrink images, speed builds, and keep your production containers boring — in the best way.
Ten common Python Dockerfile mistakes and their fixes. Learn to pin bases, cache deps, use multi-stage builds, drop root, and ship lean, secure images.
Let’s be real: a sloppy Dockerfile will betray you at the worst moment — slow CI, fat images, brittle deploys. The good news? Most problems come from a handful of anti-patterns. Below are ten I keep seeing in Python projects, plus drop-in fixes you can apply today.
1) FROM python:latest
What goes wrong: Unpinned bases drift. Your image breaks “randomly” when upstream flips to a new minor.
Instead:
FROM python:3.12-slim AS base
# or: python:3.11-slim-bookworm (pin distro + minor)Why it works: Reproducible builds and predictable security updates. Use a renovate bot to bump intentionally.
2) Copying the whole repo before installing deps
Anti-pattern:
COPY . /app
RUN pip install -r /app/requirements.txtEvery code edit invalidates the layer; CI rebuilds all wheels.
Fix (cache wins):
WORKDIR /app
COPY requirements.txt .
RUN --mount=type=cache,target=/root/.cache/pip \
pip install --no-cache-dir -r requirements.txt
COPY . .Why it works: Requirements are cached until they change. BuildKit’s cache mount speeds repeated installs.
3) Leaving caches and build tools in the final image
Anti-pattern: install compilers/headers in the only stage, then ship them to prod.
Fix (multi-stage):
FROM python:3.12-slim AS build
WORKDIR /wheels
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential gcc && rm -rf /var/lib/apt/lists/*
COPY requirements.txt .
RUN --mount=type=cache,target=/root/.cache/pip \
pip wheel --no-cache-dir -r requirements.txt
FROM python:3.12-slim AS runtime
WORKDIR /app
COPY --from=build /wheels /wheels
RUN pip install --no-cache-dir /wheels/* && rm -rf /wheels
COPY . .
CMD ["python","-m","app"]Why it works: You compile once, then ship only wheels + runtime.
4) Shipping dev servers and shell-form CMD
Anti-pattern:
CMD "python app.py" # shell form; poor signal handling
# or Flask dev server:
CMD ["python","app.py"]Fix (production server + exec form):
# ASGI example
CMD ["uvicorn","app:app","--host","0.0.0.0","--port","8000"]
# WSGI example
# CMD ["gunicorn","-k","gthread","-w","2","-b","0.0.0.0:8000","app:app"]Why it works: Proper signal handling, graceful shutdowns, and realistic performance.
5) Running as root
Anti-pattern: never creating a user. Any RCE becomes a root shell.
Fix:
RUN useradd -m -u 10001 -s /usr/sbin/nologin appuser
USER appuserOptionally mount the root FS read-only in prod and write to a named volume like /data.
6) apt-get without hygiene
Anti-pattern:
RUN apt-get update && apt-get install -y curl gitLeaves apt metadata, pulls extras, and grows images.
Get Modexa’s stories in your inbox
Join Medium for free to get updates from this writer.
Fix:
RUN apt-get update && \
apt-get install -y --no-install-recommends curl ca-certificates && \
rm -rf /var/lib/apt/lists/*Tip: Install only in the build stage when possible.
7) ADD for everything
Anti-pattern: ADD silently unpacks archives and fetches URLs—surprises you don’t need.
Fix: use COPY for local files.
COPY pyproject.toml ./If you must download at build time, fetch with curl and verify checksums explicitly in a RUN step.
8) Baking secrets and using ARG wrong
Anti-pattern:
ARG API_KEY=abc123
ENV API_KEY=$API_KEY # now it lives in the image foreverFix: Pass secrets at runtime (or use BuildKit secrets for private indexes).
# build: DOCKER_BUILDKIT=1 docker build --secret id=pip_cfg,src=pip.conf .
# Dockerfile:
RUN --mount=type=secret,id=pip_cfg,target=/etc/pip.conf \
pip install --no-cache-dir -r requirements.txtRule: ARG is for build toggles (e.g., --build-arg TARGETARCH), not credentials.
9) No .dockerignore
Anti-pattern: shipping .git/, __pycache__/, .venv/, tests, datasets, and screenshots.
Fix (starter):
__pycache__/
*.pyc
*.pyo
*.pyd
.env
.venv/
.git/
tests/
docs/
*.ipynb
node_modules/
dist/
build/
data/Why it works: Smaller context → faster copies → faster CI.
10) Environment and layering mistakes
Two flavors here:
- Forgetting sane envs:
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
PIP_DISABLE_PIP_VERSION_CHECK=1- Fewer
.pycfiles, cleaner logs, less surprise. - Layering extremes: jamming everything into one
RUNhurts cache granularity; exploding into 50RUNs adds overhead.
Rule of thumb: group logically (OS deps in one, pip install in one, app copy in one).
A “good bones” Dockerfile you can adapt
# syntax=docker/dockerfile:1.6
FROM python:3.12-slim AS build
WORKDIR /app
# system deps for wheels only in build stage
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential gcc && rm -rf /var/lib/apt/lists/*
# envs that improve build behavior
ENV PIP_DISABLE_PIP_VERSION_CHECK=1 \
PIP_NO_CACHE_DIR=1
# cache-friendly dependency layers
COPY pyproject.toml poetry.lock* ./
RUN --mount=type=cache,target=/root/.cache/pip \
pip install --upgrade pip wheel setuptools && \
pip wheel --no-deps -w /wheels ".[prod]" || true
FROM python:3.12-slim AS runtime
WORKDIR /app
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1
# create non-root user
RUN useradd -m -u 10001 -s /usr/sbin/nologin appuser
# install deps from prebuilt wheels
COPY --from=build /wheels /wheels
RUN pip install --no-cache-dir /wheels/* && rm -rf /wheels
# copy only what you need (thanks to .dockerignore)
COPY ./src ./src
USER appuser
EXPOSE 8000
CMD ["uvicorn","src.app:app","--host","0.0.0.0","--port","8000"]Why this works: pinned, slim base; multi-stage; cache-friendly deps; no dev servers; non-root runtime; minimal surface area.
Quick sanity checks (worth automating)
- Image size: aim for <250 MB for typical FastAPI apps; wheels + slim should get you there.
- Build time: after cache warm-up, ❤0–60s on CI for code-only changes.
- Start time: container “ready” in <2s (ASGI) with healthcheck.
- Vuln scan: run
trivyor similar in CI and fail on high severity.
Final Take
Dockerfiles are tiny documents with outsized consequences. Pin your base. Cache dependencies correctly. Keep compilers out of production. Drop root. Treat secrets like secrets. When in doubt, prefer boring, explicit choices — you’ll get faster builds, smaller images, and fewer 2 a.m. surprises.
If this helped, tell me which anti-pattern you’ve seen the most in the wild — or paste your Dockerfile and I’ll suggest a lean rewrite.









