Deploy with Docker
Packaging the Application With Its Environment
An application that runs on a developer's laptop depends on the laptop: its Python version, its installed libraries, its files, and its settings. A container packages the application together with exactly the environment it needs, so the same image runs the same way on any machine that can run containers. Docker is the most widely used tool for building and running containers, and a Dockerfile is the text file that describes how an image is built.
This lesson covers how to write a Dockerfile for an application that calls Claude, how to keep secrets and local files out of the image, how to run the process as a non-root user, and how to check a Dockerfile against the rules this section has established. The key rule is that the image contains code and dependencies, and the settings and secrets arrive when the container starts, as Lessons 164 and 170 described.
What Goes in the Image
An image should contain what the program needs to run, and nothing else. Development tools, test files, local settings, version control history, and secret files do not belong in it. The image is built once and run many times, often in several places, so anything inside it is copied everywhere the image goes. A key placed in the image is a key in every copy of the image, in every registry that stores it, and in every machine that pulls it.
The standard approach has four parts: a pinned base image, the dependencies installed from a requirements file, the application code, and a non-root user that runs the process. Configuration and secrets are supplied at run time through the environment.
A Dockerfile
The Dockerfile below packages a small application. Each instruction is a separate layer, and the order matters: dependencies are installed before the code is copied, so that changing the code does not reinstall the dependencies:
# Pin a specific version of the base image, never "latest"
FROM python:3.12-slim
# Work in a fixed directory inside the image
WORKDIR /app
# Install dependencies first, so this layer is reused when only the code changes
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy the application code, and nothing else
COPY app/ ./app/
# Run as an unprivileged user, not root
RUN useradd --create-home appuser
USER appuser
# Output is shown immediately in container logs
ENV PYTHONUNBUFFERED=1
CMD ["python", "-m", "app.main"]
Each line makes a specific choice. The base image is pinned to a version, so a rebuild does not silently change the Python version. The dependencies come from a requirements file, so the same versions are installed every time. The code is copied from one folder, so local files outside it stay out. The process runs as appuser, so a compromised process has fewer privileges inside the container. The environment variable makes log lines appear as they are written, which matters when a container's logs are read live.
Keeping Secrets Out
The Dockerfile contains no key, and it must not. A key set with an ENV instruction is stored in the image's history, where anyone with the image can read it, even after a later instruction deletes it. A key copied in with COPY .env ends up in the image for the same reason. The correct approach is to supply the key when the container starts, through the platform's secret mechanism or the environment, as Lesson 164 described:
docker run --env-file ./local.env my-app-image
The --env-file option reads the variables from a file on the host at run time, and nothing is written into the image. The file stays on the developer's machine, and it is excluded from the build.
Excluding Files From the Build
A .dockerignore file lists paths that are not sent to the build at all. It works like the ignore file for version control (Lesson 173), and it should exclude at least the files that must never reach an image:
.env
.env.*
!.env.example
.git
__pycache__
*.pyc
tests/
*.pem
Excluding files from the build context is safer than relying on COPY to select them, because a file never sent to the build cannot be copied by mistake.
Checking the Dockerfile
Before a Dockerfile is used, it can be checked against the rules that have been established. The checker below reads the text and reports violations: an unpinned base image, an API key set in an ENV instruction, a COPY that brings in a .env file, and a process that runs as root. It is a simple text check, so it can run in a continuous integration pipeline and reject a risky Dockerfile before an image is built:
"""dockerfile_check.py: check a Dockerfile against the rules for secrets, pinning, and privilege."""
def lint(text: str) -> list[str]:
problems = []
lines = [line.strip() for line in text.splitlines() if line.strip() and not line.strip().startswith("#")]
for line in lines:
words = line.split()
if words[0] == "FROM" and words[1].endswith(":latest"):
problems.append("base image uses :latest; pin a version")
if words[0] == "ENV" and any("API_KEY" in word for word in words[1:]):
problems.append("API key set in an ENV instruction")
if words[0] == "COPY" and ".env" in words[1:]:
problems.append("COPY brings in a .env file")
if not any(line.split()[0] == "USER" and line.split()[1] != "root" for line in lines):
problems.append("container runs as root")
return problems
GOOD = """FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app/ ./app/
USER appuser
CMD ["python", "-m", "app.main"]
"""
BAD = """FROM python:latest
ENV ANTHROPIC_API_KEY=placeholder
COPY . .
COPY .env .
CMD ["python", "-m", "app.main"]
"""
if __name__ == "__main__":
print("good:", lint(GOOD) or "no problems")
print("bad:", lint(BAD))
Output:
good: no problems
bad: ['base image uses :latest; pin a version', 'API key set in an ENV instruction', 'COPY brings in a .env file', 'container runs as root']
How It Works
- Comments and blank lines are removed first, so a commented-out instruction is never mistaken for a live one.
- Each instruction is read by its first word, so the checks look at the instruction and its arguments rather than matching substrings anywhere in the file.
- The base image check looks for the
latesttag, which changes over time and makes builds unrepeatable. - The
ENVcheck looks for key names among the arguments, which catches a key placed in the image's history. - The
COPYcheck looks for.envas a separate word, so a file named.envis caught without flagging a name that merely contains the letters. - The root check requires an explicit non-root
USER. A Dockerfile with noUSERinstruction runs as root by default, so its absence is a violation.
The check is a text rule, and it cannot confirm that the image is secure. It catches the most common mistakes before an image is built, and the image still needs scanning for vulnerabilities in its base and its dependencies.
Build, Run, and Verify
The image is built from the folder that holds the Dockerfile, and it is run with the settings it needs:
docker build -t my-app-image .
docker run --env-file ./local.env my-app-image
After the container starts, its logs should show the structured lines from Lesson 187, and no secret values should appear in them. Verifying the running container, and not only the build, catches problems that the build cannot see, such as a settings file that is missing at run time.
Summary
A container packages the application with its dependencies and runs it the same way everywhere. A Dockerfile pins the base image, installs dependencies before copying code, copies only what the program needs, and runs as a non-root user. Secrets never enter the image: they arrive at run time, and a .dockerignore file keeps local files out of the build. A text check catches the most common mistakes before a build. The next lesson applies these ideas to a web application built with FastAPI.