PromptisePromptise
Docs
GitHub
Promptise - AI Framework LogoPromptise

The foundation layer for agentic intelligence. Build, secure and operate autonomous AI systems with Promptise Foundry.

pip install promptise

[01] Foundry

  • MCPcast
  • The Promptise Agent
  • Reasoning Engine
  • MCP
  • Agent Runtime
  • Prompt Engineering
  • Execution Engine
  • Agent Identity

[02] Resources

  • Documentation
  • GitHub
  • Guides
  • Learning Paths
  • Questions

[03] Company

  • About
  • Terms of Service
  • Privacy Policy
  • Cookie Policy
  • Subprocessors

© 2026 Promptise by Manser Ventures. All rights reserved.

Open source · Python

← Learning paths> Agentic Engineering with Promptise Foundry
Lesson 34 / 71
48%

How to Deploy an AI Agent with Docker (2026 Guide)

Deploy an AI agent with Docker: a Dockerfile, env config and secrets, health checks, graceful shutdown and docker compose with an MCP server. Tested.

Intermediate18 minDeploymentModel Context Protocol (MCP)
All 71 lessons↓
  1. ✓What Is an AI Agent? Definition, How It Works, Examples
  2. ✓How to Build an AI Agent in Python (2026 Guide)
  3. ✓What Is an Agent Harness? Parts and How to Build One
  4. ✓System Prompt Best Practices for AI Agents in Python
  5. ✓Best LLM for AI Agents: Test and Switch Models in Python
  6. ✓AI Agent YAML: Define and Run an Agent From a Config File
  7. ✓Streaming LLM Responses with FastAPI: Tokens and Tool Calls
  8. ✓Claude AI Agent with MCP: Use Anthropic Models in Promptise
  9. ✓Gemini AI Agent with MCP: Use Google Models in Promptise
  10. ✓Azure OpenAI Agent with MCP: Use Deployments in Promptise
  11. ✓OpenRouter AI Agent with MCP: Many Models in Promptise
  12. ✓Ollama AI Agent with MCP: Run Local Models in Promptise
  13. ✓LLM Semantic Cache and Model Fallback in Python
  14. ✓What Is MCP (Model Context Protocol)? A Developer's Guide
  15. ✓What Is Tool Calling? LLM Function Calling Explained
  16. ✓MCP Tools vs Resources vs Prompts (With Python Examples)
  17. ✓How to Connect MCP Servers to Your AI Agent in Python
  18. ✓Build a Production MCP Server in Python (Step by Step)
  19. ✓MCP Transports: stdio vs Streamable HTTP vs SSE (Python)
  20. ✓FastAPI MCP Server: Turn Your FastAPI App into MCP Tools
  21. ✓OpenAPI to MCP: Turn Any REST API into an MCP Server
  22. ✓MCP Authentication in Python: API Keys, JWT and OAuth
  23. ✓Build an Authenticated MCP Server for Your AI Agent (Python)
  24. ✓MCP Security Best Practices: A Checklist for MCP Servers
  25. ✓Testing MCP Servers in Python: pytest Without a Live Server
  26. ✓Too Many MCP Tools? LLM Tool Selection, Measured
  27. ✓MCP Server Caching, Timeouts and Circuit Breakers in Python
  28. ✓MCP vs CLI for AI Agents: Tokens, Security, When to Use Each
  29. ✓AI Agent Audit Logging: Tamper-Evident Logs in Python
  30. ✓AI Agent Human in the Loop: Approve Tool Calls in Python
  31. ✓AI Agent Prompt Injection: How to Defend Against It
  32. ✓Build a Multi-Agent System in Python (and When Not To)
  33. ✓Context Engineering for AI Agents: Context Window Management
  34. 34How to Deploy an AI Agent with Docker (2026 Guide)
  35. 35LLM Tool Rate Limiting: Per-Client Limits for MCP Servers
  36. 36MCP Long-Running Tasks: Background Jobs in Python
  37. 37MCP Server Monitoring: Prometheus, OpenTelemetry and Logs
  38. 38Python AI Agent Frameworks Compared: Which to Choose (2026)
  39. 39Self-Improving AI Agent: Learn From Tool Failures in Python
  40. 40AI Agent Identity: Authenticate Agents to MCP Servers
  41. 41Code-Action Agents in Python: One Program vs 37 Tool Calls
  42. 42Multi-Tenant AI Agents: Keep Each Customer's Data Apart
  43. 43What Is Agentic RAG? How It Differs from Classic RAG
  44. 44AI Agent Memory in Python: Short-Term and Long-Term
  45. 45Agentic RAG in Python: Give Your AI Agent a Search Tool
  46. 46AI Agent Reasoning Patterns in Python: 9 Patterns Tested
  47. 47Plan-and-Execute Agents in Python: Beyond the ReAct Loop
  48. 48Self-Modifying AI Agent in Python: Open Mode & Limits
  49. 49CrewAI Alternatives: 7 Options and How to Migrate (2026)
  50. 50Migrate from AutoGen: Agent Framework, Promptise and More
  51. 51What Is an LLM Guardrail? Types, Examples and Limits
  52. 52AI Agent Guardrails in Python: Block Injection, Redact PII
  53. 53AI Agent Code Execution Sandbox: Run LLM Code Safely
  54. 54AI Agent Production Checklist: What to Check Before Launch
  55. 55Approval Fatigue: Auto-Approve Safe AI Agent Actions
  56. 56What Is Human in the Loop AI? Approvals for AI Agents
  57. 57AI Agent Notifications: Webhook and Slack Alerts in Python
  58. 58AI Agent Observability: Trace Tool Calls, Tokens and Cost
  59. 59AI Agent Stuck in a Loop? How to Detect and Stop It
  60. 60How to Run, Debug and Deploy an AI Agent in Python
  61. 61AI Agent Cost Tracking: Tokens and Cost per Run and User
  62. 62How to Evaluate AI Agents: Evals, LLM Judges and pass^k
  63. 63AI Agent Cost Control: Budgets That Stop Runaway Spend
  64. 64How to Test LLM Prompts and AI Agents in Python
  65. 65AI Agent Evals in CI: Run Them on Every Pull Request
  66. 66Autonomous AI Agent Runtime: Budgets, Crash Recovery, API
  67. 67Event-Driven AI Agents: Webhook, Cron and File Triggers
  68. 68Durable AI Agents: Crash Recovery and Safe Resume
  69. 69Deploy AI Agents in Production: Long-Running Python Agents
  70. 70What Is an AI Agent Framework? When You Need One
  71. 71LangGraph Alternatives: 7 Options and How to Migrate (2026)

