Deploy Astro on AWS

The web framework for content-driven websites with islands architecture.

astro.build

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

Terminal window
bunx create-astro@latest my-astro-app
cd my-astro-app

Install Thunder

Add Thunder as a development dependency. It provides the CDK constructs you’ll use to define your AWS infrastructure.

Terminal window
bun add @thunder-so/thunder --development

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

astro.config.mjs
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.

stack/prod.ts
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.

Terminal window
bun run build
npx cdk deploy --app "bunx tsx stack/prod.ts" --profile default

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

Terminal window
bun add @astrojs/node
astro.config.mjs
import { 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.

stack/prod.ts
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.

Dockerfile
FROM public.ecr.aws/docker/library/node:22-alpine AS builder
WORKDIR /app
COPY package.json bun.lockb ./
RUN curl -fsSL https://bun.sh/install | bash && export PATH="$HOME/.bun/bin:$PATH"
RUN bun install --frozen-lockfile
COPY . .
RUN bun run build
FROM public.ecr.aws/docker/library/node:22-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV HOST=0.0.0.0
ENV PORT=4321
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./
EXPOSE 4321
CMD ["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.

stack/prod.ts
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.

Terminal window
npx cdk deploy --app "bunx tsx stack/prod.ts" --profile default

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

Terminal window
bun add @astro-aws/adapter

Configure 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/.

astro.config.mjs
import { defineConfig } from 'astro/config';
// @ts-ignore package does not provide types
import 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.

stack/prod.ts
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.

stack/prod.ts
const config: AstroProps = {
// ...
serverProps: {
dockerFile: 'Dockerfile',
memorySize: 2048,
},
};

Dockerfile

Dockerfile
FROM public.ecr.aws/lambda/nodejs:22
# Copy all lambda files
COPY . ./
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.

stack/prod.ts
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.

Terminal window
bun run build
npx cdk deploy --app "bunx tsx stack/prod.ts" --profile default

After deployment, CDK outputs a CloudFront URL that serves both your SSR responses and static assets.