← Go back to homepage
Self-Hosting a Next.js app in AWS ECS Fargate

Self-Hosting a Next.js app in AWS ECS Fargate

As a software engineer, I usually deploy my Next.js apps to Vercel because of how seamless and well-integrated the experience is. That said, there are situations where self-hosting can be worth considering. It gives you more control over the infrastructure, which can be helpful if you need to integrate with custom backend systems, work around platform limitations like request timeouts, or fine-tune performance in ways Vercel doesn’t allow. Self-hosting can also offer better cost predictability at scale and more control over data handling, which might be important for apps with strict privacy or compliance requirements. It’s not the default choice for most projects, but it’s good to know it’s an option when the use case calls for it.

This being said, let's look at ways we can easily go the self-hosting route, using our favorite companion for Infrastructure as Code - AWS CDK - which will save a tremendous amount of time.

Initialize the project

Create 2 folders: cdk and nextjs. The cdk folder will contain the CDK code, while the nextjs folder will contain the Next.js app.

mkdir cdk nextjs

Initialize a Next.js app in the nextjs folder:

cd nextjs npx create-next-app@latest .

Make sure you can run it locally:

cd nextjs npm run dev

Open your browser and navigate to http://localhost:3000 to see the default Next.js app running. So far so good. Now it's time to switch to deployment of this app.

Initialize the CDK project

In the cdk folder, initialize the CDK project:

cd cdk cdk init app --language=typescript

This will create a new CDK project with the necessary files and folders. You can now start adding your CDK code to deploy the Next.js app. The CDK project will be responsible for creating the necessary AWS resources to run the Next.js application. For example, ECS Cluster, Task Definition, and Fargate Service.

Inside the default CDK stack (or better - create a new Stack), you can start adding the necessary resources to deploy the Next.js app.

Create ECS Cluster

const cluster = new Cluster(this, 'Cluster', { vpc: Vpc.fromLookup(this, 'Vpc', {isDefault: true}), });

Here I picked to launch the cluster in the default VPC of the AWS account, but feel free to customize.

Create the Task Definition

The task definition is the blueprint for your application. It defines how your application will run, including the memory and CPU constraints, as well as Docker container definitions (where the Dockerfile lives locally, etc).

const taskDefinition = new FargateTaskDefinition(this, 'FargateTaskDef') taskDefinition.addContainer('nextjs', { image: ContainerImage.fromAsset(path.resolve(__dirname, '../../nextjs'), { // Use Linux architecture, regardless if deployment happens on Linux or Windows or MacOS platform: Platform.LINUX_AMD64, }), logging: new AwsLogDriver({streamPrefix: 'nextjs'}), environment: { NODE_ENV: 'production', PORT: '3000', }, portMappings: [ { containerPort: 3000, hostPort: 3000, protocol: Protocol.TCP, }, ], })

Make sure to adjust the relative path to the folder where the Next.js app lives, in case you decided to deviate from the first steps of this tutorial.

A good minimal Dockerfile you can use to manage the building and running of the Next.js app is shown below. Make sure to put it inside nextjs/Dockerfile. Feel free to adjust or optimize with multi-stage Docker builds in the future.

# syntax=docker.io/docker/dockerfile:1 # Inspired by: https://github.com/vercel/next.js/blob/canary/examples/with-docker/Dockerfile # As linked by: https://nextjs.org/docs/app/building-your-application/deploying#docker-image FROM node:22-alpine AS base ENV NEXT_TELEMETRY_DISABLED=1 ENV NODE_ENV=production # Copy package files and install dependencies COPY package*.json ./ RUN npm ci || npm install --no-audit # Copy the rest of the app COPY . . # Build the Next.js app RUN npm run build # Expose default Next.js port EXPOSE 3000 # Start the app CMD ["npm", "start"]

Create the Fargate Service

Now that we have the ECS cluster ready to launch services in, and we have the Task Definition as the blueprint of the Next.js app, it's time to create the Fargate Service.

const fargateService = new FargateService(this, 'FargateService', { taskDefinition, cluster, assignPublicIp: true, })

Assigning a public IP is required so that the task can pull the Docker image from the ECR repository.

Making the app accessible through the internet, through a Load Balancer

The final step is to make sure the Next.js app has some sort of public entrypoint, through which we can access it.

The AWS-native way to do this would be the ALB service.

Create the ALB:

const alb = new ApplicationLoadBalancer(this, 'ALB', { vpc: cluster.vpc, internetFacing: true, })

Create a listener for the ALB:

const listener = alb.addListener('Listener', { port: 80, open: true, })

By default an ALB is not accessible from the internet. By adding the port 80 listener, we are making it accessible through a URL like http://ALB_URL:80 URL.

The final piece of the puzzle is attaching the Fargate Service as a "target" to the ALB listener:

const targetGroup = listener.addTargets('TargetGroup', { port: 3000, protocol: ApplicationProtocol.HTTP, targets: [fargateService], })

Deploy the CDK stack

Now that we have all the pieces in place, we can deploy the CDK stack. Make sure you are in the cdk folder and run:

cdk deploy

Obviously, valid AWS credentials are required. Setting up the CDK CLI and AWS credentials is outside the scope of this tutorial.

← Go back to homepage