Lessons in this path

  1. ✓What Is an AI Agent? Definition, How It Works, Examples
  2. ✓How to Build an AI Agent in Python (2026 Guide)
  3. ✓What Is an Agent Harness? Parts and How to Build One
  4. ✓System Prompt Best Practices for AI Agents in Python
  5. ✓Best LLM for AI Agents: Test and Switch Models in Python
  6. ✓AI Agent YAML: Define and Run an Agent From a Config File
  7. ✓Streaming LLM Responses with FastAPI: Tokens and Tool Calls
  8. ✓Claude AI Agent with MCP: Use Anthropic Models in Promptise
  9. ✓Gemini AI Agent with MCP: Use Google Models in Promptise
  10. ✓Azure OpenAI Agent with MCP: Use Deployments in Promptise
  11. ✓OpenRouter AI Agent with MCP: Many Models in Promptise
  12. ✓Ollama AI Agent with MCP: Run Local Models in Promptise
  13. ✓LLM Semantic Cache and Model Fallback in Python
  14. ✓What Is MCP (Model Context Protocol)? A Developer's Guide
  15. ✓What Is Tool Calling? LLM Function Calling Explained
  16. ✓MCP Tools vs Resources vs Prompts (With Python Examples)
  17. ✓How to Connect MCP Servers to Your AI Agent in Python
  18. ✓Build a Production MCP Server in Python (Step by Step)
  19. ✓MCP Transports: stdio vs Streamable HTTP vs SSE (Python)
  20. ✓FastAPI MCP Server: Turn Your FastAPI App into MCP Tools
  21. ✓OpenAPI to MCP: Turn Any REST API into an MCP Server
  22. ✓MCP Authentication in Python: API Keys, JWT and OAuth
  23. ✓Build an Authenticated MCP Server for Your AI Agent (Python)
  24. ✓MCP Security Best Practices: A Checklist for MCP Servers
  25. ✓Testing MCP Servers in Python: pytest Without a Live Server
  26. ✓Too Many MCP Tools? LLM Tool Selection, Measured
  27. ✓MCP Server Caching, Timeouts and Circuit Breakers in Python
  28. ✓MCP vs CLI for AI Agents: Tokens, Security, When to Use Each
  29. ✓AI Agent Audit Logging: Tamper-Evident Logs in Python
  30. ✓AI Agent Human in the Loop: Approve Tool Calls in Python
  31. ✓AI Agent Prompt Injection: How to Defend Against It
  32. ✓Build a Multi-Agent System in Python (and When Not To)
  33. ✓Context Engineering for AI Agents: Context Window Management
  34. 34How to Deploy an AI Agent with Docker (2026 Guide)
  35. 35LLM Tool Rate Limiting: Per-Client Limits for MCP Servers
  36. 36MCP Long-Running Tasks: Background Jobs in Python
  37. 37MCP Server Monitoring: Prometheus, OpenTelemetry and Logs
  38. 38Python AI Agent Frameworks Compared: Which to Choose (2026)
  39. 39Self-Improving AI Agent: Learn From Tool Failures in Python
  40. 40AI Agent Identity: Authenticate Agents to MCP Servers
  41. 41Code-Action Agents in Python: One Program vs 37 Tool Calls
  42. 42Multi-Tenant AI Agents: Keep Each Customer's Data Apart
  43. 43What Is Agentic RAG? How It Differs from Classic RAG
  44. 44AI Agent Memory in Python: Short-Term and Long-Term
  45. 45Agentic RAG in Python: Give Your AI Agent a Search Tool
  46. 46AI Agent Reasoning Patterns in Python: 9 Patterns Tested
  47. 47Plan-and-Execute Agents in Python: Beyond the ReAct Loop
  48. 48Self-Modifying AI Agent in Python: Open Mode & Limits
  49. 49CrewAI Alternatives: 7 Options and How to Migrate (2026)
  50. 50Migrate from AutoGen: Agent Framework, Promptise and More
  51. 51What Is an LLM Guardrail? Types, Examples and Limits
  52. 52AI Agent Guardrails in Python: Block Injection, Redact PII
  53. 53AI Agent Code Execution Sandbox: Run LLM Code Safely
  54. 54AI Agent Production Checklist: What to Check Before Launch
  55. 55Approval Fatigue: Auto-Approve Safe AI Agent Actions
  56. 56What Is Human in the Loop AI? Approvals for AI Agents
  57. 57AI Agent Notifications: Webhook and Slack Alerts in Python
  58. 58AI Agent Observability: Trace Tool Calls, Tokens and Cost
  59. 59AI Agent Stuck in a Loop? How to Detect and Stop It
  60. 60How to Run, Debug and Deploy an AI Agent in Python
  61. 61AI Agent Cost Tracking: Tokens and Cost per Run and User
  62. 62How to Evaluate AI Agents: Evals, LLM Judges and pass^k
  63. 63AI Agent Cost Control: Budgets That Stop Runaway Spend
  64. 64How to Test LLM Prompts and AI Agents in Python
  65. 65AI Agent Evals in CI: Run Them on Every Pull Request
  66. 66Autonomous AI Agent Runtime: Budgets, Crash Recovery, API
  67. 67Event-Driven AI Agents: Webhook, Cron and File Triggers
  68. 68Durable AI Agents: Crash Recovery and Safe Resume
  69. 69Deploy AI Agents in Production: Long-Running Python Agents
  70. 70What Is an AI Agent Framework? When You Need One
  71. 71LangGraph Alternatives: 7 Options and How to Migrate (2026)
