Containerizing an Agent with Docker
A Docker image freezes the agent runtime, OS packages, and Python deps so every host runs the same binary surface.
Use this recipe when you promote a working agent from a laptop venv into something a registry and orchestrator can ship.
Summary
Build a multi-stage, non-root image with pinned deps, a health endpoint, runtime-injected secrets, and tags you can roll back.
Recipe
- Split app code, dependency lock, and runtime config (env/secrets).
- Write a
.dockerignorethat excludes.venv,.git,.env, and tests caches. - Use a multi-stage Dockerfile: builder installs wheels; runtime copies only what executes.
- Install only runtime OS packages your tools need (for example
ca-certificates; add browsers only when required). - Run as a non-root user; listen on
PORTfrom the environment. - Expose a cheap health check that does not call the model.
- Build with an immutable tag (
gitSHA); never rely onlatestalone in production. - Scan and push to your registry; deploy the digest or tag through CI.
- Inject secrets at run time (
-e, orchestrator secrets, or a secrets sidecar) - neverENV KEY=...with real values in the Dockerfile. - Smoke-test
healthz+ one agent turn against the running container before cutting traffic.
Working Example
# requirements.txt (pin in real repos)
fastapi==0.115.6
uvicorn[standard]==0.34.0
httpx==0.28.1
pydantic==2.10.4# agent_app.py
import os
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class Turn(BaseModel):
message: str
@app.get("/healthz")
def healthz() -> dict:
return {"status": "ok"}
@app.post("/v1/turn")
async def turn(body: Turn) -> dict:
# Replace with your agent loop; keep I/O timeouts explicit
return {"reply": body.message[::-1], "model": os.getenv("AGENT_MODEL", "unset")}# Dockerfile
FROM python:3.12-slim AS builder
WORKDIR /build
RUN apt-get update && apt-get install -y --no-install-recommends build-essential \
&& rm -rf /var/lib/apt/lists/*
COPY requirements.txt .
RUN pip install --upgrade pip \
&& pip wheel --no-cache-dir --wheel-dir /wheels -r requirements.txt
FROM python:3.12-slim AS runtime
ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1 PORT=8080
WORKDIR /app
RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates \
&& rm -rf /var/lib/apt/lists/* \
&& useradd --create-home --uid 10001 appuser
COPY --from=builder /wheels /wheels
COPY requirements.txt .
RUN pip install --no-cache-dir --no-index --find-links=/wheels -r requirements.txt \
&& rm -rf /wheels
COPY agent_app.py .
USER appuser
EXPOSE 8080
CMD ["sh", "-c", "uvicorn agent_app:app --host 0.0.0.0 --port ${PORT}"]docker build -t your-registry.example/agent:$(git rev-parse --short HEAD) .
docker run --rm -p 8080:8080 \
-e PORT=8080 \
-e AGENT_MODEL=openai/gpt-4o-mini \
-e OPENROUTER_API_KEY \
your-registry.example/agent:$(git rev-parse --short HEAD)Deep Dive
What belongs in the image
| Include | Exclude |
|---|---|
| Language runtime and pinned libs | .env and secret files |
| Minimal OS libs tools need | Dev compilers in the final stage |
| App source and static prompts if versioned with code | Local datasets you mount at runtime |
| Non-root user definition | SSH keys, cloud credentials |
Prompts and tool schemas that change with releases should be versioned with the image or loaded from an explicit config version (Versioning Prompts and Tool Schemas Alongside Code).
Layers and cache
- Copy lockfiles before source so dependency layers cache across code edits.
- Multi-stage drops build-essential from the final image.
- Prefer
pip install --no-cache-dir(or equivalent) to avoid leftover indexes in layers.
Tooling extras
Agents that run Playwright, OCR, or native libs need extra packages and often larger images.
Keep those deps in a dedicated image flavor (agent:tools) rather than bloating every API replica.
Sandbox code-exec tools in separate containers when possible (Containerized Sandboxes for Code-Executing Agent Tools).
Orchestrator contract
Most hosts expect:
- A process that listens on
PORTor a documented port - A health/readiness path
- Exit non-zero on fatal misconfig (missing required env)
- Graceful shutdown on
SIGTERMso in-flight turns can finish or checkpoint
# pattern: fail fast on missing prod secrets
if os.getenv("ENV") == "production" and not os.getenv("OPENROUTER_API_KEY"):
raise SystemExit("OPENROUTER_API_KEY required in production")Gotchas
- Baking secrets into
ENVorCOPY .env. Image history and registries leak them. - Running as root. Break-outs and volume mounts become more dangerous.
- Calling the model inside health checks. Probes cause spend and flappy restarts.
- Assuming
/tmpis large and durable. Many platforms give small ephemeral disks. - Forgetting timezone / locale / CA certs. TLS and log timestamps break in slim images.
latestonly tags. You cannot roll back cleanly.- Huge build contexts. Missing
.dockerignoreslows CI and can include secrets. - One mega-image for API + browser + GPU. Split flavors; scale them independently.
Alternatives
| Approach | When | Trade-off |
|---|---|---|
| Single-stage Dockerfile | Tiny demos | Larger, dirtier images |
| Distroless / chainguard-style bases | Hardened prod | Harder debugging shells |
| Buildpacks | Teams avoiding Dockerfiles | Less control over OS packages |
| Zip deploy (Lambda) without container | Pure serverless Python | Different packaging rules |
| Nix / deterministic builds | Strong supply-chain goals | Steeper learning curve |
FAQs
Do I need Docker Compose in production?
Not required. Compose is excellent for local multi-service setups. Production often uses a managed orchestrator or a single container service.
How do I pass secrets on Kubernetes?
Mount Secrets as env vars or files; reference them from the Deployment. Rotate by updating the Secret and rolling pods.
Should prompts live in the image?
If they ship with the release, yes or load a versioned object from storage. Avoid editing prompts only on a live server with no audit trail.
What base image should I pick?
Official language slim images are a solid default. Pin digests for higher supply-chain control when your org requires it.
How do I debug a non-root, no-shell image?
Keep a debug image flavor with a shell for incidents, or use ephemeral debug sidecars your platform supports.
Can the same image run on Lambda and ECS?
Sometimes via Lambda container images, with an adapter entrypoint. Prefer one primary runtime path unless you invest in dual smoke tests.
Where should browser binaries live?
In a dedicated tools image or a managed browser service - not in every lightweight API container.
How small should the image be?
Small enough for fast pulls on scale-out. Measure pull time under deploy, not vanity MB counts alone.
Related
Related: Deploying Agents Basics
Related: Choosing a Deployment Target for an Agent Workload
Related: Deploying an Agent to Vercel or AWS Lambda Serverless Functions
Related: Environment and Secrets Management Across Deployment Targets
Related: Deploying Agents Best Practices
Related: Containerized Sandboxes for Code-Executing Agent Tools
Stack versions: Pins from the category manifest (verify at build): OpenRouter (~315+ models, July 2026 pricing/fees); LangGraph 1.0+; CrewAI 1.14+; Microsoft Agent Framework 1.0; Vercel AI SDK 6; Pydantic AI (latest); LlamaIndex (latest); OpenAI Agents SDK (latest + MCP); MCP (Linux Foundation governance); A2A (HTTP+SSE+JSON-RPC 2.0); Solana
@solana/web3.js+@solana/spl-token.