Browse the docs

Deploy to Foundry

Build the image, create an immutable agent version with deploy.py, grant the instance identity, and avoid the known deploy gotchas.

Before you startA built agent, a container registry the project can pull from, and the `Foundry Project Manager` role on the project.View as Markdown

1. Build the image

bash
docker build --platform linux/amd64 -f invoice-summarizer-agent/Dockerfile \
  -t invoice-summarizer-agent:$(git rev-parse --short HEAD) .

# or build server-side, with no local push:
az acr build --registry <registry> --platform linux/amd64 \
  --image invoice-summarizer-agent:<git-sha> \
  -f invoice-summarizer-agent/Dockerfile --no-logs .
  • linux/amd64 is required: the platform is x86_64 only.
  • Tag by git SHA. Never `:latest`, never a reused tag. The platform caches by tag, so a tag rebuilt after a version points at it is a fix that never shipped. deploy.py refuses :latest.

2. Configure .env

.env
FOUNDRY_PROJECT_ENDPOINT=https://<account>.services.ai.azure.com/api/projects/<project>
ANTHROPIC_FOUNDRY_RESOURCE=<account>
AGENT_IMAGE=<registry>.azurecr.io/invoice-summarizer-agent:<git-sha>

ANTHROPIC_DEFAULT_OPUS_MODEL=<opus-deployment-name>
ANTHROPIC_DEFAULT_SONNET_MODEL=<sonnet-deployment-name>
ANTHROPIC_DEFAULT_HAIKU_MODEL=<haiku-deployment-name>
CLAUDE_AGENT_MODEL=sonnet

MAX_TURNS=<from a measured run>
MAX_USD_PER_RUN=<from a measured run>
RUN_TIMEOUT_SECONDS=3300
SESSION_IDLE_TIMEOUT_SECONDS=3600

STEP_TOKENS=on
EMIT_STEP_ACTIVITY_LOG=true

HOSTED_CPU=2
HOSTED_MEMORY=4Gi

# only used to print a ready-to-paste role assignment
AZURE_SUBSCRIPTION_ID=<subscription-id>
AZURE_RESOURCE_GROUP=<resource-group>

There are no secrets in this file. ANTHROPIC_FOUNDRY_RESOURCE is the resource name only. Sandbox tiers are 0.5/1Gi, 1/2Gi and 2/4Gi; start on 2/4Gi. Guessing low doesn't give a slow run, it gives a container that dies at turn start with no envelope.

3. Run deploy.py

bash
pip install "azure-ai-projects>=2.6.0" azure-identity python-dotenv
az login
python deploy.py

deploy.py doesn't build or push. It creates a version from the image, waits for it to become active, routes 100% of the endpoint to it, and prints both agent principals and the endpoint URL.

deploy.py: the version definition
HostedAgentDefinition(
    cpu=CPU,
    memory=MEMORY,
    container_configuration=ContainerConfiguration(image=image),
    environment_variables=agent_environment(foundry_resource),
    protocol_versions=[ProtocolVersionRecord(protocol="responses", version="2.0.0")],
    session_configuration=SessionConfiguration(
        idle_timeout_seconds=timedelta(seconds=SESSION_IDLE_TIMEOUT_SECONDS)),
)

The version's environment sets CLAUDE_CODE_USE_FOUNDRY=1, ANTHROPIC_FOUNDRY_RESOURCE, the three pins, CLAUDE_AGENT_MODEL, MAX_TURNS, MAX_USD_PER_RUN (required, no defaults), RUN_TIMEOUT_SECONDS, CLAUDE_CODE_MAX_RETRIES (default 5), STEP_TOKENS, EMIT_STEP_ACTIVITY_LOG, and any integration settings you add. All of it is immutable per version, so "what limit was this run held to" is answerable from the deployment record.

Version payload gotchas

  • Protocol `responses` version `2.0.0`, not `1.0.0`. 1.0.0 gives an opaque internal server error on every request while the container looks healthy.
  • Session idle timeout on every version. Without it every run longer than 15 minutes dies. It needs SessionConfiguration, and the field is a timedelta, not an integer.
  • NAGENT_* or FOUNDRY_* names** in environment_variables: the platform rejects the whole version as reserved. Container defaults for those names go in the Dockerfile. The platform injects FOUNDRY_AGENT_VERSION, FOUNDRY_AGENT_NAME and FOUNDRY_PROJECT_ENDPOINT, which you can read.
  • Rollback is a routing change, not a rebuild. Versions are immutable and retained; point the version selector at a previous one.

4. Grant the identities

A hosted agent carries two principals: a blueprint and an instance identity. The sandbox authenticates as the instance identity. Granting the blueprint looks right in the portal and produces 401 Principal does not have access to API/Operation. The instance identity is stable across versions, so you grant it once.

Model access (printed by deploy.py)
az role assignment create \
  --assignee-object-id <instance-identity-principal-id> \
  --assignee-principal-type ServicePrincipal \
  --role "Foundry User" \
  --scope "/subscriptions/<subscription-id>/resourceGroups/<resource-group>/providers/Microsoft.CognitiveServices/accounts/<account>"
  • Account scope, not project. Claude Code calls the account's /anthropic/v1/messages endpoint itself, and account-level endpoints aren't proxied by the project.
  • Use `--assignee-object-id` with `--assignee-principal-type ServicePrincipal`. The plain --assignee form does a Graph lookup a non-privileged account can't perform on these principals.
  • Image pull uses the account and project managed identities, not the agent's. image_pull_failed, UnauthorizedAcrPull or InvalidAcrPullCredentials mean they lack classic AcrPull on the registry. Container Registry Repository Reader is inert on a registry in legacy permissions mode. The registry also needs azureADAuthenticationAsArmPolicy enabled.
  • Every outbound server the skill calls must allow the same instance identity: an app role and, if the server keeps one, its caller allowlist.

Outbound integrations

An unattended agent that must write to a Microsoft 365 document library or any Graph-backed store needs an app-to-app MCP server the brain calls. Foundry's built-in document-library tool is read-only grounding and refuses managed identities. When that API is app-only, no person can hold a token for it, so {"mode":"preflight"} is the only test. Adding a delegated scope so a person can probe it removes the server's security property.

Runtime notes

  • CLAUDE_CODE_MAX_RETRIES is bounded at 5. Foundry returns no rate-limit headers, and the CLI's default backoff makes a hard failure look like a hang.
  • There is no per-request caller authorization on the data plane. Any principal with project data-plane access can invoke a run and spend up to MAX_USD_PER_RUN. Don't route a production agent to callers beyond the deploying team, and say so in its README.
  • Set an Azure budget and action group on the subscription. It's the only bound that survives restarts.

Windows workstations

  • The Azure CLI can crash with UnicodeEncodeError: 'charmap' while streaming output. Set $env:PYTHONIOENCODING = "utf-8" and use az acr build --no-logs. The server-side operation usually succeeded, so check state before retrying.
  • docker push through Docker Desktop's proxy can die partway. Use az acr build.
  • Git Bash rewrites a leading /subscriptions/... argument into a Windows path. Run scope-bearing az commands from PowerShell or set MSYS_NO_PATHCONV=1. JMESPath filters break in PowerShell; run those from Bash.

Last updated 2026-10-21