Browse the docs

Build the Docker image

The generated Dockerfile, the skill-specific prep patterns, the non-root user, and the port convention.

Before you startA generated agent directory and Docker.View as Markdown

The Dockerfile builds from the repository root, so the build context can reach both the agent directory and the source skill. Here it is filled in for a skill that writes reports to /outputs:

Dockerfile
# Report Generator Agent: Claude Agent SDK + report-generator skill
# Build from the repo root:
#   docker build -f azurefoundry_report_generator_skills/Dockerfile -t report-generator-skills-agent .
FROM python:3.12-slim

WORKDIR /app

# No Node.js: claude-agent-sdk>=0.2.138 bundles the Claude Code CLI inside the wheel.
RUN apt-get update \
    && apt-get install -y --no-install-recommends bash curl ca-certificates \
    && rm -rf /var/lib/apt/lists/*

# Skill-specific prep ({{DOCKERFILE_SKILL_PREP_BLOCK}}): a writable outputs dir
RUN mkdir -p /outputs && chmod 777 /outputs

RUN pip install --no-cache-dir uv

# Python deps first, for layer caching
COPY azurefoundry_report_generator_skills/pyproject.toml azurefoundry_report_generator_skills/uv.lock* ./
RUN uv pip install --system -e .

# The source skill, into the SDK discovery path
COPY skills/report-generator /app/.claude/skills/report-generator

# Agent code
COPY azurefoundry_report_generator_skills/ /app/

# Non-root user ({{NONROOT_EXTRA_DIRS}} = /outputs)
RUN useradd --create-home --shell /bin/bash --uid 1000 claude \
    && chown -R claude:claude /app /home/claude /outputs
USER claude

ENV HOME=/home/claude
ENV AGENT_PORT=9024
ENV AGENT_HOST=0.0.0.0
ENV REPORT_OUTPUTS_DIR=/outputs
ENV CLAUDE_AGENT_CWD=/app
ENV CLAUDE_AGENT_MODEL=claude-sonnet-4-6

EXPOSE ${AGENT_PORT}

CMD ["sh", "-c", "uv run . --host ${AGENT_HOST} --port ${AGENT_PORT}"]
  • pyproject.toml is what the image installs from. It never reads requirements.txt, so the claude-agent-sdk>=0.2.138 floor that matters lives in pyproject.toml. Keep the two files in step.
  • Do not lower the SDK floor. Releases below 0.2.138 need Node.js, which the image no longer installs.
  • The skill is copied unchanged to /app/.claude/skills/<skill-name>/, where setting_sources=["project"] finds it.

Skill prep patterns

Replace {{DOCKERFILE_SKILL_PREP_BLOCK}} with whichever of these the skill needs. Concatenate several if needed.

Hardcoded session paths

Some skills written in a Claude Cowork session hardcode a session-specific path such as /sessions/<session-id>/mnt/outputs/... in their scripts. Create the exact path; do not try to detect it dynamically.

dockerfile
RUN mkdir -p /sessions/<exact-id-from-skill>/mnt/outputs/<rest-of-path> \
    && chmod -R 777 /sessions

A CLI binary the skill calls

dockerfile
ARG TOOL_VERSION=1.2.3
RUN curl -fsSL "https://github.com/<org>/<repo>/releases/download/v${TOOL_VERSION}/<asset>.tar.gz" \
      -o /tmp/tool.tgz \
    && tar -xzf /tmp/tool.tgz -C /tmp \
    && mv /tmp/<bin-name> /usr/local/bin/<bin-name> \
    && chmod +x /usr/local/bin/<bin-name> \
    && rm /tmp/tool.tgz

If the skill's bootstrap looks for the binary at a specific path, put it exactly there instead of on PATH.

A writable outputs directory

dockerfile
RUN mkdir -p /outputs && chmod 777 /outputs
ENV REPORT_OUTPUTS_DIR=/outputs

Mount a file share at /outputs at deploy time so files survive container restarts.

A token or credentials directory

dockerfile
ENV REPORT_DIR=/mnt/report

Set only the env var. Do not create the directory in the image: a file share is mounted there at deploy time. Document the share setup in the README.

System packages

dockerfile
RUN apt-get update \
    && apt-get install -y --no-install-recommends \
        bash curl ca-certificates \
        jq pandoc \
    && rm -rf /var/lib/apt/lists/*

Extend the existing apt-get install step instead of adding a second one, to keep layers small. Do not add Node.js back.

Python deps for skill scripts

Add what the skill's scripts import to {{SKILL_EXTRA_DEPS}} in pyproject.toml (for example "feedparser>=6.0.10") and mirror it in requirements.txt. No Dockerfile change is needed.

The non-root user

The template always runs as user claude (uid 1000). Add every extra directory the skill writes to {{NONROOT_EXTRA_DIRS}}, for example /sessions /outputs or /sessions /mnt/<name> /outputs. If you skip the chown, the agent starts but fails with Permission denied when the skill writes.

Staying non-root also protects the permission model. The bundled CLI refuses --dangerously-skip-permissions, which permission_mode="bypassPermissions" translates to, when it runs as root. An agent that switched to bypassPermissions and ran as root would start and pass /health, then fail every run with --dangerously-skip-permissions cannot be used with root/sudo privileges for security reasons.

What stays out of the image

  • Secrets. Never ENV ANTHROPIC_API_KEY=... and never COPY .env .. Credentials come at runtime from Container Apps secrets or --env-file.
  • `tests/`. .dockerignore excludes it. Keep that line.
  • Local edits to the skill. Copy the skill as it lives in the repo. Patch the filesystem or the env, not the skill.
.dockerignore
__pycache__/
*.pyc
*.pyo
*.pyd
.pytest_cache/
.venv/
venv/
.env
*.log
.git/
.gitignore
README.md
tests/

Ports

SurfacePort
Plain HTTPThe lowest unused port at or above 9024, recorded in a port list kept with your agents. 9000–9023 are reserved for the orchestrator, websocket server, and frontend dev server; 9100 and up for non-A2A internal services.
AMC variant8080, with no port list entry needed.

Never invent a port for a plain-HTTP agent: check the list first and add the new agent to it in the same change.

Last updated 2026-10-21