How to Deploy a Next.js App to AWS: EC2, S3, and CloudFront
A complete guide to deploying your Next.js application on AWS with S3 static hosting, CloudFront CDN, and EC2 for server-side features. Covers SSL, cache invalidation, and cost optimization.
On this page
- Why AWS Over Vercel?
- Architecture Overview
- Step 1: Build Your Next.js App for Static Export
- Step 2: Create an S3 Bucket
- Via AWS CLI
- Bucket Policy for CloudFront
- Step 3: Upload Your Build
- Step 4: Set Up CloudFront
- Create a Distribution
- Key Settings
- Handle Client-Side Routing
- Step 5: SSL Certificate with ACM
- Step 6: DNS Configuration
- Step 7: Automated Deployments
- Cost Breakdown
- When You Need EC2
- Related Guides
- The One-Command Alternative
Launch your SaaS in 30 minutes with production-ready auth, payments, monitoring, and deployment. $49 one-time.
Get Instant AccessWhy AWS Over Vercel?
Vercel is the default choice for Next.js deployment, and for good reason — it's simple. Push to git, and your app is live.
But Vercel's simplicity comes with trade-offs:
- Pricing: $20/month per team member on Pro. For a 3-person team, that's $60/month before you write a line of code.
- Vendor lock-in: Vercel-specific features (Edge Functions, ISR) tie you to their platform.
- Limited server access: No SSH, no custom server config, no background processes.
- Bandwidth costs: Overages can surprise you. AWS pricing is predictable.
AWS is more work to set up, but you get full control over your infrastructure at a fraction of the cost. If deployment is the last piece you need before launch, our 30-minute SaaS launch guide covers the rest of the stack — auth, payments, monitoring — that pairs with this AWS setup.
Architecture Overview
For a static Next.js export (which covers most SaaS landing pages and dashboards):
User → CloudFront (CDN) → S3 (Static Files)
↓
ACM (SSL Certificate)
↓
Route 53 (DNS)
For apps that need server-side rendering:
User → CloudFront → ALB → EC2 (Next.js Server)
↓
S3 (Static Assets)
This guide covers the static export approach, which is simpler and cheaper.
Step 1: Build Your Next.js App for Static Export
Add the export configuration to your next.config.js:
/** @type {import('next').NextConfig} */
const nextConfig = {
output: 'export',
trailingSlash: true,
images: {
unoptimized: true,
},
};
module.exports = nextConfig;
Then build:
npm run build
This generates an out/ directory with your entire app as static HTML.
Step 2: Create an S3 Bucket
Via AWS CLI
# Create the bucket
aws s3 mb s3://your-saas-app --region us-east-1
# Enable static website hosting
aws s3 website s3://your-saas-app \
--index-document index.html \
--error-document 404/index.html
Bucket Policy for CloudFront
Don't make the bucket public. Instead, create an Origin Access Control (OAC) in CloudFront and use a bucket policy that only allows CloudFront to read:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowCloudFrontServicePrincipal",
"Effect": "Allow",
"Principal": {
"Service": "cloudfront.amazonaws.com"
},
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::your-saas-app/*",
"Condition": {
"StringEquals": {
"AWS:SourceArn": "arn:aws:cloudfront::YOUR_ACCOUNT_ID:distribution/YOUR_DISTRIBUTION_ID"
}
}
}
]
}
Step 3: Upload Your Build
# Sync the build output to S3
aws s3 sync out/ s3://your-saas-app \
--delete \
--cache-control "public, max-age=31536000, immutable"
# Override cache headers for HTML files (should revalidate)
aws s3 sync out/ s3://your-saas-app \
--exclude "*" \
--include "*.html" \
--cache-control "public, max-age=0, must-revalidate"
The cache strategy matters: static assets (JS, CSS, images) get year-long caching because Next.js content-hashes their filenames. HTML files get no caching so users always see the latest content.
Step 4: Set Up CloudFront
Create a Distribution
aws cloudfront create-distribution \
--origin-domain-name your-saas-app.s3.us-east-1.amazonaws.com \
--default-root-object index.html
Key Settings
- Price Class: Use
PriceClass_100(US + Europe) to save costs, orPriceClass_Allfor global CDN - Viewer Protocol Policy: Redirect HTTP to HTTPS
- Compress Objects: Enable for Gzip and Brotli compression
- Default TTL: 86400 (24 hours)
- Error Pages: Map 403/404 to your custom 404 page
Handle Client-Side Routing
Next.js with trailingSlash: true generates directories with index.html files. CloudFront needs a function to handle this:
// CloudFront Function for clean URLs
function handler(event) {
var request = event.request;
var uri = request.uri;
// If URI ends with '/' append index.html
if (uri.endsWith('/')) {
request.uri += 'index.html';
}
// If URI doesn't have an extension, add /index.html
else if (!uri.includes('.')) {
request.uri += '/index.html';
}
return request;
}
Attach this as a Viewer Request function to your CloudFront distribution.
Step 5: SSL Certificate with ACM
# Request a certificate (must be in us-east-1 for CloudFront)
aws acm request-certificate \
--domain-name yoursaas.com \
--subject-alternative-names "*.yoursaas.com" \
--validation-method DNS \
--region us-east-1
ACM will give you a CNAME record to add to your DNS for validation. Once validated, attach the certificate to your CloudFront distribution.
Step 6: DNS Configuration
If you're using Route 53:
# Create an alias record pointing to CloudFront
aws route53 change-resource-record-sets \
--hosted-zone-id YOUR_ZONE_ID \
--change-batch '{
"Changes": [{
"Action": "CREATE",
"ResourceRecordSet": {
"Name": "yoursaas.com",
"Type": "A",
"AliasTarget": {
"HostedZoneId": "Z2FDTNDATAQYW2",
"DNSName": "d1234567890.cloudfront.net",
"EvaluateTargetHealth": false
}
}
}]
}'
If you're using another DNS provider (Cloudflare, Namecheap), create a CNAME record pointing to your CloudFront distribution domain.
Step 7: Automated Deployments
Create a deployment script that builds, uploads, and invalidates the CDN cache:
#!/bin/bash
set -e
echo "Building Next.js app..."
npm run build
echo "Uploading to S3..."
aws s3 sync out/ s3://your-saas-app --delete
# Set proper cache headers
aws s3 sync out/ s3://your-saas-app \
--exclude "*.html" \
--cache-control "public, max-age=31536000, immutable"
aws s3 sync out/ s3://your-saas-app \
--exclude "*" --include "*.html" \
--cache-control "public, max-age=0, must-revalidate"
echo "Invalidating CloudFront cache..."
aws cloudfront create-invalidation \
--distribution-id YOUR_DISTRIBUTION_ID \
--paths "/*"
echo "Deployed successfully!"
Cost Breakdown
For a SaaS with ~50K monthly page views:
| Service | Monthly Cost |
|---|---|
| S3 storage (1GB) | $0.02 |
| S3 requests (100K) | $0.04 |
| CloudFront data (10GB) | Free tier |
| CloudFront requests (100K) | Free tier |
| Route 53 hosted zone | $0.50 |
| ACM certificate | Free |
| Total | ~$0.56/month |
Compare that to Vercel Pro at $20/month. Even at 10x the traffic, AWS stays under $5/month for static hosting.
Skip the AWS console clicking. SaaSInMinutes ships a one-command deploy script that handles S3, CloudFront, OAC, ACM, cache headers, and invalidation. $49 one-time, includes auth, payments, and monitoring. Get instant access →
When You Need EC2
Static export works for most SaaS products, but you'll need a server (EC2) if you require:
- Server-side rendering on every request
- API routes that run server-side logic
- WebSocket connections
- Background job processing
- Cron jobs
For a minimal EC2 setup:
# t3.micro is free tier eligible
# 2 vCPU, 1 GB RAM — enough for moderate traffic
aws ec2 run-instances \
--image-id ami-0c55b159cbfafe1f0 \
--instance-type t3.micro \
--key-name your-key-pair \
--security-group-ids sg-xxxxxx
Install Node.js, clone your repo, and run:
npm install
npm run build
npm start
Use PM2 for process management and Nginx as a reverse proxy.
Related Guides
- Launch a SaaS in 30 Minutes with Next.js + Supabase — the full launch flow this deployment fits into
- Next.js + Supabase Authentication: The Complete Guide — auth wiring you'll deploy alongside this stack
- LemonSqueezy Payment Integration for Next.js — payments to deploy with your app
- Supabase RLS for Multi-Tenant SaaS — database security that goes live with deployment
- SaaS Boilerplate vs Building From Scratch — when manual AWS setup is worth it
The One-Command Alternative
Setting all of this up manually takes 2–4 hours the first time, and every new SaaS you launch repeats most of it. SaaSInMinutes ships a deployment script that handles everything — S3 bucket creation, CloudFront configuration with OAC, ACM SSL setup, cache header tuning, CloudFront function for clean URLs, and cache invalidation — in a single command:
bash scripts/deploy.sh
For $49 one-time, you also get auth, payments, email, monitoring, and the rest of the SaaS stack — not just deployment. Lifetime updates. Unlimited personal projects.
Your time is better spent on features than fighting AWS console screens.
Get instant access — $49 one-time →
Deploy once, configure never. Focus on building your product.
Written by SaaSInMinutes