Hosting a Next.js (App Router) app on Amazon S3
In this tutorial, we'll walk through the steps to deploy a Next.js App Router app on Amazon S3. We'll start by creating and properly configuring an S3 bucket with its CloudFront distribution, then we'll configure Next.js to export a static build. In a future tutorial, we'll explore how to create a deployment pipeline for automatic deployments on every git push, using AWS CodePipeline.
(This website is built with Next.js 15 App Router, exported to a static build, and hosted on Amazon S3.)
Prerequisites
-
You need an AWS account; there is no charge for the resources we'll create, but you'll be billed for usage when exceeding the free tier limits. To complete the tutorial, you'll need a custom domain (registered on Amazon Route 53 or any other DNS provider) and an ACM certificate associated with it; see the AWS documentation for more information on obtaining a certificate.
-
You will also need the AWS CLI installed and configured with your credentials. See the AWS CLI documentation for more information, and AWS CLI SSO configuration if you need help setting up your account.
-
This tutorial is for Next.js 14, Next.js 15 and assumes you're using the App Router. If you don't have a Next.js App Router project, you can create one by running
npx create-next-app@latest
. When prompted, always choose the default suggested options.
Since Amazon S3 can only host static contents, we will start by understanding what a static site is and which features are supported.
Understanding what's a static site
A static site is a website where the code is generated at build time. This means that the HTML is generated before the user requests it, and the same HTML is served for every request (i.e, it doesn't need to be generated on-demand based on cookies, user sessions, etc.).
Things you can have in a static site:
- Client components, including
useState
, animations, and everything you could do with old vanilla React 18 / Next.js 13 - Client components performing client-side network requests (e.g. fetch data from an external API)
- Server components that can be generated at build time (e.g. a blog post page that fetches the post content from a CMS when deploying)
When you run next build to generate a static export, Server Components consumed inside the app directory will run during the build, similar to traditional static-site generation. The resulting component will be rendered into static HTML for the initial page load and a static payload for client navigation between routes.
Things that are not supported when exporting to a static site:
- Dynamic routes (there's a way to make dynamic routes work, see next.js docs for generateStaticParams)
- Cookies, headers
- Middleware, Rewrites, Redirects
- Server actions
Now that you have decided if you can go with a static site, let's start by creating the infrastructure!
Infrastructure overview
We'll create the following resources on AWS; in another tutorial, we'll see how to create this infrastructure with Infrastructure-as-Code using AWS CDK. For now, let's see how to do it manually.
Here's an outline of what each service will do:
- Amazon CloudFront: The CDN that will serve our static files. Using CloudFront will allow us to use a custom domain name and configure an SSL certificate to serve our site over HTTPS.
- Amazon S3: The storage service that will host our static files. We'll need to enable S3 Static Website Hosting so that our files are served as a website (i.e.
index.html
is served when you navigate to the root URL, and subfolders are served as well). - AWS Certificate Manager (ACM): The service to request and manage our SSL/TLS certificates.
- Amazon Route 53: The DNS service that will route our custom domain name to our CloudFront distribution.
- AWS CodePipeline: The service to create a pipeline that will automatically deploy our changes to S3 when we push to our Git repository.
Update the Next.js configuration
We need to update the Next.js configuration to export a static build, and to build it in a way that is compatible with S3 Static Website Hosting.
Locate your next.config.js
(or next.config.mjs
, next.config.ts
) file and add the following properties:
{
// ... existing configuration
images: {
// Disable Next.js optimizations for images (it's not available for static sites
// and will throw an error)
unoptimized: true,
},
// This tells Next.js to export a static build to the `out` folder
output: "export",
// This tells Next.js to export pages as "folders with an `index.html` file inside"
// We use this option so we can avoid having the `.html` extension at the end of the page URLs.
trailingSlash: true,
}
Try running next build
and see if it works. Errors are generally self-explanatory, but if you have any issues, please review the checklist above ("Things that are not supported when exporting to a static site").
If the build succeeds, you should see a new folder called out
with the static build of your site. It will contain the index.html
file for each page, as well as any other assets like images, stylesheets, and all the js files.
Create the infrastructure
Create the S3 bucket
Navigate to the S3 console, click on Create bucket, and fill in the following fields:
- Region: Select the region you want to host your site in (it doesn't really matter since we'll be using CloudFront to serve the files from the closest edge location). You can change the region from the top-right dropdown.
- Bucket name: Your domain name (e.g.
allthingsserverless.com
); this doesn't really matter, actually, you can name it whatever you want. - Object ownership: Leave it as ACLs disabled.
- Block all public access: Uncheck this box - your website will need to be public.
- Acknowledge the warning about the bucket public access.
- Click on Create bucket.
Enable static website hosting
The next step is to enable static website hosting for the bucket. This setting is required so that S3 knows how to serve the files as a website (i.e. index.html
is served when you navigate to the root URL, and subfolders are served as well).
Once the bucket is created, click on it to open the bucket settings page, then:
- Enter the Properties tab.
- Scroll down to the bottom of the page and find the Static website hosting section.
- Choose Edit
- Check the Enable box under Static website hosting
- Confirm Hosting type is set to Host a static website
- In Index document, enter
index.html
- In Error document, enter
index.html
- Click on Save changes
Allow public read access to the bucket contents
We now need to allow public read access to the bucket contents so that CloudFront can serve the files via the Website Hosting feature.
- Go back to the bucket settings page, click on the Permissions tab, and find the Bucket policy section.
- Click on Edit
- Replace the content of the policy with the following:
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::your-domain-name/*"
}]
}
- Make sure to replace
your-domain-name
with your actual domain name or bucket name; don't remove the/*
at the end as we need it to allow access to all objects in the bucket - Click on Save changes
Create the CloudFront distribution
Navigate to the CloudFront console (Option+S, CloudFront), click on Create distribution, and fill in the following fields:
- As Origin domain, select the bucket we created in the previous step.
- A warning will tell you you should use the website endpoint; accept the warning by clicking on Use website endpoint
- Find the Viewer protocol policy section, and set it to Redirect HTTP to HTTPS to make sure your site is only served over HTTPS
- Scroll down to the Web Application Firewall (WAF) section, and select Do not enable security protections; we don't need them for our simple static site
- Before confirming the distribution creation, continue to the next paragraph.
Attach a custom domain
Find the Settings section towards the bottom of the page:
- Under Alternate domain name (CNAME), press Add item and enter your domain name (e.g.
allthingsserverless.com
) - Repeat the process for the Alternate domain name (CNAME) section, this time entering
www.allthingsserverless.com
(with the www prefix) - Select the SSL certificate. Only certificates compatible with your domain name will be shown in the dropdown. If you don't have a certificate, see these instructions on obtaining one.
- Optionally enable HTTP/3
- Click on Create distribution
Take note of the Distribution ID; we'll need it later, it's a string like E3QEPN1YNJOOW8
.
Creating the distribution will take approximately 5 minutes. While waiting for it to be created, you can continue to the next section. Until then, you will see a DNS resolution error.
Update the DNS settings
If you're using Route 53, follow these instructions:
- Navigate to the Route 53 console, click on Hosted zones, and select your domain name.
- Click on the Create record button.
- Set the record type to A.
- Check the Alias option.
- In the Route traffic to field, select Alias to a CloudFront distribution.
- From the Choose distribution dropdown, select the distribution we need to connect our domain name to. Only distributions with a compatible custom domain name will be shown in the dropdown.
- Click on Create records.
If you're not using Route 53:
Please refer to your DNS provider's documentation for more information. You probably need to create a CNAME record having the DNS name of the CloudFront distribution as its target/value. You can find the DNS name of the distribution in the CloudFront console, under the Distribution Settings section.
Upload the static files to S3
Now that we have created the infrastructure and configured our Next.js 15 static site, we need to upload it.
For this tutorial, we'll use a simple script to upload the static files to S3 and invalidate the CloudFront cache. In a later tutorial, we'll see how to create a deployment pipeline for automatic deployments on every git push, using AWS CodePipeline.
You'll need the AWS Command Line Interface. Install the AWS CLI if you don't have it already by following the Installing the AWS CLI v2 guide. You also need to configure the AWS CLI with your credentials; this depends on how your AWS account and organization is set up. I acknowledge IAM can be quite complex to set up, especially if you're not used to it, so I recommend you follow this comprehensive guide by AWS: https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-sso.html.
Once you have the AWS CLI installed and configured, create the following script (I name it deploy.sh
):
#! /bin/bash -e
# Build (next static)
pnpm run build
# Copy to S3
aws s3 sync ./out s3://YOUR-DOMAIN-NAME --delete
# Invalidate CloudFront cache
aws cloudfront create-invalidation --distribution-id YOUR_DISTRIBUTION_ID_COPIED_FROM_PREVIOUS_STEP --paths "/*"
Here's an explanation of what each command does:
pnpm run build
: Runsnext build
to generate the static filesaws s3 sync ./out s3://YOUR-DOMAIN-NAME --delete
: Copies the static files to the S3 bucket, deleting any files that no longer exist in theout
folderaws cloudfront create-invalidation --distribution-id YOUR_DISTRIBUTION_ID_COPIED_FROM_PREVIOUS_STEP --paths "/*"
: Invalidates the CloudFront cache so that the new files are served (optional)
Now make the file executable by running chmod +x deploy.sh
.
You can deploy your changes by running ./deploy.sh
.
Your changes will be published within a few seconds. You can run the script again to publish a new version every time you make changes to your site.
Conclusion
You did it! You are now hosting a Next.js 15 App Router static site on Amazon S3. It's fully serverless:
- it will cost you nothing if no one visits your site
- it will cost you a few cents per month if your site gets visitors
- never goes down
Remember S3 charges for Storage, Requests and Data Transfer, and CloudFront charges for Requests and Data Transfer. You can use the AWS Calculator to estimate your costs once your free tier usage is exceeded.