Build the Docker image
The generated Dockerfile, the skill-specific prep patterns, the non-root user, and the port convention.
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:
# 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.tomlis what the image installs from. It never readsrequirements.txt, so theclaude-agent-sdk>=0.2.138floor that matters lives inpyproject.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>/, wheresetting_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.
RUN mkdir -p /sessions/<exact-id-from-skill>/mnt/outputs/<rest-of-path> \
&& chmod -R 777 /sessionsA CLI binary the skill calls
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.tgzIf the skill's bootstrap looks for the binary at a specific path, put it exactly there instead of on PATH.
A writable outputs directory
RUN mkdir -p /outputs && chmod 777 /outputs
ENV REPORT_OUTPUTS_DIR=/outputsMount a file share at /outputs at deploy time so files survive container restarts.
A token or credentials directory
ENV REPORT_DIR=/mnt/reportSet 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
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 neverCOPY .env .. Credentials come at runtime from Container Apps secrets or--env-file. - `tests/`.
.dockerignoreexcludes 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.
__pycache__/
*.pyc
*.pyo
*.pyd
.pytest_cache/
.venv/
venv/
.env
*.log
.git/
.gitignore
README.md
tests/Ports
| Surface | Port |
|---|---|
| Plain HTTP | The 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 variant | 8080, 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.
Related
Last updated 2026-10-21