# How to Deploy an AI Agent

- Tool: Any LLM or framework
- Difficulty: Advanced
- Time required: 45-60 min
- Compatibility: Python 3.11+, Docker with buildx, an AWS account with Bedrock AgentCore access, an Anthropic API key, macOS/Linux
- Last updated: November 2026

## TL;DR

Deploying an AI agent means wrapping it behind a stable HTTP contract, containerizing it, and running it somewhere that handles scaling, session isolation, and restarts for you. This guide deploys an appointment-scheduling agent for a dental clinic: a FastAPI service with invoke and health-check endpoints, an ARM64 Dockerfile, and a run on AWS Bedrock AgentCore Runtime. Budget 45-60 minutes, Docker, and an AWS account.

## What you'll build

You'll deploy a scheduling agent for a dental clinic's phone and web intake: it checks open appointment slots and books one, given a patient's preferred day and reason for the visit. Locally this is just a Python function; in production it needs to survive restarts, serve multiple patients concurrently without their sessions bleeding into each other, and run somewhere you're not personally keeping a terminal window open.

This guide wraps the agent in a small FastAPI app that implements two endpoints: POST /invocations, which takes a request and returns the agent's response, and GET /ping, a health check any hosting platform uses to know the service is alive. That contract is deliberately generic, not tied to one vendor, which is why it's also what AWS Bedrock AgentCore Runtime expects from a custom container. You build the image once, push it to a container registry, and AgentCore handles the rest: scaling, per-session isolation, and shutting down idle sessions automatically.

