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
-
GitHub account and repository
-
A working Next.js 15 app
-
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.
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.