← Path overview

This guide shows how to deploy an AI agent with Docker, from a Dockerfile to a docker compose stack with the agent's MCP tool server beside it. It is for developers who have an agent that works on their laptop and now need it to run unattended. You will package the agent as an HTTP service, configure it with environment variables, keep secrets out of the image, add health and readiness checks, and make it finish its running requests when the container stops. Every command below was run, and the outputs are real.

[01]

How do you deploy an AI agent with Docker?

Run the agent as a long-lived HTTP service in a slim Python image, with every setting and secret coming from environment variables. Give it a liveness endpoint and a readiness endpoint, start it in exec form so it receives SIGTERM, and let it drain running requests before it exits. Put each MCP tool server in its own container on a private network, and start the agent only once those servers are healthy. With Promptise Foundry, promptise run --prod --serve provides the HTTP API, the probes, JSON logs and the drain.

  • Build once, configure at run time. The same image runs in staging and production. Only the environment changes.

  • Secrets never enter the image. Exclude .env with .dockerignore, and inject keys from your orchestrator.

  • Liveness and readiness are different questions. /healthz means the process is up. /readyz means the agent is built and its tools are connected.

  • Shutdown is part of the deploy. A rolling update sends SIGTERM. An agent request can take many seconds, so the grace period must be longer than your slowest request.

  • Agents scale out like any stateless web service, as long as conversation state lives in a shared store.


[02]

How a containerized AI agent works

An agent service has the same shape as any API service. The difference is what happens inside a request: several model calls and tool calls that can take from a few seconds to a minute.

Text
1
2
3
4
5
6
7
8
9
10
11
12
                 docker compose network (private)
client ──HTTPS──▶ ┌───────────────────────────────┐        ┌─────────────────────────┐
(app, gateway)    │ agent container               │  MCP   │ shop-mcp container      │
                  │ promptise run --prod --serve  │──HTTP─▶│ python server.py http   │──▶ orders DB
 probes ────────▶ │  GET  /healthz   liveness     │        │  POST /mcp              │
                  │  GET  /readyz    readiness    │        │  GET  /health           │
                  │  POST /v1/invoke, /v1/stream  │        └─────────────────────────┘
                  └──────────────┬────────────────┘
                                 │ HTTPS
                                 ▼
                         model provider API
   env: OPENAI_API_KEY, PROMPTISE_SERVE_TOKEN, SHOP_MCP_URL     (injected, never baked in)

