ITUGUI
Deploy Next.js Static Site to AWS S3 & CloudFront with CI/CD

Deploy Next.js Static Site to AWS S3 & CloudFront with CI/CD

5 min
CI/CDNext.JSGitHub ActionsAWSS3CloudFrontDevOpsautomationcloud-deployment

In this guide, you’ll learn how to host a statically exported Next.js 15 app on AWS S3 and serve it via CloudFront—fully automated using GitHub Actions.

Prerequisites

  1. GitHub account and repository

  2. A working Next.js 15 app

  3. AWS account with:

    • An S3 bucket
    • A CloudFront distribution -An IAM user with the right permissions

What is Next.js Static Export?

Next.js static export enables you to pre-render your app into static HTML files. Using static export in Next.js generates pre-rendered HTML files for each page during build time.

Without static export, pages are typically server-side rendered, meaning HTML is generated on-the-fly for each request.

For experienced users, skip to the deployment section.

To create static pages with Nextjs you’ll need to configure the next.config.mjs as follows:

import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  output: "export",
  trailingSlash: false,
  //...
};

export default nextConfig;

Configure S3 bucket

Set the name for the bucket as the domain name - not mandatory, but it’s useful when you have multiple sites to run and manage.

Bucket Settings:

  • Block all public access (Permission Tab)

  • Create a policy to only allow access to bucket from CloudFront:

    {
    "Version": "2008-10-17",
    "Id": "PolicyForCloudFrontPrivateContent",
    "Statement": [
        {
            "Sid": "AllowCloudFrontServicePrincipal",
            "Effect": "Allow",
            "Principal": {
                "Service": "cloudfront.amazonaws.com"
            },
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::$BUCKET_NAME/*",
            "Condition": {
                "StringEquals": {
                    "AWS:SourceArn": "arn:aws:cloudfront::$DISTRIBUTION"
                }
            }
        }
    ]}

Do not enable Static website hosting if you’re using OAC (Origin Access Control). Instead, keep the bucket private and allow access via CloudFront only.

Configure CloudFront Distribution

Amazon CloudFront is a content delivery network (CDN). CloudFront accelerates the delivery of static and dynamic web content by caching it at numerous edge locations worldwide.

Create a distribution, generate also a SSL Certificate to have HTTPS for your site.

Create an Origin as follows:

  • For origin domain : choose your bucket.
  • Origin path: leave empty
  • Origin Access: Use Origin access control settings (recommended)
  • Create new OAC: Sign requests (recommended)

Configure IAM User

To create a User search for IAM then go to Users from Access management section and create user.

  • Give it a name, eg: frontend-automation
  • Attach policies directly and choose:
    • CloudFrontFullAccess
    • AmazonS3FullAccess
  • Create Access Key

Here is the final configuration that you will have. For production setups, limit the IAM user’s access using the Principle of Least Privilege.

Configuring IAM User

Deploying the App from DEV machine

If you are a solo dev, building a pet project, you can get away with these commands:

npm run build
aws configure
aws s3 sync out/ s3://$BUCKET_NAME/ --delete
aws cloudfront create-invalidation --distribution-id $DISTRIBUTION_ID --paths "/*"

Deploying the App from Github Actions

Go to the actions tab and create a new workflow. We need to add the following secrets (Settings -> Secrets and variables -> Actions):

  • AWS_ACCESS_KEY_ID
  • AWS_S3_BUCKET
  • AWS_SECRET_ACCESS_KEY
  • CLOUDFRONT_DISTRIBUTION_ID

Here is the code for the Github Action that is deploying the static content to AWS S3 Bucket, followed by a CloudFront cache invalidation to ensure that the users are receiving the latest content update.

I’ve split the workflow into three stages (jobs): build - nextjs generates the static pages, release - pushing into S3 and finally cache-invalidation - invalidate the CloudFront cache.

on:
  push:
    branches: ["master"]
  workflow_dispatch:

permissions:
    contents: read
    pages: write
    id-token: write
jobs: 
    build:
      runs-on: ubuntu-latest
      outputs:
        runName: ${{ steps.setRunName.outputs.runName }}
      permissions:
        contents: read
        packages: write
      defaults:
        run:
          working-directory: ./
      steps:
        - name: Checkout code
          uses: actions/checkout@v3
        - name: Use Node.js 22
          uses: actions/setup-node@v4
          with:
            node-version: 22
        - name: npm install
          run: npm install
        - name: next build
          run: npx next build
        - name: 'Upload Artifact'
          uses: actions/upload-artifact@v4
          with:
            name: app
            path: ./out
            if-no-files-found: error
            retention-days: 1
    release:
      needs: build
      runs-on: ubuntu-latest
      environment: Production
      steps:
        - name: Download build artifact
          uses: actions/download-artifact@v4
          with:
            name: app
        - name: Show files
          run: ls
        - name: Deploy to S3
          uses: jakejarvis/s3-sync-action@master
          with:
            args: --delete
          env:
            AWS_S3_BUCKET: ${{ secrets.AWS_S3_BUCKET }}
            AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
            AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
            AWS_REGION: 'eu-central-1'
            SOURCE_DIR: '.'
    cache-invalidation:
      needs: release
      runs-on: ubuntu-latest
      environment: Production
      steps:
        - name: Invalidate CloudFront
          uses: chetan/invalidate-cloudfront-action@v2
          env:
            DISTRIBUTION: ${{ secrets.CLOUDFRONT_DISTRIBUTION_ID }}
            PATHS: "/*"
            AWS_REGION: "eu-central-1"
            AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
            AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}

And there you have it! You’ve successfully built a robust, automated CI/CD pipeline for your Next.js static pages using GitHub Actions, AWS S3, and CloudFront. You’ve eliminated the manual steps of uploading files and invalidating the CloudFront distribution.

If you have a backend also can read further Deploy Docker compose to EC2.

If you have any questions or got stuck, reach out.

References