HomeBlogHow to Deploy a Next.js App to AWS: EC2, S3, and CloudFront
AWSNext.jsDeploymentCloudFrontDevOps

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.

SaaSInMinutes
8 min read

Why 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, or PriceClass_All for 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:

ServiceMonthly 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 certificateFree
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.

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