Four things change when an agent moves from a script to a container:

  • Configuration comes from the environment. A .env file on your laptop must not end up in the image.

  • The process is managed. Docker or Kubernetes starts it, probes it, restarts it when it dies and stops it with SIGTERM.

  • Startup has dependencies. The agent connects to its MCP servers when it is built. If a server isn't up yet, the agent fails to start.

  • More than one copy may run. Anything kept in process memory, such as a conversation, is lost on restart and not shared between replicas.


[03]

When to use Docker for an AI agent (and when not to)

Use a container when the agent is a service other systems call: a support bot behind your web app, an internal API, a worker that runs on a schedule. It gives you the same artifact in every environment, and it is the unit Kubernetes, ECS, Cloud Run and Fly.io deploy.

You don't need a separate container when the agent is a library inside an existing web app. Then you deploy the app as usual and call build_agent() inside it. A one-off batch job can also run with plain python main.py in CI. For agents that run on their own for days with triggers and budgets, see deploying long-running AI agents.


[04]

What you need

  • Docker 24 or later with Docker Compose v2. The commands here ran on Docker 27.4.

  • Python 3.10 or later and Promptise Foundry 1.4.1 for local runs: pip install "promptise==1.4.1".

  • An OpenAI API key in OPENAI_API_KEY. The agent uses gpt-5-mini.

  • About 30 minutes, most of it the first image build.

The example is a shop support agent with two tools, get_order and check_stock, served by an MCP server. The layout:

Text
1
2
3
4
5
6
7
8
9
support-bot/
├── compose.yaml
├── agent/          # the agent service
│   ├── Dockerfile  .dockerignore  requirements.txt
│   ├── promptise.toml
│   └── agent.py
└── shop-mcp/       # the MCP tool server
    ├── Dockerfile  .dockerignore
    └── server.py

[05]

Step 1: Write the agent factory and the manifest

Build the agent in a function, and read every deployment setting from the environment. The URL of the MCP server is configuration, not code:

Pythonagent/agent.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
"""The support agent. It reaches the shop tools over MCP at SHOP_MCP_URL."""
import os

from promptise import PromptiseAgent, build_agent
from promptise.config import HTTPServerSpec


async def create_agent(ctx=None) -> PromptiseAgent:
    return await build_agent(
        model=os.environ.get("AGENT_MODEL", "openai:gpt-5-mini"),
        instructions="You answer shop customers. Use the tools for facts. Keep answers short.",
        servers={"shop": HTTPServerSpec(url=os.environ["SHOP_MCP_URL"])},
        max_invocation_time=60,      # seconds; a stuck request ends instead of hanging
    )

max_invocation_time=60 ends a request that runs longer than a minute, so one stuck request can't hold a worker forever. The manifest tells promptise run which factory to serve, which variables must be set, and how to behave in production:

TOMLagent/promptise.toml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
name = "support-bot"
version = "1.0.0"
agent = "agent.py:create_agent"

[env]
required = ["OPENAI_API_KEY", "SHOP_MCP_URL", "PROMPTISE_SERVE_TOKEN"]

[prod]
log_format = "json"
observe = "basic"          # one JSON log line per run, with its token totals
shutdown_timeout = 30      # seconds running requests get after SIGTERM

[serve]
port = 8080
max_concurrency = 8        # requests beyond this get 429

[pricing]
"gpt-5-mini*" = { input = 0.25, output = 2.00 }
  • `[env] required` lists the variables the container needs. If one is missing, the process stops with exit code 2 before it builds anything.

  • `[prod]` turns on JSON logs on stderr, records one log line per agent run with its token totals (observe = "basic"), and gives running requests 30 seconds after SIGTERM.

  • `[serve]` sets the port and how many requests run at the same time. Requests beyond max_concurrency get 429 with Retry-After instead of piling up.

  • `[pricing]` adds a cost estimate to the final stats log line.

The manifest holds no secrets, so it is safe to commit and to copy into the image.


[06]

Step 2: Write the Dockerfile

Dockerfileagent/Dockerfile
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# syntax=docker/dockerfile:1
FROM python:3.12-slim

ENV PYTHONUNBUFFERED=1 \
    PIP_NO_CACHE_DIR=1 \
    PIP_DISABLE_PIP_VERSION_CHECK=1 \
    # Configuration comes from the container's environment, never from a .env file.
    PROMPTISE_NO_DOTENV=1

# An unprivileged user to run the agent.
RUN useradd --create-home --uid 10001 app
WORKDIR /app

# Dependencies first, so a code change does not reinstall them.
COPY requirements.txt .
RUN pip install -r requirements.txt

# promptise run only trusts a manifest owned by the user that runs it.
COPY --chown=app:app promptise.toml agent.py ./

