Deploy with Docker

Su Suriya Ravichandran Updated 10 Nov 2026
7 min read ·Lesson 188 of 200

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

  1. Comments and blank lines are removed first, so a commented-out instruction is never mistaken for a live one.
  2. 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.
  3. The base image check looks for the latest tag, which changes over time and makes builds unrepeatable.
  4. The ENV check looks for key names among the arguments, which catches a key placed in the image's history.
  5. The COPY check looks for .env as a separate word, so a file named .env is caught without flagging a name that merely contains the letters.
  6. The root check requires an explicit non-root USER. A Dockerfile with no USER instruction 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.

0 Comments

Reviewed before they appear

No comments yet.

Anthropic SDK Using Python
Anthropic SDK Intro Claude API Intro SDK Requirements Install Anthropic SDK Upgrade the SDK Create an API Key Set API Keys Use Environment Variables Import Anthropic Create a Client Send Your First Message Choose a Claude Model Set Max Tokens Read a Response Print Response Text Understand Message Blocks Use System Prompts Send User Messages Set Temperature Handle Stop Reasons Messages API Intro Create Messages Use Message Roles Send Conversation History Continue a Conversation Build a Chat Loop Reset Chat History Use Multiple Turns Inspect Message IDs Read Usage Data Write Clear Prompts Set a System Prompt Add Context to Prompts Use Prompt Templates Add Examples to Prompts Request JSON Output Use XML Tags Control Response Length Improve Prompt Quality Avoid Prompt Injection Read Text Blocks Read Content Blocks Check Stop Reasons Count Input Tokens Count Output Tokens Count Tokens Before Sending Limit Output Tokens Use Stop Sequences Read Request IDs Parse API Responses Streaming Intro Stream Text Output Use Stream Events Use Stream Helpers Get Final Messages Stream Async Responses Handle Stream Errors Build a Live Chat Stream Long Responses Close Streams Safely Build a Chatbot Keep Chat History Manage Chat Sessions Add Chat Memory Build a CLI Chatbot Build a Web Chatbot Use Flask with Claude Use FastAPI with Claude Return JSON from an API Add Chat Error Messages Async SDK Intro Create an Async Client Send Async Messages Use asyncio Run Parallel Requests Stream Async Text Handle Async Errors Set Async Timeouts Close Async Clients Choose Sync or Async Tool Use Intro Define a Tool Create Tool Schemas Send Tools to Claude Read Tool Calls Run Python Tools Return Tool Results Continue After Tool Use Control Tool Choice Handle Tool Errors Use the beta_tool Decorator Create a Tool Runner Add Tool Docstrings Validate Tool Inputs Use Multiple Tools Limit Tool Calls Log Tool Activity Build a Weather Tool Build a Search Tool Build a Calculator Tool Structured Output Intro Request JSON Data Validate JSON Output Use Pydantic Models Handle Invalid JSON Extract Fields from Text Generate Typed Data Use Image URLs Send Base64 Images Read PDFs with Claude Use Output Schemas Build Data Extraction Test Structured Responses Send Image Inputs Upload Files Use File IDs Process Text Files Summarize Documents Extract Table Data Handle File Errors Read Cache Usage Reduce Repeated Input Costs Debug Cache Misses Prompt Caching Intro Enable Prompt Caching Cache System Prompts Cache Long Context Test Cache Performance Use Cache Breakpoints Cache Best Practices Message Batches Intro Create a Message Batch Set Batch IDs Check Batch Status List Message Batches Get Batch Results Process Batch Outputs Handle Failed Batch Items Use Batches for Bulk Tasks Compare Batches and Requests Handle API Errors Handle Rate Limits Retry Failed Requests Set Request Timeouts Handle Network Errors Use Exponential Backoff Build Reliable API Calls Use Extended Thinking Log API Errors Track Request IDs Paginate API Results Set Thinking Budgets Read Thinking Blocks Use Tool Search Use Web Search Tools Use Computer Tools Use Citations Use Token Counting Read Raw Responses Set Custom Headers Use Claude on AWS Bedrock Use Claude on Vertex AI Use Claude on Microsoft Foundry Configure Cloud Credentials Choose a Cloud Provider Compare API Integrations Configure Provider Clients Handle Cloud Errors Use Cloud SDK Extras Deploy Cloud Apps Protect API Keys Use a .env File Ignore Secrets in Git Validate User Input Reduce Prompt Injection Limit Tool Permissions Avoid Unsafe Tool Calls Sanitize Tool Results Set Usage Limits Review Security Logs Test SDK Functions Mock API Responses Test Error Handling Test Streaming Code Measure Token Usage Track API Costs Configure Logging Deploy with Docker Deploy a FastAPI App Monitor Production Apps Build a Claude Chatbot Build a Text Summarizer Build a Code Explainer Build a JSON Extractor Build a PDF Assistant Build a Streaming Chat App Build a Tool-Calling Agent Build a Batch Processor Build a Research Assistant Build a Claude API Dashboard
Ask about this post
AI Ask about this post

Ask questions about Deploy with Docker and get answers drawn from it.

Signed-in readers only.