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 directlyand choose:CloudFrontFullAccessAmazonS3FullAccess
- 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.