USER app
EXPOSE 8080
HEALTHCHECK --interval=10s --timeout=3s --start-period=20s --retries=3 \
  CMD ["python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8080/readyz', timeout=2)"]

# Exec form: promptise is PID 1 and receives the SIGTERM that docker stop sends.
CMD ["promptise", "run", "--prod", "--serve", "--host", "0.0.0.0"]

What each part does:

  • `python:3.12-slim` is a small Debian image with Python. The built agent image is about 516 MB, most of it Python packages.

  • `PROMPTISE_NO_DOTENV=1` turns off .env loading, so a stray file can never override the container's environment.

  • Dependencies before code. requirements.txt pins promptise==1.4.1, and Docker caches that layer, so changing agent.py rebuilds in seconds.

  • `COPY --chown=app:app` matters. promptise run refuses a manifest that is not owned by the user running it, because the manifest decides what runs. Without --chown, the files belong to root and the container exits with refusing to use this manifest: owned by uid 0, not by you (uid 10001).

  • `USER app` runs the agent without root, which limits what a compromised process can do: it can't install packages or change the Python installation.

  • `HEALTHCHECK` uses Python's standard library, because the slim image has no curl. It checks /readyz, so Docker reports the container healthy only once the agent is built.

  • Exec form `CMD [...]` makes promptise process 1. With the shell form (CMD promptise run ...), /bin/sh can end up as process 1, and it does not pass the SIGTERM from docker stop on to the agent.

The .dockerignore keeps secrets and local state out of the build context:

Textagent/.dockerignore
1
2
3
4
5
6
7
8
.env
.env.*
.venv/
__pycache__/
.promptise/
reports/
*.json
.git/

[07]

Step 3: Configure it with environment variables

Every setting the container needs arrives through the environment, and the process checks them before it starts. Run the image without them to see what happens:

>_Terminal
docker run --rm -e OPENAI_API_KEY support-bot-agent; echo "exit code: $?"
Output
{"ts": "2026-10-18T10:24:21.054+00:00", "level": "error", "logger": "promptise.run", "msg": "preflight failed", "problems": ["missing required environment variable(s): SHOP_MCP_URL, PROMPTISE_SERVE_TOKEN (set them, or add them to .env)"]}
exit code: 2

One JSON log line lists every missing variable at once, and the exit code is 2, so an orchestrator reports a configuration error instead of a crash loop with a stack trace. The serve token is required because the container listens on 0.0.0.0: promptise run refuses to serve on a network address without a bearer token.

In CI, check the image with the same command the container runs, plus --check:

>_Terminal
docker run --rm -e OPENAI_API_KEY -e SHOP_MCP_URL -e PROMPTISE_SERVE_TOKEN \
  support-bot-agent promptise run --prod --serve --check
Output
✓ support-bot: agent.py:create_agent (serve) is ready

--check validates the manifest, the environment and the factory's module, then exits with 0 or 2 without serving anything.

Where secrets come from

Pass secrets as environment variables from the platform's secret store: Kubernetes Secrets, AWS Secrets Manager through ECS task definitions, Docker Swarm secrets, or your CI's protected variables. Never use ENV OPENAI_API_KEY=... or ARG for secrets in a Dockerfile, because both end up in the image metadata. Generate the serve token with openssl rand -hex 32 and rotate it like any API key.


[08]

Step 4: Put the MCP server in its own container

The tool server is a separate service with its own image, so you can deploy, scale and restrict it on its own. It needs one change from the local version: it must listen on all interfaces inside its container, and it should only answer the host names it is reached by.

Pythonshop-mcp/server.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
"""A minimal MCP server for a small shop: two tools and one resource.

Run it over stdio (the default) or over Streamable HTTP:
    python server.py            # stdio: the client starts this process
    python server.py http       # HTTP on port 8000, path /mcp
"""
import os
import sys

from promptise.mcp.server import MCPServer

server = MCPServer(name="shop", version="1.0.0")

ORDERS = {
    "A1001": {"status": "delivered", "carrier": "UPS", "delivered_on": "2026-10-12"},
    "A1002": {"status": "in_transit", "carrier": "DHL", "eta": "2026-10-21"},
}
STOCK = {"TENT-2P": 4, "STOVE-MINI": 0}


@server.tool(read_only_hint=True)
async def get_order(order_id: str) -> dict:
    """Look up an order by its id (for example A1002): status, carrier and dates."""
    order = ORDERS.get(order_id.strip().upper())
    if order is None:
        return {"error": f"No order {order_id!r}. Order ids look like A1001."}
    return {"order_id": order_id.upper(), **order}


@server.tool(read_only_hint=True)
async def check_stock(sku: str) -> dict:
    """How many units of a product (by SKU, for example TENT-2P) are in stock."""
    units = STOCK.get(sku.strip().upper())
    if units is None:
        return {"error": f"Unknown SKU {sku!r}."}
    return {"sku": sku.upper(), "units": units}


