Skip to content

Grokem · https://www.grokem.com/devops-and-platform/containers/python-dockerfile-small-and-safe

ContainersHow-to guide

Write a small, safe Dockerfile for a Python app

Use a slim base image, install dependencies before copying code so builds are cached, run as a normal user, keep secrets out of the image and ignore what does not belong.

Grokem Editorial

Editorial desk

Published
  • Intermediate
  • 15 min to complete

0 sec

Before you start

  • Docker installed (Docker Desktop or the engine) and a Python project with a requirements.txt
  • A command that starts your app, for example gunicorn app:app or python -m myapp

A Dockerfile is a recipe for an image: a base, your dependencies, your code and a command. A first attempt usually works and is also large, slow to rebuild and runs as root. A few habits fix all three.

A starting point

dockerfile
FROM python:3.12-slim

# keep Python from writing .pyc files and buffering output, which helps container logs
ENV PYTHONDONTWRITEBYTECODE=1 \
    PYTHONUNBUFFERED=1

WORKDIR /app

# 1. dependencies first: this layer only rebuilds when requirements.txt changes
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# 2. then the code, which changes far more often
COPY . .

# 3. do not run as root
RUN useradd --create-home --uid 10001 appuser
USER appuser

EXPOSE 8000
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "app:app"]

Why it is written this way

A slim, pinned base. python:3.12-slim is a Debian-based image with Python and little else, so it is much smaller than the full image and has fewer things to patch. Naming the version (3.12) instead of leaving it out means the base only changes when you decide.

Dependencies before code. Docker builds in layers and reuses a layer when nothing above it has changed. If you copy all your code first, every edit invalidates the dependency install and every build re-downloads every package. Copying only requirements.txt first means a code-only change rebuilds just the last layers, in seconds.

--no-cache-dir. Pip's download cache is useless inside an image and only makes it bigger.

A non-root user. By default a container's process runs as root. If an attacker finds a flaw in the app, they then act as root inside the container. A normal user shrinks what they can do. Create the user after installing packages, and give it a fixed numeric id so file permissions are predictable.

The exec form of CMD. The list form (["gunicorn", ...]) runs the program directly as the main process, so it receives stop signals and shuts down cleanly. The plain string form runs through a shell that may swallow them.

Add a .dockerignore file

COPY . . copies everything in the folder, including things that should never be in an image. A .dockerignore next to the Dockerfile keeps them out and shrinks the build context:

text
.git
.venv
__pycache__/
*.pyc
.env
*.env
node_modules
tests/
Dockerfile
docker-compose*.yml

The line that matters most is .env. Without it, your secrets are copied into a layer that anyone who can pull the image can read.

Secrets and configuration

  • Never put secrets in the image, whether through COPY, ENV or ARG. Image layers are stored and shared, and deleting a file in a later layer does not remove it from the earlier one.
  • Pass configuration at run time: docker run --env-file .env ..., or your orchestrator's secret mechanism.
  • If a build needs a credential (a private package index), use a build secret (RUN --mount=type=secret,...) so it is never saved in a layer.

Build, run, check

bash
docker build -t myapp:1.0 .
docker run --rm -p 8000:8000 --env-file .env myapp:1.0
docker run --rm myapp:1.0 id            # the user the app runs as: not root
docker images myapp                      # the size

Change one line of code and build again: the dependency step should report that it used the cache.

Going further

  • Multi-stage builds compile or build in one stage and copy only the result into a clean final image, keeping compilers and build tools out of production.
  • Scan the image for known vulnerabilities (your registry or a scanner) and rebuild regularly to pick up base-image fixes.
  • Add a health check so the platform knows when the app is really ready.
  • Keep one process per container and let the platform handle restarts and scaling.

Expected result

docker build produces an image of modest size, the container starts your app as a non-root user, and rebuilding after a code-only change reuses the cached dependency layer.

Frequently asked questions

Why not just use the 'latest' tag?

latest moves. A build now and a build in a month can use different bases, and you cannot tell which one a bug came from. Pin at least the Python version, and pin the full digest if you need exact reproducibility.

Alpine or slim?

Slim (Debian-based) images are the safer default for Python, because many packages ship ready-built wheels for them. Alpine uses a different C library, and packages with compiled parts often need to be built from source, which makes builds slower and sometimes harder.

References

From Grokem: https://www.grokem.com/devops-and-platform/containers/python-dockerfile-small-and-safe

Lesson 1 of 3 in Infrastructure basics for data work

How did this strike you?

Be the first to react – it is anonymous.

Community

Questions about this?

Ask in Questions and help and get help from other readers.

Ask a questionBrowse Questions and help

Comments

No comments yet. Share what worked for you, or ask a question.

Choose what we may use. You can change this at any time with “Privacy choices” at the foot of every page. Details are in the Cookie policy and the Privacy policy.

Strictly necessary

Remembers your theme, the lessons you marked as done and this choice. It stays on your device, tracks nothing and cannot be switched off.

Always on
What exactly is collected, and why

Grokem’s own counts

Collects
The pages you open and how long you stay, how far you scroll, where you came from, your device type, browser, screen size, language and time zone. Your IP address is turned into a code that changes every day; it is never stored as it is.
Why
To see how long lessons are read and where readers leave. Not shared with anyone.
Stored
A random visitor ID, a visit ID and the time of your last page view in your browser’s storage (gr_vid, gr_sid, gr_last). No name or email is attached.