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.
| Fact | Value |
|---|---|
| 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 |
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.
Prerequisites
- 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
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.
1from claude_agent_sdk import query, ClaudeAgentOptions, ResultMessage23SLOTS = ["Tue 10am", "Tue 2pm", "Wed 9am"]45async def run_scheduling_agent(patient_request: str) -> str:6 options = ClaudeAgentOptions(7 system_prompt=(8 f"You are a dental clinic scheduling assistant. Open slots: {SLOTS}. "9 "Pick the closest matching slot to the patient's preference and confirm it."10 ),11 allowed_tools=[],12 )13 final_text = ""14 async for message in query(prompt=patient_request, options=options):15 if isinstance(message, ResultMessage) and message.subtype == "success":16 final_text = message.result17 return final_textExpected result: await run_scheduling_agent('I need a cleaning next Tuesday morning') returns a confirmation referencing 'Tue 10am'.
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.
1from fastapi import FastAPI, HTTPException2from pydantic import BaseModel3from typing import Any, Dict45app = FastAPI(title="Scheduling Agent")67class InvocationRequest(BaseModel):8 input: Dict[str, Any]910class InvocationResponse(BaseModel):11 output: Dict[str, Any]1213@app.post("/invocations", response_model=InvocationResponse)14async def invoke_agent(request: InvocationRequest):15 prompt = request.input.get("prompt", "")16 if not prompt.strip():17 raise HTTPException(status_code=400, detail="'prompt' must be a non-empty string")18 try:19 reply = await run_scheduling_agent(prompt)20 except Exception as e:21 raise HTTPException(status_code=500, detail=f"Agent processing failed: {e}")22 return InvocationResponse(output={"message": reply})2324@app.get("/ping")25async def ping():26 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.
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.
1uvicorn agent:app --host 0.0.0.0 --port 808023# In another terminal:4curl http://localhost:8080/ping5curl -X POST http://localhost:8080/invocations \6 -H "Content-Type: application/json" \7 -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.
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.
1# Dockerfile2FROM --platform=linux/arm64 python:3.11-slim34WORKDIR /app5COPY requirements.txt .6RUN pip install --no-cache-dir -r requirements.txt78COPY agent.py .910EXPOSE 808011CMD ["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.
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.
1docker buildx build --platform linux/arm64 \2 -t <account-id>.dkr.ecr.us-west-2.amazonaws.com/scheduling-agent:latest --push .34import boto356client = boto3.client("bedrock-agentcore-control", region_name="us-west-2")7response = client.create_agent_runtime(8 agentRuntimeName="dental_scheduling_agent",9 agentRuntimeArtifact={10 "containerConfiguration": {11 "containerUri": "<account-id>.dkr.ecr.us-west-2.amazonaws.com/scheduling-agent:latest"12 }13 },14 networkConfiguration={"networkMode": "PUBLIC"},15 roleArn="arn:aws:iam::<account-id>:role/AgentRuntimeRole",16 lifecycleConfiguration={"idleRuntimeSessionTimeout": 300, "maxLifetime": 1800},17)18print(response["agentRuntimeArn"])Expected result: create_agent_runtime returns an agentRuntimeArn you'll use to invoke the deployed agent next.
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.
1import json, uuid23agent_core_client = boto3.client("bedrock-agentcore", region_name="us-west-2")4session_id = str(uuid.uuid4()) + str(uuid.uuid4()) # ensure 33+ characters56resp = agent_core_client.invoke_agent_runtime(7 agentRuntimeArn=response["agentRuntimeArn"],8 runtimeSessionId=session_id,9 payload=json.dumps({"input": {"prompt": "I need a cleaning next Tuesday morning"}}),10 qualifier="DEFAULT",11)12print(json.loads(resp["response"].read()))1314# Once the booking conversation is done:15agent_core_client.stop_runtime_session(16 agentRuntimeArn=response["agentRuntimeArn"], runtimeSessionId=session_id, qualifier="DEFAULT",17)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 working example
1"""Dental clinic scheduling agent, deployable to AWS Bedrock AgentCore Runtime.2Fulfills the Runtime contract: POST /invocations, GET /ping, port 8080.3Run locally: uvicorn agent:app --host 0.0.0.0 --port 80804(pip install fastapi "uvicorn[standard]" claude-agent-sdk; set ANTHROPIC_API_KEY)5"""6from typing import Any, Dict78from fastapi import FastAPI, HTTPException9from pydantic import BaseModel1011from claude_agent_sdk import query, ClaudeAgentOptions, ResultMessage1213SLOTS = ["Tue 10am", "Tue 2pm", "Wed 9am"]1415app = FastAPI(title="Scheduling Agent")161718class InvocationRequest(BaseModel):19 input: Dict[str, Any]202122class InvocationResponse(BaseModel):23 output: Dict[str, Any]242526async def run_scheduling_agent(patient_request: str) -> str:27 options = ClaudeAgentOptions(28 system_prompt=(29 f"You are a dental clinic scheduling assistant. Open slots: {SLOTS}. "30 "Pick the closest matching slot to the patient's preference and confirm it."31 ),32 allowed_tools=[],33 )34 final_text = ""35 async for message in query(prompt=patient_request, options=options):36 if isinstance(message, ResultMessage) and message.subtype == "success":37 final_text = message.result38 return final_text394041@app.post("/invocations", response_model=InvocationResponse)42async def invoke_agent(request: InvocationRequest):43 prompt = request.input.get("prompt", "")44 if not isinstance(prompt, str) or not prompt.strip():45 raise HTTPException(status_code=400, detail="'prompt' must be a non-empty string")46 try:47 reply = await run_scheduling_agent(prompt)48 except Exception as e:49 raise HTTPException(status_code=500, detail=f"Agent processing failed: {e}")50 return InvocationResponse(output={"message": reply})515253@app.get("/ping")54async def ping():55 return {"status": "healthy"}565758if __name__ == "__main__":59 import uvicorn6061 uvicorn.run(app, host="0.0.0.0", port=8080)62Common mistakes
Why it's a problem: Making the health check endpoint call the agent or the model itself
How to avoid: A slow model call shouldn't fail a liveness probe. Keep /ping trivial and independent of /invocations entirely.
Why it's a problem: Building an x86 image for a platform that requires linux/arm64
How to avoid: Bedrock AgentCore Runtime requires ARM64. Use docker buildx with --platform linux/arm64 explicitly; your laptop's default build target often isn't ARM64.
Why it's a problem: Reusing one fixed runtimeSessionId across every patient conversation
How to avoid: 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.
Why it's a problem: Leaving lifecycleConfiguration at defaults without considering your actual conversation length
How to avoid: The default idle timeout may be shorter or longer than a real booking conversation needs. Set idleRuntimeSessionTimeout and maxLifetime deliberately for your use case.
Why it's a problem: Putting the Anthropic API key directly in the Dockerfile or container image
How to avoid: 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.
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
I have a FastAPI agent service with /invocations and /ping endpoints, deployed to AWS Bedrock AgentCore Runtime with a 300-second idle timeout (code below). Add a third endpoint, /invocations/stream, that streams the agent's response incrementally instead of waiting for the full result. [paste complete_code]
In Claude Code, open agent.py and ask: 'Add structured logging to invoke_agent that records the session's runtimeSessionId, prompt length, and response time for every call, without logging the full prompt text.'
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.
Talk to an Expert
Our team has built 1,000+ apps. Get personalized help with your project.
Book a free consultation