@server.resource("shop://policies/returns", mime_type="text/markdown")
async def returns_policy() -> str:
    """The shop's return policy."""
    return "Returns are free within 30 days of delivery. Items must be unused."


if __name__ == "__main__":
    transport = sys.argv[1] if len(sys.argv) > 1 else "stdio"
    server.run(
        transport=transport,
        host=os.environ.get("MCP_HOST", "127.0.0.1"),
        port=8000,
        # Host headers this server answers: its compose service name and localhost.
        allowed_hosts=["shop-mcp:*", "localhost:*", "127.0.0.1:*"],
    )
Dockerfileshop-mcp/Dockerfile
1
2
3
4
5
6
7
8
9
10
11
12
FROM python:3.12-slim
ENV PYTHONUNBUFFERED=1 PIP_NO_CACHE_DIR=1 PIP_DISABLE_PIP_VERSION_CHECK=1
WORKDIR /app
RUN pip install "promptise==1.4.1"
COPY server.py .
RUN useradd --create-home --uid 10001 app
USER app
ENV MCP_HOST=0.0.0.0
EXPOSE 8000
HEALTHCHECK --interval=10s --timeout=3s --start-period=10s --retries=3 \
  CMD ["python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/health', timeout=2)"]
CMD ["python", "server.py", "http"]

MCP_HOST=0.0.0.0 is set in the image, so the same server.py still binds 127.0.0.1 when you run it on your laptop. The Promptise MCP server answers GET /health for probes. allowed_hosts turns on Host header validation for a public bind, which protects against DNS rebinding. This server has no authentication because it is only reachable on the compose network. If anything else can reach it, add API keys or OAuth as shown in MCP server authentication.


[09]

Step 5: Run both with docker compose

YAMLcompose.yaml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
services:
  shop-mcp:
    build: ./shop-mcp
    # No ports: the MCP server is reachable only on the compose network.
    restart: unless-stopped

  agent:
    build: ./agent
    ports:
      - "8080:8080"
    environment:
      OPENAI_API_KEY: ${OPENAI_API_KEY:?set OPENAI_API_KEY}
      PROMPTISE_SERVE_TOKEN: ${PROMPTISE_SERVE_TOKEN:?set PROMPTISE_SERVE_TOKEN}
      SHOP_MCP_URL: http://shop-mcp:8000/mcp
    depends_on:
      shop-mcp:
        condition: service_healthy   # the agent connects to MCP when it starts
    stop_grace_period: 40s           # longer than [prod] shutdown_timeout
    restart: unless-stopped
  • No `ports` on `shop-mcp`. Only containers on the compose network can reach the tools.

  • `${OPENAI_API_KEY:?...}` takes the value from your shell and stops compose with a clear message when it is missing.

  • `depends_on` with `service_healthy` waits for the MCP server's health check, because the agent connects to it while it starts. Without it, the agent can start first and fail to connect.

  • `stop_grace_period: 40s` gives the agent more time than its 30-second drain before Docker sends SIGKILL. Docker's default is 10 seconds, which is shorter than many agent requests.

>_Terminal
export OPENAI_API_KEY=sk-...
export PROMPTISE_SERVE_TOKEN="$(openssl rand -hex 32)"
docker compose up -d --build
docker compose ps
Output
SERVICE    STATUS                    PORTS
agent      Up 20 seconds (healthy)   0.0.0.0:8080->8080/tcp
shop-mcp   Up 26 seconds (healthy)   8000/tcp

[10]

Step 6: Check health, readiness and the API

>_Terminal
curl -s localhost:8080/healthz
curl -s localhost:8080/readyz
curl -s -o /dev/null -w "%{http_code}\n" localhost:8080/v1/info
Output
$ curl -s localhost:8080/healthz
{"status":"ok"}
$ curl -s localhost:8080/readyz
{"status":"ready"}
$ curl -s -o /dev/null -w "%{http_code}\n" localhost:8080/v1/info
401

The probes need no token, so load balancers and Kubernetes can call them. Everything under /v1/ needs the bearer token, so /v1/info without it returns 401. Now ask the agent a question:

>_Terminal
curl -s localhost:8080/v1/invoke \
  -H "Authorization: Bearer $PROMPTISE_SERVE_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"input": "Where is my order A1002?"}'
Output
{
    "output": "Your order A1002 is in transit with DHL. Estimated delivery: 2026-10-21.\n\nWould you like me to fetch the tracking link/number or do anything else with the shipment?",
    "duration_ms": 4081.4,
    "usage": {
        "llm_calls": 2,
        "prompt_tokens": 468,
        "completion_tokens": 332,
        "total_tokens": 800
    },
    "request_id": "c4038d5aa611418499336fb592e1baa9"
}

