Skip to main content
RapidDev - Software Development Agency
build-ai-agent

How to Deploy an AI Agent

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 learn

  • Wrap an agent behind the invocation and health-check contract a hosted runtime expects
  • Build and push an ARM64 container image, required by Bedrock AgentCore Runtime
  • Create and invoke a managed agent runtime with boto3 instead of running your own server infrastructure
  • Set session timeouts and lifecycle limits so an idle or runaway session doesn't run forever
Book a free consultation
4.9Clutch rating ⭐
1,000+Happy partners
20+Countries served
200+Team members
Advanced9 min read45-60 minPython 3.11+, Docker with buildx, an AWS account with Bedrock AgentCore access, an Anthropic API key, macOS/LinuxLast updated November 2026RapidDev Engineering Team
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.

Quick facts about this guide
FactValue
ToolAny LLM or framework
DifficultyAdvanced
Time required45-60 min
CompatibilityPython 3.11+, Docker with buildx, an AWS account with Bedrock AgentCore access, an Anthropic API key, macOS/Linux
Last updatedNovember 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

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.

typescript
1from claude_agent_sdk import query, ClaudeAgentOptions, ResultMessage
2
3SLOTS = ["Tue 10am", "Tue 2pm", "Wed 9am"]
4
5async 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.result
17 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.

typescript
1from fastapi import FastAPI, HTTPException
2from pydantic import BaseModel
3from typing import Any, Dict
4
5app = FastAPI(title="Scheduling Agent")
6
7class InvocationRequest(BaseModel):
8 input: Dict[str, Any]
9
10class InvocationResponse(BaseModel):
11 output: Dict[str, Any]
12
13@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})
23
24@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.

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.

typescript
1uvicorn agent:app --host 0.0.0.0 --port 8080
2
3# In another terminal:
4curl http://localhost:8080/ping
5curl -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.

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.

typescript
1# Dockerfile
2FROM --platform=linux/arm64 python:3.11-slim
3
4WORKDIR /app
5COPY requirements.txt .
6RUN pip install --no-cache-dir -r requirements.txt
7
8COPY agent.py .
9
10EXPOSE 8080
11CMD ["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.

typescript
1docker buildx build --platform linux/arm64 \
2 -t <account-id>.dkr.ecr.us-west-2.amazonaws.com/scheduling-agent:latest --push .
3
4import boto3
5
6client = 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.

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.

typescript
1import json, uuid
2
3agent_core_client = boto3.client("bedrock-agentcore", region_name="us-west-2")
4session_id = str(uuid.uuid4()) + str(uuid.uuid4()) # ensure 33+ characters
5
6resp = 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()))
13
14# 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

agent.py
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 8080
4(pip install fastapi "uvicorn[standard]" claude-agent-sdk; set ANTHROPIC_API_KEY)
5"""
6from typing import Any, Dict
7
8from fastapi import FastAPI, HTTPException
9from pydantic import BaseModel
10
11from claude_agent_sdk import query, ClaudeAgentOptions, ResultMessage
12
13SLOTS = ["Tue 10am", "Tue 2pm", "Wed 9am"]
14
15app = FastAPI(title="Scheduling Agent")
16
17
18class InvocationRequest(BaseModel):
19 input: Dict[str, Any]
20
21
22class InvocationResponse(BaseModel):
23 output: Dict[str, Any]
24
25
26async 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.result
38 return final_text
39
40
41@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})
51
52
53@app.get("/ping")
54async def ping():
55 return {"status": "healthy"}
56
57
58if __name__ == "__main__":
59 import uvicorn
60
61 uvicorn.run(app, host="0.0.0.0", port=8080)
62

Common 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.

ChatGPT Prompt

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]

Build AI Agents Prompt

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.

RapidDev

Talk to an Expert

Our team has built 1,000+ apps. Get personalized help with your project.

Book a free consultation
Matt Graham

Written by

Matt Graham · CEO & Founder, RapidDev

1,000+ client projects delivered. Columbia University & Harvard Business School alumnus, U.S. Navy veteran. About the author →

Learning is great. Shipping is faster with help.

Our engineers have built 1,000+ apps on your platform and the tools around it. If your project needs to be live sooner than your learning curve allows — book a free consultation.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.