Deploying a Static Next.js App to AWS S3 and CloudFront Using GitHub Actions

Harshit BansalHarshit Bansal
4 min read

In the previous blog, we explored how to use the output: "export" setting in Next.js to generate a static version of your app. This approach is ideal when you want to deploy your application to platforms that serve static files, such as AWS S3.

In this post, we’ll take the next step: deploying your exported static Next.js app to Amazon S3 behind a CloudFront CDN, using GitHub Actions for continuous deployment.

Why This Approach?

With output: "export", Next.js generates plain HTML, CSS, and JS files — no server needed. Hosting these files on S3 gives you scalable, low-cost storage, and serving them via CloudFront ensures fast delivery worldwide.

This works with both the App Router and the Pages Router, as long as your app is fully static (no server-side rendering or ISR).

Prerequisites

Before you begin, ensure you have the following:

  • A Next.js app with output: "export" configured

  • An S3 bucket for hosting

  • A CloudFront distribution

  • An IAM role that GitHub can assume (OIDC enabled)

  • GitHub repository with Actions enabled


Step 1: Configure next.config.js

Update your next.config.js file like this:

// next.config.js
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  output: "export",
  trailingSlash: true,
};

export default nextConfig;

output: "export" tells Next.js to statically export all pages.


Step 2: Build and Preview Locally

Run the following:

npm run build

The static site will be output to the out/ directory.

To preview locally:

npx serve out

Step 3: Set Up S3 Bucket

  1. Go to the AWS S3 Console.

  2. Create a new bucket (e.g., my-static-nextjs-app).

  3. Keep it private.

  4. Do not enable public access or static website hosting.

You will serve the site via CloudFront, which securely accesses this private bucket.


Step 4: Secure Access with CloudFront

There are two ways to grant CloudFront access to your private S3 bucket:

Option A: Using Origin Access Identity (OAI)

  1. When creating your CloudFront distribution, enable OAI.

  2. In your S3 bucket Permissions > Bucket Policy, add:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowCloudFrontReadAccess",
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity YOUR_OAI_ID"
      },
      "Action": "s3:GetObject",
      "Resource": "arn:aws:s3:::YOUR_BUCKET_NAME/*"
    }
  ]
}

Replace:

  • YOUR_OAI_ID with your CloudFront OAI ID

  • YOUR_BUCKET_NAME with your actual bucket name

  1. Create a new CloudFront OAC

  2. Attach it to your distribution

  3. AWS will manage the bucket policy automatically


Step 5: Set Up GitHub Actions for Deployment

Create .github/workflows/deploy.yml in your repo:

name: deploy

on:
  push:
    branches:
      - main

env:
  AWS_REGION: ap-south-1

permissions:
  id-token: write
  contents: read

jobs:
  build:
    runs-on: ubuntu-latest

    strategy:
      matrix:
        node-version: [18.x]

    steps:
      - name: Checkout code
        uses: actions/checkout@v3

      - name: Install dependencies
        run: npm ci

      - name: Build static site
        run: npm run build

      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v2
        with:
          role-to-assume: arn:aws:iam::YOUR_ACCOUNT_ID:role/YOUR_GITHUB_DEPLOY_ROLE
          role-session-name: GitHubActionsSession
          aws-region: ${{ env.AWS_REGION }}

      - name: Deploy to S3
        run: aws s3 sync out s3://YOUR_BUCKET_NAME --delete

      - name: Invalidate CloudFront cache
        run: aws cloudfront create-invalidation --distribution-id YOUR_DISTRIBUTION_ID --paths '/*'

Replace the placeholders:

  • YOUR_ACCOUNT_ID

  • YOUR_GITHUB_DEPLOY_ROLE

  • YOUR_BUCKET_NAME

  • YOUR_DISTRIBUTION_ID

Your IAM role should have:

{
  "Effect": "Allow",
  "Action": [
    "s3:*",
    "cloudfront:CreateInvalidation"
  ],
  "Resource": "*"
}

Make sure this role is trusted for GitHub OIDC authentication.


Step 6: Done! Push to Deploy

Push to the main branch and watch your app deploy automatically:

  1. npm run build generates the static site

  2. Files are uploaded to your S3 bucket

  3. CloudFront cache is invalidated so users see the latest version


Demo Repository

You can find the complete working example of this setup on GitHub:

🔗 GitHub Repository: https://github.com/harshitbansall/blog-demo-nextjs-client-apps

🔗 Deployment: https://d2tzkcpgro4cmj.cloudfront.net

This repository contains:

  • A minimal Next.js app configured with output: 'export'

  • A GitHub Actions workflow to deploy to S3 and invalidate CloudFront cache

  • Sample IAM policy and trust relationship for GitHub OIDC

  • Instructions to reproduce the deployment from scratch

Notes

  • Ensure your Next.js app is fully static — no API routes, dynamic rendering, or server functions.

  • If you're using dynamic routes with generateStaticParams, make sure all necessary paths are statically generated at build time.

  • trailingSlash: true is required to map S3 folder-style routes correctly.

Conclusion

By combining the power of static exports in Next.js with S3 and CloudFront, you get a fast, reliable, and cost-effective way to deploy your frontend. With GitHub Actions, your deployments are now fully automated.

This architecture is especially useful for marketing sites, documentation, or SPAs — offering global performance without any servers to manage.

0
Subscribe to my newsletter

Read articles from Harshit Bansal directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Harshit Bansal
Harshit Bansal