EzAI
Back to Blog
Tutorial Mar 29, 2026 8 min read

Build an AI Docker Compose Generator with Python

E

EzAI Team

Build an AI Docker Compose Generator with Python

Writing docker-compose.yml files by hand is tedious. You need to figure out which services your project requires, configure health checks, set up volumes, map ports, and wire networks together. Miss one dependency and your stack fails silently at 2 AM.

This tutorial builds a Python CLI tool that scans your project directory — reading package.json, requirements.txt, Dockerfiles, Prisma schemas, and source code — then sends that context to Claude via EzAI to generate a production-ready docker-compose.yml. The output includes health checks, named volumes, restart policies, and environment variable placeholders. Under 120 lines of Python.

How It Works

The generator follows a three-phase pipeline. First, it walks your project tree and collects signals: dependency files reveal which databases and caches you need, Dockerfiles show existing container configs, and source imports hint at service requirements. Second, it packs everything into a structured prompt and sends it to Claude. Third, it extracts the YAML response and writes it to disk.

AI Docker Compose Generator workflow: project files to Claude analysis to docker-compose.yml output

Project files → Claude analysis via EzAI → production-ready docker-compose.yml

Claude is particularly good at this because it can reason about the relationships between your services. It sees prisma in your dependencies and knows you need Postgres. It spots bullmq and adds Redis. It reads your .env.example and maps those variables into the compose file. That kind of cross-file reasoning is where LLMs outperform template-based generators.

Prerequisites

  • Python 3.9+ with httpx installed (pip install httpx)
  • An EzAI API key — sign up takes 30 seconds, and new accounts get 15 free credits
  • A project directory you want to containerize

Project Structure

text
compose-gen/
├── generate.py      # Main script — project scanner + API caller
└── README.md

One file. That's it. The entire tool is a single Python script that you can drop into any project.

Core Generator Code

Here's the full implementation. It scans the project, builds context, calls Claude, and writes the output:

python
import httpx, json, sys, os
from pathlib import Path

EZAI_KEY = os.environ["ANTHROPIC_API_KEY"]
EZAI_URL = "https://ezaiapi.com/v1/messages"

# Files that reveal project dependencies and structure
SIGNAL_FILES = [
    "package.json", "requirements.txt", "pyproject.toml",
    "Dockerfile", "Dockerfile.dev", ".env.example",
    "prisma/schema.prisma", "docker-compose.yml",
    "Gemfile", "go.mod", "Cargo.toml",
]

def scan_project(root: Path) -> dict:
    """Read signal files and collect directory tree."""
    context = {"files": {}, "tree": []}

    for rel in SIGNAL_FILES:
        path = root / rel
        if path.exists():
            text = path.read_text(errors="ignore")[:4000]
            context["files"][rel] = text

    # Shallow directory listing (depth 2)
    for entry in sorted(root.rglob("*")):
        rel = entry.relative_to(root)
        if len(rel.parts) <= 2 and not any(
            p.startswith(".") or p == "node_modules"
            for p in rel.parts
        ):
            context["tree"].append(str(rel))

    return context

The scanner reads up to 4KB per file — enough context for Claude to understand dependencies without blowing up the token count. The directory tree is capped at depth 2 to give structural context without noise.

Building the Prompt

The prompt is the critical piece. Vague instructions produce vague YAML. Specific instructions produce deployable configs:

python
SYSTEM = """You are a Docker infrastructure expert.
Given project files, generate a production-ready docker-compose.yml.

Rules:
- Use compose spec version 3.8+
- Pin image tags (postgres:16-alpine, not postgres:latest)
- Add healthchecks for every service
- Use named volumes for persistent data
- Set restart: unless-stopped on all services
- Add depends_on with condition: service_healthy
- Include .env variable references with sensible defaults
- Add comments explaining non-obvious choices
- Output ONLY the YAML inside ```yaml fences, nothing else"""

def build_prompt(context: dict) -> str:
    parts = ["Analyze this project and generate docker-compose.yml:\n"]

    parts.append("## Directory Tree")
    parts.append("\n".join(context["tree"][:50]))

    for name, content in context["files"].items():
        parts.append(f"\n## {name}\n```\n{content}\n```")

    return "\n".join(parts)

The system prompt enforces production standards — pinned versions, health checks, named volumes. Without these constraints, Claude tends to generate minimal configs that work in dev but fall apart in production. The explicit rules fix that.

Calling Claude via EzAI

The API call itself is straightforward. We use claude-sonnet-4-5 for the right balance of speed and quality — Opus would be overkill for YAML generation, and Haiku sometimes misses dependency relationships:

python
import re

