Deploy Astro on AWS
The web framework for content-driven websites with islands architecture.
astro.buildDeploy your Astro applications to AWS using Thunder. Choose the pattern that fits your app’s needs.
Available Patterns
Prerequisites
Getting Started
Create Project
Scaffold a new Astro project using your preferred package manager. This sets up the project structure, installs dependencies, and prepares you for development.
bunx create-astro@latest my-astro-appcd my-astro-appnpm create astro@latest my-astro-appcd my-astro-apppnpm create astro my-astro-appcd my-astro-appInstall Thunder
Add Thunder as a development dependency. It provides the CDK constructs you’ll use to define your AWS infrastructure.
bun add @thunder-so/thunder --developmentnpm install @thunder-so/thunder --save-devpnpm add -D @thunder-so/thunderAstro Static Site Deployment
Deploy a fully pre-rendered Astro site to S3 with CloudFront as the CDN. This is the simplest and most cost-effective pattern — no server required. Every page is generated at build time and served as static files.
Configure
Astro defaults to static output, but it’s good practice to be explicit. Set output: 'static' in your config to ensure all pages are pre-rendered at build time.
import { defineConfig } from 'astro/config';
export default defineConfig({ output: 'static',});Stack
Create a stack file that defines your AWS infrastructure. The Static construct provisions an S3 bucket, a CloudFront distribution, and optionally a Route53 DNS record.
import { Cdk, Static, type StaticProps } from '@thunder-so/thunder';
const config: StaticProps = { env: { account: 'YOUR_ACCOUNT_ID', region: 'us-east-1' }, application: 'myapp', service: 'web', environment: 'prod', rootDir: '.', outputDir: 'dist',};
new Static(new Cdk.App(), `${config.application}-${config.service}-${config.environment}-stack`, config);Deploy
Build your Astro site first — this generates the static files in dist/. Then deploy with CDK, which uploads the files to S3 and provisions the CloudFront distribution.
bun run buildnpx cdk deploy --app "bunx tsx stack/prod.ts" --profile defaultnpm run buildnpx cdk deploy --app "npx tsx stack/prod.ts" --profile defaultpnpm run buildpnpm exec cdk deploy --app "pnpm exec tsx stack/prod.ts" --profile defaultAfter deployment, CDK outputs a CloudFront URL where your static site is live.
Astro Containerized Deployment with Fargate
Run your Astro app as a Node.js server inside a Docker container on ECS Fargate. Traffic is routed through an Application Load Balancer. This pattern supports full SSR, API routes, and any server-side logic.
Configure for Node Server
Install the official Astro Node.js adapter, then configure it in standalone mode so the build output is a self-contained server entry point.
bun add @astrojs/nodenpm install @astrojs/nodepnpm add @astrojs/nodeimport { defineConfig } from 'astro/config';import node from '@astrojs/node';
export default defineConfig({ output: 'server', adapter: node({ mode: 'standalone' }),});Stack
The Fargate construct creates an ECS cluster, a Fargate task definition, and an Application Load Balancer.
import { Cdk, Fargate, type FargateProps } from '@thunder-so/thunder';
const config: FargateProps = { env: { account: 'YOUR_ACCOUNT_ID', region: 'us-east-1' }, application: 'myapp', service: 'web', environment: 'prod', rootDir: '.', serviceProps: { dockerFile: 'Dockerfile', architecture: Cdk.aws_ecs.CpuArchitecture.ARM64, cpu: 512, memorySize: 1024, port: 4321, desiredCount: 1, healthCheckPath: '/', },};
new Fargate(new Cdk.App(), `${config.application}-${config.service}-${config.environment}-stack`, config);Dockerfile
Create a Dockerfile in your project root. The multi-stage build keeps the final image lean by separating the build environment from the runtime.
FROM public.ecr.aws/docker/library/node:22-alpine AS builderWORKDIR /appCOPY package.json bun.lockb ./RUN curl -fsSL https://bun.sh/install | bash && export PATH="$HOME/.bun/bin:$PATH"RUN bun install --frozen-lockfileCOPY . .RUN bun run build
FROM public.ecr.aws/docker/library/node:22-alpine AS runnerWORKDIR /appENV NODE_ENV=productionENV HOST=0.0.0.0ENV PORT=4321COPY --from=builder /app/dist ./distCOPY --from=builder /app/node_modules ./node_modulesCOPY --from=builder /app/package.json ./EXPOSE 4321CMD ["node", "./dist/server/entry.mjs"]Environment Variables and Secrets
Runtime environment variables are injected into the Fargate task at deploy time. For sensitive values, store them in AWS Secrets Manager and reference them by ARN — Thunder fetches and injects them automatically.
const config: FargateProps = { // ... serviceProps: { // ... variables: [ { NODE_ENV: 'production' }, ], secrets: [ { key: 'DATABASE_URL', resource: 'arn:aws:secretsmanager:us-east-1:123456789012:secret:/myapp/DATABASE_URL-abc123', }, ], },};Deploy
CDK builds the Docker image, pushes it to ECR, and deploys it to Fargate. No manual Docker commands needed.
npx cdk deploy --app "bunx tsx stack/prod.ts" --profile defaultnpx cdk deploy --app "npx tsx stack/prod.ts" --profile defaultpnpm exec cdk deploy --app "pnpm exec tsx stack/prod.ts" --profile defaultAfter deployment, CDK outputs the Load Balancer DNS for your application.
Astro Serverless Fullstack Deployment
Deploy Astro with SSR using AWS Lambda for server-side rendering, S3 for static assets, and CloudFront to unify both behind a single domain. This pattern scales to zero and charges only for actual requests.
Install Adapter for Lambda
The @astro-aws/adapter package adapts Astro’s SSR output to the Lambda function handler format expected by API Gateway.
bun add @astro-aws/adapternpm install @astro-aws/adapterpnpm add @astro-aws/adapterConfigure Astro for AWS Lambda
Set output: 'server' to enable SSR and point the adapter at @astro-aws/adapter. The build will produce a Lambda handler in dist/lambda/ and static assets in dist/client/.
import { defineConfig } from 'astro/config';// @ts-ignore package does not provide typesimport aws from '@astro-aws/adapter';
export default defineConfig({ output: 'server', adapter: aws(),});Stack (Zip mode)
The Astro construct wires up Lambda, API Gateway, S3, and CloudFront automatically. By default, Thunder packages your Lambda handler as a Zip deployment — the fastest option for most apps.
import { Cdk, Astro, type AstroProps } from '@thunder-so/thunder';
const config: AstroProps = { env: { account: 'YOUR_ACCOUNT_ID', region: 'us-east-1' }, application: 'myapp', service: 'web', environment: 'prod', rootDir: '.',};
new Astro(new Cdk.App(), `${config.application}-${config.service}-${config.environment}-stack`, config);Container Mode
Zip deployments have a 250 MB unzipped size limit. If your app has large dependencies — native modules, ML libraries, or heavy assets — switch to container mode. Thunder builds a Docker image, pushes it to ECR, and deploys it as a container Lambda, which supports up to 10 GB.
Stack (Container mode)
Add dockerFile to serverProps to enable container mode.
const config: AstroProps = { // ... serverProps: { dockerFile: 'Dockerfile', memorySize: 2048, },};Dockerfile
FROM public.ecr.aws/lambda/nodejs:22
# Copy all lambda filesCOPY . ./
CMD ["index.handler"]Environment Variables and Secrets
Runtime environment variables are injected into the Lambda function at deploy time. For sensitive values, store them in AWS Secrets Manager and reference them by ARN — Thunder fetches and injects them automatically.
const config: AstroProps = { // ... serverProps: { variables: [ { NODE_ENV: 'production' }, { PUBLIC_API_URL: 'https://api.example.com' }, ], secrets: [ { key: 'DATABASE_URL', resource: 'arn:aws:secretsmanager:us-east-1:123456789012:secret:/myapp/DATABASE_URL-abc123', }, ], },};Deploy
Build your Astro app first to generate the Lambda handler and static assets, then deploy with CDK.
bun run buildnpx cdk deploy --app "bunx tsx stack/prod.ts" --profile defaultnpm run buildnpx cdk deploy --app "npx tsx stack/prod.ts" --profile defaultpnpm run buildpnpm exec cdk deploy --app "pnpm exec tsx stack/prod.ts" --profile defaultAfter deployment, CDK outputs a CloudFront URL that serves both your SSR responses and static assets.