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, and ENTRYPOINT instructions 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 .env files, .git history, and local virtualenvs into your image.
  • Multi-stage build — a Dockerfile that defines more than one FROM stage 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 FROM that your stage starts from; choosing python:3.11-slim-bookworm over python:3.11 for 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.

Loading diagram...

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 --from=builder /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

  1. Do copy requirements.txt and pip install before your source code — this is the single biggest cache win; it turns minute-long rebuilds into two-second ones.
  2. Do use a multi-stage build with a -slim runtime base — the compiler toolchain belongs in the builder stage, never in the image you ship to production.
  3. Do install dependencies into a self-contained virtualenv and COPY --from=builder it into the runtime — the runtime stage gets the resolved packages with zero build tooling, and pip itself never runs in the final image.

Don'ts

  1. Don't use COPY . . without a .dockerignore — you will eventually leak a .env, a private key, or a stale venv/ into a published image layer.
  2. Don't pin FROM python:latest or use unpinned tags — reproducibility evaporates the moment upstream republishes the tag; pin to python:3.11-slim-bookworm or a digest.
  3. Don't pip install after copying your source, or interleave dependency and code steps — any code edit then busts the dependency layer and forces a full reinstall; keep the requirements.txt install 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.

More free lessons in Kubernetes Essentials for GenAI Engineers

All free lessons in GenAI Agent Engineering