The agent called get_order on the MCP container and answered. usage counts the model calls and tokens of this request, which is what per-user cost tracking needs (see serve mode for every field). request_id also appears in the X-Request-ID response header and in every log line of the request, so you can find a bad answer in the logs.


[11]

Step 7: Shut down without dropping requests

Start a request, and stop the container while it runs:

>_Terminal
curl -s localhost:8080/v1/invoke -H "Authorization: Bearer $PROMPTISE_SERVE_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"input": "Is TENT-2P in stock, and what about order A1001?"}' &
sleep 1.5; docker compose stop agent; wait
Output
{"output":"TENT-2P — 4 units in stock.\nOrder A1001 — delivered via UPS on 2026-10-12.\n\nAnything else I can help with?","duration_ms":5166.1,"usage":{"llm_calls":2,"prompt_tokens":540,"completion_tokens":297,"total_tokens":837},"request_id":"0501a40d6760458d8b28878d3545a108"}exit=0

The request that was running when SIGTERM arrived still got its full answer. The JSON logs show the order of events:

>_Terminal
docker compose logs --no-log-prefix agent | jq -c 'select(.msg == "Received SIGTERM: draining"
  or (.msg == "http" and .path == "/v1/invoke") or .msg == "run.exit" or .msg == "stats")
  | {msg, path, status, duration_ms, code, cost_usd: .totals.cost_usd, total_tokens: .totals.total_tokens}
  | del(..|nulls)'
Output
{"msg":"http","path":"/v1/invoke","status":200,"duration_ms":4082.0}
{"msg":"Received SIGTERM: draining"}
{"msg":"http","path":"/v1/invoke","status":200,"duration_ms":5167.0}
{"msg":"run.exit","code":0}
{"msg":"stats","cost_usd":0.00151,"total_tokens":1637}