def generate_compose(context: dict) -> str:
    """Send project context to Claude and extract YAML."""
    resp = httpx.post(
        EZAI_URL,
        headers={
            "x-api-key": EZAI_KEY,
            "anthropic-version": "2023-06-01",
            "content-type": "application/json",
        },
        json={
            "model": "claude-sonnet-4-5",
            "max_tokens": 4096,
            "system": SYSTEM,
            "messages": [{"role": "user", "content": build_prompt(context)}],
        },
        timeout=60,
    )
    resp.raise_for_status()
    text = resp.json()["content"][0]["text"]

    # Extract YAML from markdown fences
    match = re.search(r"```ya?ml\n(.*?)```", text, re.DOTALL)
    if not match:
        raise ValueError("No YAML block found in response")
    return match.group(1).strip()

The regex extraction is important. Claude wraps its YAML in markdown fences, and you want the raw content without the fence markers. If the model returns prose instead of YAML (rare with a good system prompt), the ValueError gives you a clear signal to retry.

CLI Entry Point

Wire it all together with a simple main that takes a project path and writes the output:

python
def main():
    root = Path(sys.argv[1]) if len(sys.argv) > 1 else Path(".")
    out = root / "docker-compose.yml"

    print(f"🔍 Scanning {root.resolve()}...")
    context = scan_project(root)
    print(f"   Found {len(context['files'])} config files")

    print("🤖 Generating docker-compose.yml with Claude...")
    yaml_content = generate_compose(context)

    out.write_text(yaml_content)
    print(f"✅ Written to {out}")
    print(f"   Run: docker compose up -d")

if __name__ == "__main__":
    main()

Run it against any project:

bash
export ANTHROPIC_API_KEY="sk-your-ezai-key"
python3 generate.py /path/to/your/project

# Output:
# 🔍 Scanning /path/to/your/project...
#    Found 4 config files
# 🤖 Generating docker-compose.yml with Claude...
# ✅ Written to /path/to/your/project/docker-compose.yml
#    Run: docker compose up -d

Example Output

Point it at a typical Node.js + Prisma + Redis project and you get something like this:

yaml
version: "3.8"

services:
  app:
    build:
      context: .
      dockerfile: Dockerfile
    ports:
      - "${APP_PORT:-3000}:3000"
    environment:
      - DATABASE_URL=postgresql://${POSTGRES_USER:-app}:${POSTGRES_PASSWORD:-secret}@postgres:5432/${POSTGRES_DB:-appdb}
      - REDIS_URL=redis://redis:6379
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
    restart: unless-stopped

  postgres:
    image: postgres:16-alpine
    environment:
      - POSTGRES_USER=${POSTGRES_USER:-app}
      - POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-secret}
      - POSTGRES_DB=${POSTGRES_DB:-appdb}
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-app}"]
      interval: 5s
      timeout: 3s
      retries: 5
    restart: unless-stopped

  redis:
    image: redis:7-alpine
    command: redis-server --maxmemory 256mb --maxmemory-policy allkeys-lru
    volumes:
      - redisdata:/data
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 5s
      timeout: 3s
      retries: 3
    restart: unless-stopped

volumes:
  pgdata:
  redisdata:

Claude inferred Postgres from the Prisma schema, Redis from the BullMQ dependency, wired the DATABASE_URL from the .env.example pattern, and added health checks with appropriate readiness probes for each service. That's the kind of reasoning a static template can't do.

Cost Breakdown

A typical project scan produces 2,000-3,000 input tokens (file contents + tree) and 800-1,200 output tokens (the YAML). With claude-sonnet-4-5 through EzAI:

  • Input: ~3K tokens × $3/M = $0.009
  • Output: ~1K tokens × $15/M = $0.015
  • Total per generation: ~$0.024

Under three cents per compose file. For comparison, the time you'd spend manually writing and debugging a multi-service compose config is 15-30 minutes. At any engineering hourly rate, the AI approach pays for itself thousands of times over. Check the EzAI pricing page for current model rates.

Extending the Generator

The base version handles common stacks well. Here are three upgrades worth adding for production use:

Multi-environment support. Add a --env flag that generates different configs for dev (with hot reload volumes and debug ports) versus production (with resource limits and logging drivers). Pass the flag value in the user prompt so Claude adapts the output.

Validation pass. After generating the YAML, parse it with PyYAML and run basic checks: do all depends_on targets exist as services? Are all referenced volumes declared? Do port mappings conflict? A 10-line validation function catches the rare hallucination before you hit docker compose up.

Incremental updates. If a docker-compose.yml already exists, read it as additional context and ask Claude to merge new services rather than regenerate from scratch. This preserves manual tweaks while adding new dependencies.

What's Next

You now have a working AI-powered Docker Compose generator in under 120 lines. The pattern — scan files, build context, call Claude, extract structured output — applies to any config generation task: Kubernetes manifests, CI pipelines, nginx configs, Terraform modules.


Related Posts