Choose a managed runtime like AgentCore over a plain always-on server when you want per-session isolation (one patient's booking attempt can't see another's) and don't want to operate your own autoscaling. If you just need a single always-on process with no per-session isolation requirement, a simpler container host (Cloud Run, a basic EC2 instance, Vercel's serverless functions for the HTTP layer) is less to configure.

## Before you start

- Python 3.11+ and Docker with buildx installed
- An Anthropic API key (ANTHROPIC_API_KEY)
- An AWS account with Bedrock AgentCore access and an IAM role for the runtime
- pip install fastapi "uvicorn[standard]" claude-agent-sdk boto3

## Step-by-step guide

### 1. Write the agent function

Keep the agent itself as a plain async function, independent of any web framework or hosting detail. It takes a patient's request string and returns a response string; everything about how it's exposed and scaled lives in the next step, not here.

```
from claude_agent_sdk import query, ClaudeAgentOptions, ResultMessage

SLOTS = ["Tue 10am", "Tue 2pm", "Wed 9am"]

async def run_scheduling_agent(patient_request: str) -> str:
    options = ClaudeAgentOptions(
        system_prompt=(
            f"You are a dental clinic scheduling assistant. Open slots: {SLOTS}. "
            "Pick the closest matching slot to the patient's preference and confirm it."
        ),
        allowed_tools=[],
    )
    final_text = ""
    async for message in query(prompt=patient_request, options=options):
        if isinstance(message, ResultMessage) and message.subtype == "success":
            final_text = message.result
    return final_text
```

**Expected result:** await run_scheduling_agent('I need a cleaning next Tuesday morning') returns a confirmation referencing 'Tue 10am'.

### 2. Wrap it in the invocation and health-check contract

Build a FastAPI app with exactly two routes: POST /invocations, which reads a prompt field from the request body and calls the agent, and GET /ping, which returns a simple healthy status with no dependency on the agent itself. This split matters: a slow or stuck model call shouldn't make the health check fail and trigger an unnecessary restart.

```
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from typing import Any, Dict

app = FastAPI(title="Scheduling Agent")

class InvocationRequest(BaseModel):
    input: Dict[str, Any]

class InvocationResponse(BaseModel):
    output: Dict[str, Any]

@app.post("/invocations", response_model=InvocationResponse)
async def invoke_agent(request: InvocationRequest):
    prompt = request.input.get("prompt", "")
    if not prompt.strip():
        raise HTTPException(status_code=400, detail="'prompt' must be a non-empty string")
    try:
        reply = await run_scheduling_agent(prompt)
    except Exception as e:
        raise HTTPException(status_code=500, detail=f"Agent processing failed: {e}")
    return InvocationResponse(output={"message": reply})

@app.get("/ping")
async def ping():
    return {"status": "healthy"}
```

**Expected result:** curl -X POST localhost:8080/invocations -d '{"input":{"prompt":"book me Tuesday"}}' returns a JSON response, and curl localhost:8080/ping returns {'status': 'healthy'} instantly.

### 3. Test the service locally before containerizing

Run it with uvicorn and hit both endpoints from a second terminal. Confirming this works outside Docker first isolates whether a later failure is in your code or in the container build.

```
uvicorn agent:app --host 0.0.0.0 --port 8080

# In another terminal:
curl http://localhost:8080/ping
curl -X POST http://localhost:8080/invocations \
  -H "Content-Type: application/json" \
  -d '{"input": {"prompt": "I need a cleaning next Tuesday morning"}}'
```

**Expected result:** Both curl commands return 200 with the expected JSON bodies, confirmed before any Docker involvement.

### 4. Write the ARM64 Dockerfile

Bedrock AgentCore Runtime requires linux/arm64 images. Base the image on a slim Python build, install dependencies, copy the app, and run uvicorn on port 8080 to match the contract the previous steps already established.

```
# Dockerfile
FROM --platform=linux/arm64 python:3.11-slim

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

COPY agent.py .

EXPOSE 8080
CMD ["uvicorn", "agent:app", "--host", "0.0.0.0", "--port", "8080"]
```

**Expected result:** docker buildx build --platform linux/arm64 -t scheduling-agent:arm64 --load . completes with no errors.

### 5. Build, push, and deploy to AgentCore Runtime

Build for ARM64, push to an ECR repository, then call create_agent_runtime with boto3, pointing at that image. lifecycleConfiguration caps how long an idle session and a session overall are allowed to live, which bounds both cost and the blast radius of a stuck run.

```
docker buildx build --platform linux/arm64 \
  -t <account-id>.dkr.ecr.us-west-2.amazonaws.com/scheduling-agent:latest --push .

import boto3

client = boto3.client("bedrock-agentcore-control", region_name="us-west-2")
response = client.create_agent_runtime(
    agentRuntimeName="dental_scheduling_agent",
    agentRuntimeArtifact={
        "containerConfiguration": {
            "containerUri": "<account-id>.dkr.ecr.us-west-2.amazonaws.com/scheduling-agent:latest"
        }
    },
    networkConfiguration={"networkMode": "PUBLIC"},
    roleArn="arn:aws:iam::<account-id>:role/AgentRuntimeRole",
    lifecycleConfiguration={"idleRuntimeSessionTimeout": 300, "maxLifetime": 1800},
)
print(response["agentRuntimeArn"])
```

**Expected result:** create_agent_runtime returns an agentRuntimeArn you'll use to invoke the deployed agent next.

### 6. Harden it: invoke with a real session ID and stop idle sessions early

Each invocation needs a runtimeSessionId at least 33 characters long, which is what AgentCore uses to isolate one patient's conversation from another's. Generate a real one per conversation rather than reusing a fixed string, and call stop_runtime_session once a conversation is clearly finished instead of waiting for the idle timeout to reclaim it.

```
import json, uuid

agent_core_client = boto3.client("bedrock-agentcore", region_name="us-west-2")
session_id = str(uuid.uuid4()) + str(uuid.uuid4())  # ensure 33+ characters

resp = agent_core_client.invoke_agent_runtime(
    agentRuntimeArn=response["agentRuntimeArn"],
    runtimeSessionId=session_id,
    payload=json.dumps({"input": {"prompt": "I need a cleaning next Tuesday morning"}}),
    qualifier="DEFAULT",
)
print(json.loads(resp["response"].read()))

# Once the booking conversation is done:
agent_core_client.stop_runtime_session(
    agentRuntimeArn=response["agentRuntimeArn"], runtimeSessionId=session_id, qualifier="DEFAULT",
)
```

**Expected result:** Each patient conversation gets its own isolated session, and a finished conversation stops immediately instead of idling for the full 300-second timeout.

## Complete code example

File: `agent.py`

```python
"""Dental clinic scheduling agent, deployable to AWS Bedrock AgentCore Runtime.
Fulfills the Runtime contract: POST /invocations, GET /ping, port 8080.
Run locally: uvicorn agent:app --host 0.0.0.0 --port 8080
(pip install fastapi "uvicorn[standard]" claude-agent-sdk; set ANTHROPIC_API_KEY)
"""
from typing import Any, Dict

from fastapi import FastAPI, HTTPException
from pydantic import BaseModel

from claude_agent_sdk import query, ClaudeAgentOptions, ResultMessage

SLOTS = ["Tue 10am", "Tue 2pm", "Wed 9am"]

app = FastAPI(title="Scheduling Agent")


class InvocationRequest(BaseModel):
    input: Dict[str, Any]


class InvocationResponse(BaseModel):
    output: Dict[str, Any]


async def run_scheduling_agent(patient_request: str) -> str:
    options = ClaudeAgentOptions(
        system_prompt=(
            f"You are a dental clinic scheduling assistant. Open slots: {SLOTS}. "
            "Pick the closest matching slot to the patient's preference and confirm it."
        ),
        allowed_tools=[],
    )
    final_text = ""
    async for message in query(prompt=patient_request, options=options):
        if isinstance(message, ResultMessage) and message.subtype == "success":
            final_text = message.result
    return final_text


@app.post("/invocations", response_model=InvocationResponse)
async def invoke_agent(request: InvocationRequest):
    prompt = request.input.get("prompt", "")
    if not isinstance(prompt, str) or not prompt.strip():
        raise HTTPException(status_code=400, detail="'prompt' must be a non-empty string")
    try:
        reply = await run_scheduling_agent(prompt)
    except Exception as e:
        raise HTTPException(status_code=500, detail=f"Agent processing failed: {e}")
    return InvocationResponse(output={"message": reply})


@app.get("/ping")
async def ping():
    return {"status": "healthy"}


if __name__ == "__main__":
    import uvicorn

    uvicorn.run(app, host="0.0.0.0", port=8080)
```

## Common mistakes

- **Making the health check endpoint call the agent or the model itself** — undefined Fix: A slow model call shouldn't fail a liveness probe. Keep /ping trivial and independent of /invocations entirely.
- **Building an x86 image for a platform that requires linux/arm64** — undefined Fix: Bedrock AgentCore Runtime requires ARM64. Use docker buildx with --platform linux/arm64 explicitly; your laptop's default build target often isn't ARM64.
- **Reusing one fixed runtimeSessionId across every patient conversation** — undefined Fix: Sessions isolate conversations from each other. A shared session ID means one patient's context can bleed into another's. Generate a real unique ID (33+ characters) per conversation.
- **Leaving lifecycleConfiguration at defaults without considering your actual conversation length** — undefined Fix: The default idle timeout may be shorter or longer than a real booking conversation needs. Set idleRuntimeSessionTimeout and maxLifetime deliberately for your use case.
- **Putting the Anthropic API key directly in the Dockerfile or container image** — undefined Fix: Pass it as a runtime environment variable or through AWS Secrets Manager, never baked into the image, which anyone with image access could then extract.

## Best practices

- Keep the agent's own logic independent of the web framework, so it's testable without spinning up a server.
- Make /ping trivial and dependency-free; a healthy process should answer it even if a downstream API is degraded.
- Generate a real unique session ID per conversation, never a shared or hardcoded one.
- Set lifecycleConfiguration's idle and max timeouts to match your actual conversation length, not the platform default.
- Pass all secrets through environment variables or a secrets manager, never baked into the container image.
- Test the FastAPI service locally with curl before touching Docker, so a failure's cause is obvious.

## Use cases

### undefined



### undefined



### undefined



## Frequently asked questions

### Do I have to use AWS Bedrock AgentCore Runtime, or does this contract work elsewhere?

The /invocations and /ping contract is specific to how AgentCore Runtime expects a custom container to behave, but the underlying pattern, a stateless HTTP wrapper around your agent logic, works on Cloud Run, a basic container host, or any platform that can run a Docker image and route HTTP traffic to it.

### Why does AgentCore require ARM64 specifically?

It's a platform requirement for the Runtime's underlying infrastructure. If you're building on an x86 machine, docker buildx with --platform linux/arm64 cross-compiles the image correctly without needing ARM hardware yourself.

### What happens if a session's maxLifetime is reached mid-conversation?

The runtime session ends regardless of whether the conversation finished. Set maxLifetime generously enough for your longest realistic conversation, and have your client code handle a session-ended response by starting a fresh session if needed.

### How much does running this cost per month?

AgentCore Runtime bills by vCPU-hour and GB-hour for active sessions (roughly $0.0895 per vCPU-hour plus $0.00945 per GB-hour as of late 2026), only while a session is active, not for idle capacity. A low-volume scheduling agent typically costs a few dollars a month in runtime, plus the Claude API usage per conversation.

### Can I deploy the same container to a non-AWS host instead?

Yes. Since the contract is just HTTP on port 8080 with two routes, the same image runs on Cloud Run, a Fargate task, or a plain VM; only the deployment and session-management code in the last two steps is AgentCore-specific.

### Can someone handle this deployment for me?

Yes. RapidDev deploys agents to AWS Bedrock AgentCore, Cloud Run, and Vercel, including the container setup and session-lifecycle tuning most tutorials skip. A free 30-minute consultation is the fastest way to scope your version.

---

Source: https://www.rapidevelopers.com/build-ai-agent/how-to-deploy-an-ai-agent
© RapidDev — https://www.rapidevelopers.com/build-ai-agent/how-to-deploy-an-ai-agent
