Next.js on AWS: Deployment Strategies
RedRobot.dev
Posted on October 28, 2024
In this post we will explore the primary methods for deploying a Next.js app to AWS, discussing the pros and cons of each method.
Before proceeding, it's beneficial to know some of the core Next.js concepts such as SSR, SSG, Hybrid, Hydration and difference between SPA and MPA. If you're unsure what these are, you can learn more by reading the Next.js docs.
Original Post
Strategies
In this section, we'll discuss all major deployment strategies for Next.js and the key points to consider when selecting a deployment method in the context of AWS. We'll also highlight potential pitfalls and important points that may seem trivial but become significant in practice. Here are the list of strategies:
- (SSG + SPA) Static File Hosting
- (SSG + SSR + SPA) Containerization
- on EC2
- on ECS
- (SSG + SSR + SPA) Serverless
- Amplify Gen 1
- Amplify Gen 2
- SST
- OpenNext
Pure static generated website
SSG stands for Static Site Generation, which essentially means the content of the website is generated at build time.
This strategy focuses on generating static pages during the build or deployment stage of your Next.js application.
This method is contingent on knowing all the necessary data for constructing the pages at build or deployment time. This approach, pre-renders pages to static HTML, which can then be efficiently served to users. It's ideal for content that doesn't change frequently and when you can predict all possible page routes at build time.
At the end, you will have a folder with all the files generated, including the css, javascript and all other assets.
With the content generated, you can host the files in any regular HTML server for example nginx, apache http server, etc.
When you should deploy your website using SSG?
There are two scenarios where this approach is suitable:
If you are developing a SPA (Single Page Application), with all the rendering and data fetching done on the client side (browser).
If the website content is available at build time, even if it's in a database, and the content is under your control and not added, updated, or deleted by third-party users.
When not to use SSG?
If your website requires multi-user support such as user registration and login functionality, or the ability for users to add or modify content like blog posts or shopping carts and etc — you're dealing with dynamic content. These are scenarios where data is created and modified externally, and the rate and timing of such content changes are not under your direct control.
This is referred to Dynamic content, which essentially means the content can change after the website is built and deployed.
How to deploy
For a detail step-by-step guide on how to deploy your Next.js app using this strategy checkout the blog post static website deployment.
SSR or Server Side rendering
Server-side rendering (SSR) generates content on each request, and this strategy is primarily used for cases where the content is dynamic. And with this usually there is a form of content source (usually a database) available to the server to access and generate pages. In this approach:
- Data is fetched and collected on the server for each incoming request
- The server then generates the complete HTML page using this data
- The fully rendered page is sent to the client
For deploying server-side rendered Next.js applications, there are several approaches:
Containerization: which means creating a Docker image of the Next.js app and running it on EC2 (Elastic Compute Cloud) or ECS (Elastic Container Service). The ECS approach offers greater flexibility and scalability, but it also requires more configuration as it has more "moving parts".
Segmented Deployment: This method requires breaking down the Next.js app output into static and dynamic segments and deploying each type of content to appropriate resources such as S3 and Lambda. While this approach optimizes resource usage, it is more complex to set up manually as it requires a deeper understanding of Next.js internals.
When you should deploy your website using SSR?
This method of deployment is particularly useful when:
- Content needs to be up-to-date with each page load as data is dynamic or user-specific.
- SEO optimization is essential - This method works much better for SEO as search engines can easily index the fully rendered content, unlike with SPA applications.
- Page load time need to be fast - This approach can actually decrease page load time, especially in cases where the client browser is not very powerful. By leveraging the server to render pages, you offload processing from the client.
- In certain cases, this method would load the page faster, particularly if the database hosting the dynamic data is close to the server generating the pages.
- For security-critical cases where you have important content that you don't want to send over the wire or share with the world, server-side rendering offers a more secure environment instead of exposing data and IDs.
When not to use SSR?
While Server-Side Rendering (SSR) in Next.js offers a balance between dynamic content and performance, leveraging caching strategies and optimized rendering processes, it's important to note that it can be more resource-intensive than static generation. The server must process each request individually, potentially leading to higher server loads, increased costs, and slightly longer response times compared to serving pre-generated static pages.
Server-Side Rendering (SSR) requires running a process to render pages dynamically, which incurs additional costs. Therefore, it's best avoided if your content is static. SSR also introduces complexity with components like reverse proxies, databases, and various frontends' (admin and client-side). This complexity not only expands the potential attack surface but also increases development and maintenance costs. While SSR offers significant flexibility, these factors should be carefully weighed when considering its implementation. The decision to use SSR should be based on a thorough evaluation of your specific needs and resources.
Important
Choose your deployment strategy carefully based on your specific use case. For instance, if you have a blog website where you plan to update content yourself infrequently, there's no need for Server-Side Rendering (SSR). Instead, you can create a Static Site Generated (SSG) application with your content hosted locally. This approach is more efficient and cost-effective for websites with primarily static content that doesn't require frequent real-time updates.
Hybrid SSG + SSR + SPA
In the majority of cases, your website or app will likely contain a mix of dynamic and statically generated content, and may even include Single Page Application (SPA) elements. A good example is an e-commerce website:
- The landing page might be static
- The products page could be dynamic
- The admin interface might be an SPA
For such mixed-content scenarios, you'll typically run Next.js in production mode. In this mode, Next.js:
- Serves static files directly from directories
- Renders dynamic pages on-demand
- Caches data to optimize file serving
This process can be broken down into two main stages:
- Deployment Stage: Where pages are built and prepared
- Live Mode: Where the Next.js process is running, serving content, and managing caching and dynamic generation as needed
This approach allows for optimal performance and flexibility, catering to the varied content types within a single application. It leverages the strengths of static content for speed and dynamic rendering for up-to-date information, all within the Next.js framework.
Next.js provides mechanism to optimize serving static and dynamic by separating static and dynamic content into different parts however you will still be running the Next.js process to host the static content which increasing CPU time and cost. It might be better to leverage Segmented Deployment options for these types of websites.
AWS Deployment Options
1. Static File Hosting
Cloudfront + S3
This method is suitable for SSG website that host static content or SPA apps. Deploying a Next.js app using CloudFront and S3 offers a robust and scalable solution for static site hosting.
The benefits of this method are:
- excellent performance due to CloudFront's edge locations,
- high scalability
- cost-effectiveness for serving static content
As mentioned This method has limitations - It's primarily suitable for static exports, which may not support all Next.js features, particularly those requiring server-side rendering.
For deployment, mainly we need to utilize CloudFront as a Content Delivery Network (CDN), Route 53 for Domain Name System (DNS) management, AWS Certificate Manager for SSL certificate generation and management, and S3 buckets for storing and hosting the static content of your website. This approach creates a robust, scalable architecture where:
- CloudFront distributes your content globally, reducing latency for users
- Route 53 manages your domain and directs traffic to CloudFront
- Certificate Manager ensures secure HTTPS connections
- S3 acts as the origin server, storing your static files
For step-by-step guide on how to deploy such an app see static website deployment.
2. Containerization
EC2
This method comprises of creating an EC2 instance, it's simple and familiar as it's a VM so you will have full control over the server environment, customizable resources (CPU, RAM, storage), you can scale vertically by adding a more powerful instance and switching over the route forwarding, and flexibility to install and configure additional software, and it's low cost specially for high-traffic applications.
However, it requires higher complexity in setup and maintenance, you have to manually handle scaling and load balancing setup, and updating security patches and as far as cost it's might be more expensive to run for low traffic websites.
This would typically involve installing a reverse proxy (Nginx), a node process manager like (PM2) and then finally the node application.
ECS
Deploying a Next.js app using Amazon ECS offers several advantages, including simplified container orchestration, easier scaling and load balancing, improved resource utilization, consistent deployments across environments, and seamless integration with other AWS services.
However, it also comes with some drawbacks. These include a learning curve for container concepts and ECS specifics, potentially higher costs compared to EC2 for small-scale applications, less direct control over the underlying infrastructure, complexity in managing stateful applications, and possible performance overhead due to containerization. Despite these challenges, ECS can be an excellent choice for teams looking to leverage the benefits of containerization and streamlined deployment processes, especially as applications grow in scale and complexity.
As illustrated in the diagram, this method involves more services and careful configuration. It's important to note that the associated costs can be higher, particularly if the setup is not optimized. Therefore, careful planning and monitoring are essential to ensure cost-effectiveness while leveraging the advantages of containerization.
If you are interested in learning more checkout the following post "Next.js Deployment using ECS with Fargate" where we guide you through step-by-step describing how to achieve this.
3. Serverless
The serverless architecture for all of the mentioned solutions here will be similar to what this diagram shows:
However each have their own additional components, some are proprietary and some open source. The diagram represent in high level what the Serverless option would look like.
AWS Amplify Gen 1
Deploying Next.js using AWS Amplify offers a mix of advantages and potential drawbacks.
On the positive side, Amplify provides easy setup, integrated AWS services, automatic CI/CD pipelines, serverless architecture, and built-in hosting with CDN capabilities. With Amplify, you can point to your Next.js app from the console and it's up and running.
However, there is no direct CDK integration - you are sort of tied into the Amplify eco system and for teams requiring maximum control over their AWS architecture might find this abstraction layer constraining. The choice to use Amplify ultimately depends on the specific needs and circumstances of the project.
AWS Amplify Gen 2
Amplify Gen 2 represents a significant evolution from its predecessor, offering several key improvements tailored to modern web development needs. Unlike Gen 1, which primarily used Amazon S3 and CloudFront for hosting static sites, Gen 2 leverages AWS Lambda@Edge and CloudFront to provide a more flexible serverless architecture. This shift enables full server-side rendering (SSR) support, a feature that was limited in Gen 1. Gen 2 delivers enhanced performance and scalability, particularly for dynamic content, thanks to its serverless foundation. It also offers faster deployments, especially for incremental updates, and improved support for advanced frameworks like Next.js, including features such as API routes.
A notable advantage of Gen 2 is its direct CDK support, allowing developers to seamlessly extend or connect their Next.js deployments with existing architecture. While Gen 1 was more straightforward for simple static sites, Gen 2 provides an improved developer experience for full-stack applications, offering greater customization options and tighter integration with other AWS services. Although Gen 2 may have different cost implications compared to Gen 1's potentially lower costs for simple static sites, it can be more cost-effective for dynamic applications due to its serverless nature. Overall, Amplify Gen 2 is designed to better support the complexities of modern web applications, offering developers more power and flexibility in building and deploying sophisticated, scalable web solutions.
If you are interested in learning more checkout the following post "Next.js Deploying using AWS CDK & Amplify" where we guide you through a step-by-step process describing how deploy a SSR-enabled Next.js app using Amplify Gen 2.
SST
Similar to Amplify, SST (Serverless Stack Toolkit) is another interesting option for deploying Next.js applications, particularly if you're looking to leverage AWS services more directly. In addition to supporting Next.js app deployment, SST offers other abstractions that makes it easy to create a full app faster compared to writing CDK from scratch yourself.
SST uses AWS CDK under the hood, giving you the ability to define your infrastructure as code.
To deploy a Next.js app using SST, you create an SST project using npx create-sst@latest
and then configure the NextJs app using using regular CDK syntax:
import { NextjsSite, SSTConfig } from "sst/constructs"
export default {
config(_input) {
return {
name: "my-next-app",
region: "us-east-1",
}
},
stacks(app) {
app.stack(function Site({ stack }) {
const site = new NextjsSite(stack, "site")
stack.addOutputs({
SiteUrl: site.url,
})
})
},
} satisfies SSTConfig
Critical
Merging this into your own CDK code is not easy and it's error prone. While SST is much closer to what we want, since it used AWS CDK however - it has it's own eco system build on top of AWS CDK which forces you to use it.
OpenNext
OpenNext is a CDK (Cloud Development Kit) library that facilitates easy integration directly into your CDK project. This approach is particularly desirable in scenarios where:
- Your company's entire infrastructure is managed using CDK
- You aim to maintain uniformity across your infrastructure code
By using OpenNext, you can keep all of your infrastructure code, including your Next.js deployment configuration, within the same project. This consistency can lead to several benefits:
- Simplified management: All infrastructure code is in one place
- Easier version control: Changes to both application and infrastructure can be tracked together
- Streamlined deployment processes: You can use the same deployment pipeline for both your application and infrastructure
This method is especially advantageous for organizations that prioritize a cohesive and standardized approach to infrastructure management across all their projects and services.
We will be using cdk-nextjs-standalone which is a package that is built on top of OpenNext.
OpenNext takes the Next.js build output and converts it into packages that can be deployed across a variety of environments. Natively OpenNext has support for AWS Lambda and classic Node Server. It also offer partial support for the edge runtime in Cloudflare Workers.
Here is a wrapper construct that encapsulated cdk-nextjs-standalone (it's good practice to always create wrappers around 3rd party code).
import { CfnOutput, StackProps } from "aws-cdk-lib"
import { Construct } from "constructs"
import * as acm from "aws-cdk-lib/aws-certificatemanager"
import * as route53 from "aws-cdk-lib/aws-route53"
import { Nextjs } from "cdk-nextjs-standalone"
export interface NextJsServerlessDeploymentProps extends StackProps {
nextJsRootPath: string
domain: string
webUrl: string
hostedZone: route53.IHostedZone
certificate: acm.Certificate
}
export class NextJsServerlessDeployment extends Construct {
constructor(
scope: Construct,
id: string,
{
nextJsRootPath,
domain,
webUrl,
hostedZone,
certificate,
}: NextJsServerlessDeploymentProps
) {
super(scope, id)
const nextjs = new Nextjs(this, "Nextjs", {
nextjsPath: nextJsRootPath, // relative path from your project root to NextJS
domainProps: {
domainName: domain,
certificate,
hostedZone,
},
})
new CfnOutput(this, "CloudFrontDistributionDomain", {
value: nextjs.distribution.distributionDomain,
})
}
}
And this is how you would use it
import * as cdk from "aws-cdk-lib"
import { Construct } from "constructs"
import { CertificateWrapper, NextJsServerlessDeployment } from "../constructs"
import { getConfig } from "../helpers"
export class MainServiceStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props)
const config = getConfig()
const domain = config.domain
const webUrl = `${config.webSubdomain}.${domain}`
const cw = new CertificateWrapper(this, "certificateWrapper", {
domain,
webUrl,
})
new NextJsServerlessDeployment(this, "dynamicWebsiteDeploy", {
nextJsRootPath: "../frontend",
domain,
webUrl,
hostedZone: cw.zone,
certificate: cw.certificate,
})
}
}
None AWS options
Vercel
Using Vercel to deploy a Next.js app is much easier and streamlined, and reportedly runs faster than compared to the alternatives listed here. Vercel is the company behind Next.js and their platform is optimized for it. To upload you NextJs app to Vercel, it's as easy as creating a new project, pointing to your git repo where it holds the NextJs code
and Vercel will deploy everything for you.
Conclusion
In conclusion, deploying a Next.js application to AWS offers a variety of strategies, each with its own strengths and considerations. From static file hosting using CloudFront and S3 for simple static sites, to containerization on EC2 or ECS for more complex applications, to serverless options like AWS Amplify Gen 2, SST, and OpenNext for scalable and flexible deployments, developers have a wide range of choices.
The selection of the most appropriate deployment method depends on factors such as the application's complexity, scalability requirements, budget constraints, and the development team's expertise. While AWS provides robust solutions, it's worth noting that alternatives like Vercel offer streamlined deployment processes optimized specifically for Next.js.
How do you choose the method?
When choosing a deployment method for your Next.js application, consider your specific needs and constraints:
- For static content: If your pages are not dynamic, opt for a static deployment method. It's fast, cost-effective, and easily scalable using services like AWS S3 and CloudFront.
- For dynamic content outside AWS: If your content is dynamic and you're not committed to AWS, Vercel offers an optimized, streamlined deployment experience specifically designed for Next.js.
- For dynamic content within AWS: If you're already invested in AWS infrastructure and have dynamic content, your main choices are between container-based (EC2/ECS) or serverless solutions.
a. For maximum control: Choose EC2 or ECS. EC2 provides full control over the server environment, while ECS offers container orchestration with improved scalability. ECS, in particular, provides high availability but may come at a higher cost.
b. For simplified management: Opt for serverless solutions like Amplify Gen 2. It's AWS's officially supported method for Next.js deployments, offering a balance between ease of use and performance. It's particularly suitable for projects that don't require extensive customization of the underlying infrastructure. Also you can extend it with your existing CDK infrastructure so it's a good choice for startups.
- Consider scalability: Serverless options like Amplify Gen 2 offer automatic scaling, while EC2/ECS solutions require more manual configuration but can be optimized for high-traffic scenarios.
- Factor in team expertise: EC2/ECS deployments require more in-depth AWS knowledge, while Amplify Gen 2 has a gentler learning curve. Amplify Gen 2 also has developer sandboxes which allows multiple developer experiment with and working on the code simultaneously.
Interested in learning more?
If you are new AWS and would like to get started using and learning it the right way (using AWS CDK as IaC) - check out our Udemy class
Posted on October 28, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.