On SIGTERM the server starts draining: /readyz and new /v1/* requests answer 503, so the load balancer stops sending traffic, and running requests get up to [prod] shutdown_timeout seconds. Then the agent closes its MCP sessions and exits with code 0. The last line carries the process's totals, including the estimated cost from [pricing].


[12]

The complete example

All files are shown above: agent/agent.py, agent/promptise.toml, agent/Dockerfile, agent/.dockerignore, agent/requirements.txt (one line, promptise==1.4.1), shop-mcp/server.py (the two-tool shop server, with the run() call from Step 4), shop-mcp/Dockerfile and compose.yaml. To run the stack:

>_Terminal
cd support-bot
export OPENAI_API_KEY=sk-...
export PROMPTISE_SERVE_TOKEN="$(openssl rand -hex 32)"
docker compose up -d --build
curl -s localhost:8080/v1/invoke -H "Authorization: Bearer $PROMPTISE_SERVE_TOKEN" \
  -H "Content-Type: application/json" -d '{"input": "Where is my order A1002?"}'
docker compose down

[13]

Running it in production

Probes and shutdown on Kubernetes

The same image runs on Kubernetes. Map the endpoints to probes and keep the grace period longer than the drain:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
spec:
  terminationGracePeriodSeconds: 40
  containers:
    - name: agent
      image: registry.example.com/support-bot-agent:1.0.0
      ports: [{containerPort: 8080}]
      env:
        - name: SHOP_MCP_URL
          value: http://shop-mcp:8000/mcp
        - name: OPENAI_API_KEY
          valueFrom: {secretKeyRef: {name: support-bot, key: openai-api-key}}
        - name: PROMPTISE_SERVE_TOKEN
          valueFrom: {secretKeyRef: {name: support-bot, key: serve-token}}
      livenessProbe:
        httpGet: {path: /healthz, port: 8080}
        periodSeconds: 10
      readinessProbe:
        httpGet: {path: /readyz, port: 8080}
        periodSeconds: 5

Don't point the liveness probe at /readyz: a pod that is draining would be killed instead of finishing its requests.

Scaling

One promptise run --serve process serves one agent and runs up to max_concurrency requests at the same time. Most of a request's time is spent waiting on the model API, so a process handles several requests at once with little CPU. Scale out by adding replicas behind a load balancer, and scale on concurrent requests or latency rather than CPU. Two rules keep replicas interchangeable. Keep conversations in a shared store (RedisConversationStore or PostgresConversationStore with a session_id per conversation), not in memory. And remember that your model provider's rate limit is shared by every replica, so more replicas can mean more 429s from the provider.

MCP servers over Streamable HTTP keep a session per client connection. When an MCP container restarts, the server answers old sessions with 404, and Promptise clients open a new session and retry the call once. For MCP servers that run several replicas, use sticky sessions at the load balancer, or check that your server handles requests without per-session state.

Logs, metrics and traces

--prod writes one JSON object per line on stderr, which Docker's logging drivers, Loki, Datadog and CloudWatch ingest as is. Raise [prod] observe to standard to log every model call and tool call, or use the OpenTelemetry transporter to send each request as a trace. AI agent observability covers what to record and what to keep out of logs.

Image hygiene

Pin the base image and your dependencies, and rebuild regularly for security patches. Scan images in CI (docker scout, Trivy or Grype). Run as a non-root user, as here, and set the root filesystem read-only where your platform allows it. Agents that execute generated code need a separate sandbox, not just a container: see AI agent code execution sandbox.

Put a gateway in front

The serve mode has one bearer token for all callers. It is meant for one application calling the agent, not for end users. Put TLS, user authentication and per-user rate limits in your API gateway or web app, and pass the user's identity on to the agent. To give each user their own identity and permissions inside the agent, embed build_agent() in your own web server instead.


[14]

Common problems and fixes

Symptom

Cause

Fix

---|---|---

refusing to use this manifest: owned by uid 0, not by you

The files were copied as root and the container runs as another user

COPY --chown=app:app promptise.toml agent.py ./

preflight failed ... missing required environment variable(s) and exit code 2

A variable in [env] required is not set in the container

Pass it with -e, environment: or a secret reference

refusing to serve on 0.0.0.0 without an auth token

Network bind without PROMPTISE_SERVE_TOKEN

Set the token from your secret store

Failed to connect to server 'shop' at start

The agent started before the MCP server was ready

depends_on with condition: service_healthy, and a health check on the MCP server

The MCP server answers 421

The Host header isn't in allowed_hosts

Add the service name, for example "shop-mcp:*"

docker stop takes 10 seconds and requests fail

Shell-form CMD, so SIGTERM doesn't reach the agent and Docker kills it after the timeout

Use exec form CMD ["promptise", "run", ...]

Long requests are cut off on deploy

The grace period is shorter than the drain or the request

Raise stop_grace_period / terminationGracePeriodSeconds above [prod] shutdown_timeout

The container uses an old or wrong key

A .env file was copied into the image

.dockerignore with .env, and PROMPTISE_NO_DOTENV=1

Many 429 busy responses

More concurrent requests than [serve] max_concurrency

Add replicas, or raise the limit if the provider's rate limit allows it


[15]

Limitations

promptise run --serve runs one agent in one process with one bearer token. It has no per-user accounts, TLS or routing, which stay with your gateway and platform. Conversations are only kept across requests when the agent has a conversation store. The image in this guide is a plain single-stage build. A multi-stage build or a distroless base can make it smaller, at the cost of a less convenient debugging shell. Kubernetes manifests are shown as a starting point, not as a tested chart.


[16]

Frequently asked questions

How do I run an AI agent in a Docker container?

Install your agent framework in a slim Python image, copy the agent code, and start it as an HTTP service in exec form. With Promptise Foundry, the command is promptise run --prod --serve --host 0.0.0.0, which serves the agent factory named in promptise.toml with health checks, an invoke endpoint and JSON logs.

How should I pass the API key to a Dockerized agent?

As an environment variable set when the container starts, from your platform's secret store. Never write it into the Dockerfile with ENV or ARG, and keep .env files out of the image with .dockerignore. Promptise reads provider keys such as OPENAI_API_KEY from the environment.

What health checks does an AI agent container need?

Two. A liveness check that only confirms the process responds (/healthz), and a readiness check that confirms the agent is built and its tool servers are connected (/readyz). Route traffic on readiness and restart on liveness, so a draining or starting container is not killed.

How do I stop an agent container without losing requests?

Make the agent process 1 with an exec-form CMD so it receives SIGTERM, let it stop accepting new requests and finish running ones, and give it a grace period longer than your slowest request. promptise run --prod --serve drains for [prod] shutdown_timeout seconds and exits with code 0.

Should the MCP server run in the same container as the agent?

Usually not. A separate container lets you deploy, restrict and scale the tools on their own and share them between agents. A stdio MCP server, which the agent starts as a child process, can live in the agent's image when only that agent uses it.

How do I scale an AI agent with Docker?

Run more replicas behind a load balancer. Each replica is stateless if conversations live in a shared store such as Redis or Postgres. Scale on concurrent requests or latency, since agents mostly wait on the model API, and watch the provider's rate limit, which all replicas share.


[17]

Next steps

  • Run, debug and serve the same project locally: Run, debug and deploy an AI agent.

  • Agents that run on their own with triggers and budgets: Deploy AI agents in production.

  • Harden the tool server: Build a production MCP server in Python.

  • Survive crashes mid-run: Durable AI agents and crash recovery.

  • Reference: promptise run serve mode and Docker and MCP server deployment.

← PreviousContext Engineering for AI Agents: Context Window ManagementNext lesson →LLM Tool Rate Limiting: Per-Client Limits for MCP Servers