Deploy a Next.js static website on AWS
Deploying a Next.js static website on AWS using CloudFront (a low-latency Content Delivery Network) allows us to be very performant at a very low cost. Let's see how to do it.
For more clarity, I will provide Cloud Formation templates and a full Makefile containing build and deploy commands.
You can find a full example here: https://github.com/thomaswinckell/next-js-static-cloudfront-example.
Enable Next.js static exports
First, you have to add output: 'export'
(see documentation) in your next.config.js
file :
const nextConfig = {
output: 'export',
}
Now, when you will build your app, static files will be output in an ./out
folder.
Please note that Next.js might throw an error saying Error occurred prerendering page
when building your app.
This means that your app cannot be rendered statically (see documentation).
Deploy a certificate
For CloudFront to trust the ownership of our domain, we need to create a certificate. This certificate should be deployed in the us-east-1 zone.
Here is the CloudFormation template to deploy the certificate (see source) :
AWSTemplateFormatVersion: '2010-09-09'
Description: Deploy a certificate for CloudFront to trust the domain ownership
Parameters:
DomainName:
Type: String
Resources:
AcmCertificate:
Type: AWS::CertificateManager::Certificate
Properties:
DomainName: !Ref DomainName
DomainValidationOptions:
- DomainName: !Ref DomainName
ValidationDomain: !Ref DomainName
Outputs:
AcmCertificateArn:
Value: !Ref AcmCertificate
You can deploy this CloudFormation template using the command :
aws cloudformation deploy --template-file ./certificate.yaml \
--stack-name=my-stack-name-certificate \
--region=us-east-1 \
--parameter-overrides "DomainName=my-domain.com"
Deploy the main stack: S3 bucket and CloudFront distribution
We now need to deploy an S3 bucket and the CloudFront distribution.
We will also deploy a custom CloudFront cache policy that allows us to cache assets from 1 minute to 1 year to reduce the cost and improve performance.
Here is the CloudFormation template to deploy the main stack (see source) :
AWSTemplateFormatVersion: '2010-09-09'
Description: Deploy a static website using S3 and CloudFront
Parameters:
DomainName:
Type: String
AcmCertificate:
Type: String
Resources:
# The S3 bucket that will receive the app static files
NextBucket:
Type: AWS::S3::Bucket
# Origin access identity (OAI) that will be used by the policy to allow CloudFront to access the S3 bucket
NextOriginAccessIdentity:
Type: AWS::CloudFront::CloudFrontOriginAccessIdentity
Properties:
CloudFrontOriginAccessIdentityConfig:
Comment: Origin Access Identity for Next static resources in S3 bucket
# Policy allowing CloudFront to get objects from the S3 bucket
NextBucketPolicy:
Type: AWS::S3::BucketPolicy
Properties:
Bucket: !Ref NextBucket
PolicyDocument:
Id: next-s3-bucket-policy
Version: 2012-10-17
Statement:
- Action:
- s3:GetObject
Effect: Allow
Principal:
CanonicalUser: !GetAtt NextOriginAccessIdentity.S3CanonicalUserId
Resource: !Sub arn:aws:s3:::${NextBucket}/*
# A custom cache policy for our static files : minimum 1 minute of caching, max 1 year
NextServerCachePolicy:
Type: AWS::CloudFront::CachePolicy
Properties:
CachePolicyConfig:
Name: next-cf-cache-policy
DefaultTTL: 60 # 1 minute
MinTTL: 60 # 1 minute
MaxTTL: 31536000 # one year
ParametersInCacheKeyAndForwardedToOrigin:
CookiesConfig:
CookieBehavior: none
HeadersConfig:
HeaderBehavior: none
QueryStringsConfig:
QueryStringBehavior: all
EnableAcceptEncodingBrotli: true
EnableAcceptEncodingGzip: true
# The CloudFront Distribution
Distribution:
Type: AWS::CloudFront::Distribution
DependsOn:
- NextBucket
- NextOriginAccessIdentity
Properties:
DistributionConfig:
Origins:
- Id: next-s3-origin
DomainName: !GetAtt NextBucket.RegionalDomainName
S3OriginConfig:
OriginAccessIdentity: !Sub origin-access-identity/cloudfront/${NextOriginAccessIdentity}
Enabled: true
HttpVersion: http2and3
DefaultRootObject: 'index'
CustomErrorResponses:
# when there's a 403 error in the S3 origin, it means that the file is not found
- ErrorCode: 403
ResponseCode: 404
ResponsePagePath: /404
DefaultCacheBehavior:
TargetOriginId: next-s3-origin
CachePolicyId: !Ref NextServerCachePolicy # we're using our custom cache policy
ForwardedValues:
QueryString: false
Cookies:
Forward: none
Compress: true
AllowedMethods:
- GET
- HEAD
ViewerProtocolPolicy: redirect-to-https
PriceClass: PriceClass_All
ViewerCertificate:
AcmCertificateArn: !Ref AcmCertificate
SslSupportMethod: sni-only
Aliases:
- !Ref DomainName
# Alias from our domain name to the CloudFront distribution
DNSName:
Type: AWS::Route53::RecordSetGroup
Properties:
HostedZoneName: !Join ['', [!Ref DomainName, .]]
RecordSets:
- Name: !Ref DomainName
Type: A
AliasTarget:
HostedZoneId: Z2FDTNDATAQYW2 # see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-route53-aliastarget-1.html#cfn-route53-aliastarget-hostedzoneid
DNSName: !GetAtt Distribution.DomainName
Outputs:
NextBucket:
Value: !Ref NextBucket
DistributionId:
Value: !Ref Distribution
You can deploy this CloudFormation template using the command :
aws cloudformation deploy --template-file ./template.yaml \
--stack-name my-stack-name \
--region=eu-west-1 \
--parameter-overrides "DomainName=my-domain.com" "AcmCertificate=my-certificate-arn"
Upload static files to S3
Now that we have deployed everything, we need to build our app and upload the static files to the S3 bucket.
Build
If you haven't done it already, you need to install and build your app :
npm install
npm run build
Static files will be generated in the ./out
folder.
Upload _next folder
First, we need to upload the _next
folder containing assets. All those assets have a unique hash in their name
(if the file changes, the hash changes) so we can set the maximum value for the cache: 1 year.
aws s3 sync ./out/_next s3://my-bucket/_next/ \
--metadata-directive REPLACE \
--cache-control max-age=31536000,public
Upload metadata files and others except html files
Then, we need to upload all metadata files and others except html files. Those files can change between builds so we should set no cache (the minimum cache from our CloudFront policy will apply: 1 minute).
aws s3 cp ./out s3://my-bucket/ \
--recursive \
--exclude "_next/*" \
--exclude "*.html" \
--metadata-directive REPLACE \
--cache-control max-age=0,no-cache,no-store,must-revalidate
Upload html files
Finally, we need to upload html files, also with no cache.
This part is more tricky as we don't want to have the .html
extension in our urls.
So using bash, we find every html files and copy them one by one, removing the .html
extension in the destination bucket.
cd ./out && find * -type f -name "*.html" -exec sh -c 'aws s3 cp "./$$0" s3://my-bucket/"$${0%.html}" --metadata-directive REPLACE --content-type text/html --cache-control max-age=0,no-cache,no-store,must-revalidate' {} \;
We're done! Check my Makefile!
Now at this point, everything should be done and your website should be fully functional!
Check the Makefile for a cleaner approach to running the build, deploy and upload commands.
To deploy everything from scratch, you should change the variables on top of the Makefile and then you can run the commands in this order :
make build
make deploy-certificate
make deploy
make upload-s3