DEV Community

Cover image for Dockerize Your Agent in 5 Minutes
Gantz AI for Gantz

Posted on

Dockerize Your Agent in 5 Minutes

The Minimal Docker Setup for Agents

You built an agent. It works on your machine.

Now you need to run it somewhere else.

Docker is the answer. Here's the simplest setup that works.

The goal

your-agent/
├── Dockerfile
├── docker-compose.yml
├── agent.py
├── requirements.txt
└── .env
Enter fullscreen mode Exit fullscreen mode

Five files. Runs anywhere.

The Dockerfile

# Dockerfile
FROM python:3.11-slim

WORKDIR /app

# Install dependencies first (better caching)
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Copy code
COPY . .

# Don't run as root
RUN useradd -m agent
USER agent

CMD ["python", "agent.py"]
Enter fullscreen mode Exit fullscreen mode

That's it. No multi-stage builds. No optimization tricks. Just works.

Why these choices?

FROM python:3.11-slim  # slim = smaller image, has what you need
WORKDIR /app           # consistent working directory
USER agent             # security: don't run as root
Enter fullscreen mode Exit fullscreen mode

The requirements

# requirements.txt
openai>=1.0.0
anthropic>=0.18.0
redis>=4.0.0
Enter fullscreen mode Exit fullscreen mode

Pin major versions. Don't over-specify.

The agent

# agent.py
import os
import openai

client = openai.OpenAI()  # Uses OPENAI_API_KEY from env

tools = [
    {
        "type": "function",
        "function": {
            "name": "run_command",
            "description": "Run a shell command",
            "parameters": {
                "type": "object",
                "properties": {
                    "command": {"type": "string"}
                },
                "required": ["command"]
            }
        }
    }
]

def execute_tool(name, args):
    if name == "run_command":
        import subprocess
        result = subprocess.run(
            args["command"],
            shell=True,
            capture_output=True,
            text=True,
            timeout=30
        )
        return result.stdout + result.stderr
    return "Unknown tool"

def run_agent(message):
    messages = [{"role": "user", "content": message}]

    while True:
        response = client.chat.completions.create(
            model=os.getenv("MODEL", "gpt-4o"),
            messages=messages,
            tools=tools
        )

        msg = response.choices[0].message
        messages.append(msg)

        if not msg.tool_calls:
            return msg.content

        for tc in msg.tool_calls:
            import json
            result = execute_tool(tc.function.name, json.loads(tc.function.arguments))
            messages.append({
                "role": "tool",
                "tool_call_id": tc.id,
                "content": result
            })

if __name__ == "__main__":
    print(run_agent("What files are in the current directory?"))
Enter fullscreen mode Exit fullscreen mode

The environment

# .env
OPENAI_API_KEY=sk-...
MODEL=gpt-4o
Enter fullscreen mode Exit fullscreen mode

Never commit this file:

# .gitignore
.env
Enter fullscreen mode Exit fullscreen mode

Build and run

# Build
docker build -t my-agent .

# Run with env file
docker run --env-file .env my-agent

# Run with inline env
docker run -e OPENAI_API_KEY=sk-... my-agent
Enter fullscreen mode Exit fullscreen mode

Done. Your agent runs in a container.

Adding docker-compose

For real projects, use compose:

# docker-compose.yml
version: '3.8'

services:
  agent:
    build: .
    env_file:
      - .env
    volumes:
      - ./workspace:/app/workspace  # Persist work
    restart: unless-stopped
Enter fullscreen mode Exit fullscreen mode
# Run
docker-compose up

# Run in background
docker-compose up -d

# Rebuild after changes
docker-compose up --build
Enter fullscreen mode Exit fullscreen mode

Adding Redis for state

Most agents need state storage:

# docker-compose.yml
version: '3.8'

services:
  agent:
    build: .
    env_file:
      - .env
    environment:
      - REDIS_URL=redis://redis:6379
    depends_on:
      - redis
    restart: unless-stopped

  redis:
    image: redis:alpine
    volumes:
      - redis_data:/data
    restart: unless-stopped

volumes:
  redis_data:
Enter fullscreen mode Exit fullscreen mode

Update your agent:

# agent.py
import os
import redis
import json

redis_client = redis.from_url(os.getenv("REDIS_URL", "redis://localhost:6379"))

def save_conversation(session_id, messages):
    redis_client.setex(f"conv:{session_id}", 3600, json.dumps(messages))

def load_conversation(session_id):
    data = redis_client.get(f"conv:{session_id}")
    return json.loads(data) if data else []
Enter fullscreen mode Exit fullscreen mode

Adding an API

Wrap your agent in an HTTP API:

# api.py
from fastapi import FastAPI
from pydantic import BaseModel
import uvicorn

app = FastAPI()

class ChatRequest(BaseModel):
    message: str
    session_id: str = "default"

class ChatResponse(BaseModel):
    response: str

@app.post("/chat", response_model=ChatResponse)
async def chat(request: ChatRequest):
    response = run_agent(request.message, request.session_id)
    return ChatResponse(response=response)

@app.get("/health")
async def health():
    return {"status": "ok"}

if __name__ == "__main__":
    uvicorn.run(app, host="0.0.0.0", port=8000)
Enter fullscreen mode Exit fullscreen mode

Update Dockerfile:

FROM python:3.11-slim

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

RUN useradd -m agent
USER agent

EXPOSE 8000

CMD ["python", "api.py"]
Enter fullscreen mode Exit fullscreen mode

Update requirements:

# requirements.txt
openai>=1.0.0
redis>=4.0.0
fastapi>=0.100.0
uvicorn>=0.22.0
pydantic>=2.0.0
Enter fullscreen mode Exit fullscreen mode

