How to Deploy a Retrieval‑Augmented Generation API on AWS App Runner — Explained Simply A developer has published a guide explaining how to deploy a Retrieval-Augmented Generation (RAG) API on AWS App Runner, using Node.js 22, DynamoDB vector search, and Claude for answer generation. The post details the use of a VPC connector for private access to DynamoDB and S3, and provides code snippets for creating the connector and deploying the service. It emphasizes App Runner's fully managed platform, which simplifies deployment by handling HTTPS, scaling, and observability. You can spin up a production‑grade RAG service without wrestling with EC2, Fargate, or complex Terraform. In a few minutes you’ll have a Node.js 22 API that pulls relevant chunks from DynamoDB vector search and calls Claude for answer generation, all running on App Runner’s fully managed platform. In plain English:App Runner gives you a “run‑your‑code” button that hides servers, load balancers, and scaling rules behind a single service definition. When you start building a Retrieval‑Augmented Generation RAG service you need three things: vectorSearch operation. Typical choices are ECS Elastic Container Service or Lambda. Both work, but they also require you to: App Runner sits in the middle: you provide a container image, tell it which VPC to use, and it takes care of health checks, HTTPS, and auto‑scaling. For a service that does a quick DynamoDB lookup and a remote HTTP call, the latency added by App Runner’s cold start 10‑30 s is usually acceptable if you keep the container warm with a health‑check ping. | Benefit | What it means for a RAG API | |---|---| Fully managed HTTPS | No need to configure a load balancer or ACM certificate. | VPC connector | Private traffic to DynamoDB and S3 stays inside your network, avoiding public internet exposure. | Automatic scaling | Instances grow and shrink based on request count, keeping cost low when traffic is idle. | Integrated observability | Built‑in CloudWatch logs and metrics without extra agents. | Key takeaway:App Runner removes the operational plumbing, letting you focus on the retrieval and generation logic. Before we write any code, we need a place where that code will run. A VPC connector is a private tunnel that lets an App Runner service reach resources inside a Virtual Private Cloud VPC . Think of it as a secure hallway that only your service can walk through to get to DynamoDB and S3. js // createVpcConnector.ts import { AppRunnerClient, CreateVpcConnectorCommand, } from "@aws-sdk/client-apprunner"; const client = new AppRunnerClient { region: "us-east-1" } ; async function createConnector { const command = new CreateVpcConnectorCommand { VpcConnectorName: "rag-app-runner-connector", Subnets: "subnet-0abc123def456ghi", // private subnet A "subnet-0jkl789mno012pqr", // private subnet B , SecurityGroups: "sg-0examplesecuritygroup" , // will need outbound HTTPS } ; const response = await client.send command ; console.log "Connector ARN:", response.VpcConnector?.VpcConnectorArn ; } createConnector .catch console.error ; Why this matters: The connector tells App Runner which subnets and security groups to use. The security group must allow outbound HTTPS port 443 to the DynamoDB VPC endpoint; otherwise you’ll see AccessDeniedException errors that look like credential problems. Tip:After creating the connector, add an outbound rule HTTPS 443 → 0.0.0.0/0 or, tighter, target the DynamoDB endpoint IPs. App Runner runs containers, so we package our Express API in a Dockerfile. Dockerfile FROM node:22-alpine AS builder Install only production dependencies WORKDIR /app COPY package .json ./ RUN npm ci --production Copy source files COPY src ./src Use a non‑root user for safety RUN addgroup -S app && adduser -S runner -G app USER runner Expose the port App Runner expects default 8080 EXPOSE 8080 Start the server CMD "node", "src/index.js" Why this matters: The --production flag keeps the image small, which reduces start‑up time. Using a non‑root user follows best‑practice security. Now we tie everything together with the App Runner SDK. js // deployAppRunner.ts import { AppRunnerClient, CreateServiceCommand, } from "@aws-sdk/client-apprunner"; const client = new AppRunnerClient { region: "us-east-1" } ; async function deploy { const command = new CreateServiceCommand { ServiceName: "rag-api", SourceConfiguration: { ImageRepository: { ImageIdentifier: "123456789012.dkr.ecr.us-east-1.amazonaws.com/rag-api:latest", ImageRepositoryType: "ECR", // App Runner pulls the image automatically when you push a new tag }, AuthenticationConfiguration: { // If the repository is private, provide an IAM role or secret }, }, InstanceConfiguration: { Cpu: "1 vCPU", Memory: "2 GB", }, HealthCheckConfiguration: { // App Runner expects a 200‑OK response on this path Path: "/health", Protocol: "TCP", Interval: 10, Timeout: 5, HealthyThreshold: 1, UnhealthyThreshold: 5, }, NetworkConfiguration: { EgressConfiguration: { EgressType: "VPC", VpcConnectorArn: "arn:aws:apprunner:us-east-1:123456789012:vpcconnector/rag-app-runner-connector", }, }, } ; const response = await client.send command ; console.log "Service URL:", response.Service?.ServiceUrl ; } deploy .catch console.error ; Why this matters: The NetworkConfiguration tells App Runner to use the VPC connector we created, making DynamoDB and S3 reachable without exposing them to the internet. The health‑check ensures the service only receives traffic when the container is ready; misconfiguring it e.g., pointing at a non‑existent route leads to silent deploy failures. Gotcha:App Runner cold starts can take 10‑30 seconds. If you need sub‑second latency for every request, consider keeping the service warm with a periodic health‑check ping or explore ECS/Fargate. Now that the platform is ready, let’s write the actual API logic. POST /rag . vectorSearch API to get the IDs of the most similar document chunks. .txt files . python // src/ragHandler.ts import express from "express"; import { DynamoDBClient } from "@aws-sdk/client-dynamodb"; import { DynamoDBDocumentClient, QueryCommand } from "@aws-sdk/lib-dynamodb"; import fetch from "node-fetch"; // Claude endpoint – replace with your own API key const CLAUDE API URL = "https://api.anthropic.com/v1/completions"; const CLAUDE API KEY = process.env.CLAUDE API KEY ; // DynamoDB client uses VPC endpoint automatically because of the VPC connector const ddbClient = new DynamoDBClient {} ; const ddbDoc = DynamoDBDocumentClient.from ddbClient ; // S3 bucket where raw documents live const S3 BUCKET = "my-rag-documents"; const router = express.Router ; / POST /rag Body: { "question": "string", "k": number? } / router.post "/rag", async req, res = { const { question, k = 5 } = req.body; if question { return res.status 400 .json { error: "question is required" } ; } try { // 1️⃣ Vector search – retrieve top‑k chunk IDs const vectorQuery = new QueryCommand { TableName: "RagVectors", IndexName: "VectorIndex", // The new vectorSearch operator 2025‑09 release KeyConditionExpression: "vectorSearch :qvec, :k ", ExpressionAttributeValues: { ":qvec": { // Convert the question to an embedding using your favorite model. // Here we fake it with a placeholder; in production you’d call an embedding API. "S": "placeholder‑embedding", }, ":k": { N: k.toString }, }, } ; const vectorResult = await ddbDoc.send vectorQuery ; const chunkIds = vectorResult.Items ?? .map item = item.chunkId.S ; // 2️⃣ Fetch raw text from S3 for each chunk const fetchChunk = async id: string = { const url = https://${S3 BUCKET}.s3.amazonaws.com/${id}.txt ; const resp = await fetch url ; if resp.ok throw new Error Failed to fetch ${id} ; return resp.text ; }; const chunks = await Promise.all chunkIds.map fetchChunk ; // 3️⃣ Build the prompt for Claude const prompt = You are a helpful assistant. Use only the information below to answer the user’s question. --- Retrieved Context --- ${chunks.join "\n---\n" } --- Question --- ${question} ; // 4️⃣ Call Claude const claudeResp = await fetch CLAUDE API URL, { method: "POST", headers: { "Content-Type": "application/json", "x-api-key": CLAUDE API KEY, }, body: JSON.stringify { model: "claude-3-5-sonnet-20240620", max tokens: 1024, temperature: 0, prompt, } , } ; if claudeResp.ok { const err = await claudeResp.text ; throw new Error Claude error: ${err} ; } const { completion } = await claudeResp.json ; // 5️⃣ Respond to the client res.json { answer: completion, usedChunkIds: chunkIds, } ; } catch e { console.error e ; res.status 500 .json { error: "internal server error" } ; } } ; export default router; Why each piece exists: vectorSearch query is the heart of Retrieval‑Augmented Generation – it finds the most semantically similar pieces of text. fetch to call Claude avoids pulling in a heavyweight SDK, keeping the container lean. python // src/index.js import express from "express"; import ragRouter from "./ragHandler.js"; const app = express ; app.use express.json ; // Simple health‑check endpoint required by App Runner app.get "/health", req, res = res.sendStatus 200 ; // Mount the RAG router app.use "/", ragRouter ; // Listen on the port App Runner provides defaults to 8080 const PORT = process.env.PORT || 8080; app.listen PORT, = { console.log RAG API listening on port ${PORT} ; } ; Plain English recap:The server receives a question, looks up the most relevant chunks, pulls their full text, asks Claude to answer, and returns the answer. Build docker build -t rag-api:latest . Tag for ECR docker tag rag-api:latest 123456789012.dkr.ecr.us-east-1.amazonaws.com/rag-api:latest Push assumes you have logged in via aws ecr get-login-password docker push 123456789012.dkr.ecr.us-east-1.amazonaws.com/rag-api:latest Tip:Enable “Automatic deployments” in the App Runner console so that every new tag triggers a fresh rollout. App Runner integrates with AWS Secrets Manager and AWS Systems Manager Parameter Store . Store your Claude API key there, then reference it in the service definition. // When creating the service, add a secret binding { ServiceName: "rag-api", SourceConfiguration: { / … / }, // SecretBinding allows the container to read the value from /run/secrets/ ServiceObservabilityConfiguration: { ObservabilityEnabled: true, }, InstanceConfiguration: { / … / }, // Attach secret Secrets: { Name: "CLAUDE API KEY", ValueFrom: "arn:aws:secretsmanager:us-east-1:123456789012:secret:claude-key-abc123", }, , } Inside the container the environment variable CLAUDE API KEY will be populated automatically. No hard‑coded credentials. Key takeaway:Using managed secrets prevents accidental leaks and lets you rotate keys without redeploying code. stdout / stderr to CloudWatch Logs automatically. Our console.error statements appear there. js // Example: add a simple request‑duration metric import { Histogram } from "prom-client"; const requestHistogram = new Histogram { name: "rag request duration seconds", help: "Duration of /rag requests", buckets: 0.1, 0.5, 1, 2, 5 , } ; router.post "/rag", async req, res = { const end = requestHistogram.startTimer ; // … existing logic … end ; // record duration } ; Tip:Export Prometheus metrics on /metrics and let App Runner scrape them with a sidecar if you prefer that ecosystem. App Runner uses concurrency‑based scaling : it adds a new instance when the average request concurrency exceeds a configurable limit default is 100 . Each instance runs a single container, so warm‑up time is the same as the container start‑up time. | Resource | Pricing 2026 | Approximate monthly cost for 10 k requests | |---|---|---| | App Runner compute 1 vCPU, 2 GB | $0.064 per vCPU‑hour + $0.008 per GB‑hour | ~$8 mostly idle | | DynamoDB read vector query | $0.25 per WCU‑hour | $1‑2 depending on k | | S3 GET requests | $0.0004 per 1 000 GETs | <$0.01 | | Claude API pay‑per‑token | $0.015 per 1 k input tokens, $0.030 per 1 k output | Varies, ~ $5‑10 for small queries | Overall, the stack stays under $20/month for modest traffic, far cheaper than a constantly‑running EC2 instance. Bottom line:For a typical RAG service that sees intermittent traffic, App Runner wins on simplicity and cost. Switch to ECS/Fargate only when you hit the cold‑start or networking limits. AccessDeniedException . With these steps you can launch a production‑grade Retrieval‑Augmented Generation API on AWS App Runner in under ten minutes, without wrestling with the usual cloud plumbing. Happy building Transparency noticeThis article was written with the help of an AI system — Groq GPT OSS 120B . Published:2026-08-20 ·Primary focus:AppRunnerAll code blocks are intended to be correct and runnable, but please verify them against the official docs for the tools mentioned before using in production. Find an error? Drop a comment — corrections are always welcome.