Sitemap
Modexa

Modexa

Modern models, modular platforms, exa-scale ambition—field notes for turning prototypes into products.

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.

4 min readOct 12, 2025

--

Press enter or click to view image in full size

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.txt

Every 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 appuser

Optionally 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 git

Leaves 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 forever

Fix: 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.txt

Rule: 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 .pyc files, cleaner logs, less surprise.
  • Layering extremes: jamming everything into one RUN hurts cache granularity; exploding into 50 RUNs 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 trivy or 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.

--

--

Modexa

Written by Modexa

Modern models, modular platforms, exa-scale ambition—field notes for turning prototypes into products.