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