Free lesson · GenAI Agent Engineering
Write a Dockerfile and build a container image for the LLM app
Create a Dockerfile that packages the Python LLM application with all its dependencies. Learn layer caching, .dockerignore, and choosing the right base image.
Course: Kubernetes Essentials for GenAI Engineers · Chapter 1 · Containerizing LLM Applications
Free to read — no subscription required.
Introduction
When you move a Python application off your laptop, it depends on your locally installed Python version, your system's OpenSSL, and whatever packages happen to exist in your virtual environment — a brittle bundle that breaks the moment another machine has a slightly different shape. Skip a clean Dockerfile and you ship a 1.5 GB image that takes a minute to pull on every Kubernetes node, drags in a full compiler toolchain as attack surface, and rebuilds from scratch every time you touch a Python file. By the end of this lesson you'll be able to author a multi-stage Dockerfile that declares every dependency explicitly, caches pip install across rebuilds, and produces a slim runtime image that excludes build tools and secrets.
Key Terminology
- Dockerfile — a declarative recipe of
FROM,COPY,RUN, andENTRYPOINTinstructions that builds a reproducible image; in this lesson it is the single source of truth for how your Python service is packaged. - Image layer — the immutable filesystem diff produced by each Dockerfile instruction; layers are cached independently, which is why instruction order determines rebuild speed.
- Build context — the directory tarball Docker sends to the daemon when you run
docker build .; without a.dockerignore, this leaks.envfiles,.githistory, and local virtualenvs into your image. - Multi-stage build — a Dockerfile that defines more than one
FROMstage so you can compile dependencies in a fat builder image and copy only the finished artifacts into a slim runtime image. - Base image — the image named in
FROMthat your stage starts from; choosingpython:3.11-slim-bookwormoverpython:3.11for the runtime stage is roughly a 5x size reduction.
Concepts
Layer caching and instruction ordering
Every Dockerfile instruction creates a new layer, and Docker caches each layer keyed on the instruction text plus the files it references. Order instructions from least-to-most frequently changed: base image first, then system packages, then pip dependencies, then application code. When requirements.txt is copied and installed in its own step before any .py files, a code-only change reuses the cached dependency layer and skips the 30–60 second pip install entirely (see Code Walkthrough).
Multi-stage builds for slim runtimes
A single-stage build keeps the compiler toolchain — gcc, python3-dev, header files needed to build packages like grpcio — in the final image. A two-stage build compiles in a throwaway builder and copies only the populated virtualenv into a slim runtime, dropping image size and attack surface together.
Build context and .dockerignore
docker build . packages the entire current directory and ships it to the daemon. Without a .dockerignore, that tarball includes your .env (with API keys), your .git history, your local venv/, and __pycache__ files compiled for the wrong platform. A .dockerignore using .gitignore-style globs excludes those paths from the context entirely, so they cannot accidentally be copied into a layer by a stray COPY . . (see Code Walkthrough).
Code Walkthrough
Having named the three levers — cache-friendly ordering, multi-stage separation, and a trimmed build context — this section shows them working together in two concrete artifacts.
The two artifacts below demonstrate all three concepts together: the .dockerignore shrinks the build context, and the multi-stage Dockerfile orders instructions for cache reuse and copies a populated virtualenv from the builder into a slim runtime.
Code snippettext
1# .dockerignore 2.git 3.gitignore 4__pycache__ 5*.pyc 6.env 7.env.* 8venv/ 9.venv/ 10.vscode/ 11.idea/ 12tests/ 13docs/ 14Dockerfile 15docker-compose*.yaml 16.dockerignore
Code snippetdockerfile
1# ---- Stage 1: Builder ---- 2FROM python:3.11-bookworm AS builder 3 4RUN python -m venv /opt/venv 5ENV PATH="/opt/venv/bin:$PATH" 6 7COPY requirements.txt /tmp/requirements.txt 8RUN pip install --no-cache-dir --upgrade pip setuptools \ 9 && pip install --no-cache-dir -r /tmp/requirements.txt 10 11# ---- Stage 2: Runtime ---- 12FROM python:3.11-slim-bookworm AS runtime 13 14COPY /opt/venv /opt/venv 15ENV PATH="/opt/venv/bin:$PATH" 16 17WORKDIR /app 18COPY settings.py main.py ./ 19 20ENV PYTHONDONTWRITEBYTECODE=1 \ 21 PYTHONUNBUFFERED=1 22ENTRYPOINT ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
The .dockerignore keeps secrets and platform-specific binaries out of the build context before the daemon ever sees them. In the Dockerfile, the builder stage installs into /opt/venv from requirements.txt alone — copied before any source — so the dependency layer survives every code-only change. The runtime stage starts from python:3.11-slim-bookworm (~150 MB), copies the populated virtualenv with COPY --from=builder so none of the compiler toolchain comes along, sets PATH so that venv's uvicorn is the one on disk, and starts the server via ENTRYPOINT.
You'll know it works when docker build -t app:dev . produces an image under 300 MB (docker image ls app:dev), and a no-op rebuild after touching main.py finishes in under 2 seconds because the pip layer is cached.
Do's and Don'ts
With the working Dockerfile in hand, these rules distill the dependency-management and multi-stage choices that keep it fast and reproducible.
Do's
- ✓Do copy
requirements.txtandpip installbefore your source code — this is the single biggest cache win; it turns minute-long rebuilds into two-second ones. - ✓Do use a multi-stage build with a
-slimruntime base — the compiler toolchain belongs in the builder stage, never in the image you ship to production. - ✓Do install dependencies into a self-contained virtualenv and
COPY --from=builderit into the runtime — the runtime stage gets the resolved packages with zero build tooling, andpipitself never runs in the final image.
Don'ts
- ✗Don't use
COPY . .without a.dockerignore— you will eventually leak a.env, a private key, or a stalevenv/into a published image layer. - ✗Don't pin
FROM python:latestor use unpinned tags — reproducibility evaporates the moment upstream republishes the tag; pin topython:3.11-slim-bookwormor a digest. - ✗Don't
pip installafter copying your source, or interleave dependency and code steps — any code edit then busts the dependency layer and forces a full reinstall; keep therequirements.txtinstall in its own earlier layer.
This lesson is free to read. Its hands-on lab — real code, in a cloud IDE — is part of the GenAI Agent Engineering subscription.
From · cancel anytime
Listen to this lesson
Audio overviews of this lesson's labs and its chapter, from GenBodha Bytes.
- Containerizing LLM ApplicationsChapter overview23 min
More free lessons in Kubernetes Essentials for GenAI Engineers
- Ch 1Write a Python app that calls the Gemini API and returns structured responses
- Ch 1Write a Dockerfile and build a container image for the LLM appYou are here
- Ch 1Use Docker Compose to run the LLM app with supporting services
- Ch 2Deploy the LLM app as your first Kubernetes pod
- Ch 4Manage deployment lifecycle with kubectl rollout
- Ch 9Create a Helm chart for the LLM chat application
- Ch 9Use Kustomize bases and overlays for the LLM app