Update compose:

# docker-compose.yml
version: '3.8'

services:
  agent:
    build: .
    env_file:
      - .env
    ports:
      - "8000:8000"
    environment:
      - REDIS_URL=redis://redis:6379
    depends_on:
      - redis
    restart: unless-stopped

  redis:
    image: redis:alpine
    volumes:
      - redis_data:/data
    restart: unless-stopped

volumes:
  redis_data:
Enter fullscreen mode Exit fullscreen mode

Now you have an API:

curl -X POST http://localhost:8000/chat \
  -H "Content-Type: application/json" \
  -d '{"message": "Hello", "session_id": "user123"}'
Enter fullscreen mode Exit fullscreen mode

Adding a workspace

Agents need to read/write files. Give them a workspace:

# docker-compose.yml
services:
  agent:
    build: .
    env_file:
      - .env
    volumes:
      - ./workspace:/app/workspace  # Mount local folder
      - /tmp/agent-work:/tmp/work   # Temp space
    working_dir: /app/workspace
Enter fullscreen mode Exit fullscreen mode

In your agent:

import os

# All file operations happen in workspace
WORKSPACE = os.getenv("WORKSPACE", "/app/workspace")

def read_file(path):
    # Prevent path traversal
    safe_path = os.path.normpath(os.path.join(WORKSPACE, path))
    if not safe_path.startswith(WORKSPACE):
        return "Error: Access denied"

    with open(safe_path) as f:
        return f.read()
Enter fullscreen mode Exit fullscreen mode

Production hardening

Resource limits

services:
  agent:
    build: .
    deploy:
      resources:
        limits:
          cpus: '1'
          memory: 512M
        reservations:
          cpus: '0.25'
          memory: 128M
Enter fullscreen mode Exit fullscreen mode

Health checks

services:
  agent:
    build: .
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 10s
Enter fullscreen mode Exit fullscreen mode

Logging

services:
  agent:
    build: .
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"
Enter fullscreen mode Exit fullscreen mode

Secrets (not env files)

services:
  agent:
    build: .
    secrets:
      - openai_key

secrets:
  openai_key:
    file: ./secrets/openai_key.txt
Enter fullscreen mode Exit fullscreen mode

In your code:

def get_secret(name):
    try:
        with open(f"/run/secrets/{name}") as f:
            return f.read().strip()
    except FileNotFoundError:
        return os.getenv(name.upper())

openai_key = get_secret("openai_key")
Enter fullscreen mode Exit fullscreen mode

The complete setup

your-agent/
├── Dockerfile
├── docker-compose.yml
├── api.py
├── agent.py
├── requirements.txt
├── .env
├── .gitignore
└── workspace/
    └── (user files here)
Enter fullscreen mode Exit fullscreen mode

Final docker-compose.yml

version: '3.8'

services:
  agent:
    build: .
    env_file:
      - .env
    ports:
      - "8000:8000"
    environment:
      - REDIS_URL=redis://redis:6379
      - WORKSPACE=/app/workspace
    volumes:
      - ./workspace:/app/workspace
    depends_on:
      redis:
        condition: service_healthy
    deploy:
      resources:
        limits:
          cpus: '1'
          memory: 512M
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
      interval: 30s
      timeout: 10s
      retries: 3
    restart: unless-stopped

  redis:
    image: redis:alpine
    volumes:
      - redis_data:/data
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 5s
      retries: 3
    restart: unless-stopped

volumes:
  redis_data:
Enter fullscreen mode Exit fullscreen mode

Final Dockerfile

FROM python:3.11-slim

WORKDIR /app

# Install curl for healthcheck
RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

RUN useradd -m agent && chown -R agent:agent /app
USER agent

EXPOSE 8000

CMD ["python", "api.py"]
Enter fullscreen mode Exit fullscreen mode

Running with Gantz

Gantz Run simplifies the tool layer:

# docker-compose.yml
services:
  gantz:
    image: gantz/gantz
    env_file:
      - .env
    ports:
      - "8000:8000"
    volumes:
      - ./gantz.yaml:/app/gantz.yaml
      - ./workspace:/app/workspace
    restart: unless-stopped
Enter fullscreen mode Exit fullscreen mode
# gantz.yaml
tools:
  - name: read
    description: Read a file
    parameters:
      - name: path
        type: string
        required: true
    script:
      shell: cat "{{path}}"

  - name: write
    description: Write to a file
    parameters:
      - name: path
        type: string
        required: true
      - name: content
        type: string
        required: true
    script:
      shell: echo "{{content}}" > "{{path}}"

  - name: run
    description: Run a command
    parameters:
      - name: command
        type: string
        required: true
    script:
      shell: "{{command}}"
Enter fullscreen mode Exit fullscreen mode

One config file. Ready to deploy.

Common commands

# Start everything
docker-compose up -d

# View logs
docker-compose logs -f agent

# Restart after code changes
docker-compose up -d --build

# Stop everything
docker-compose down

# Stop and remove volumes (fresh start)
docker-compose down -v

# Shell into container
docker-compose exec agent bash

# Check resource usage
docker stats
Enter fullscreen mode Exit fullscreen mode

Summary

Minimal Docker setup for agents:

Component Purpose
Dockerfile Build your agent image
docker-compose.yml Orchestrate services
.env Store secrets (don't commit)
volumes Persist data and workspace
healthcheck Know when things break
resource limits Don't eat all the RAM

Start minimal. Add complexity when you need it.

docker-compose up -d
Enter fullscreen mode Exit fullscreen mode

Your agent is now portable.


What's your Docker setup for agents? Any tricks I missed?

Top comments (0)