Skip to content

Deploying a Next.js App on AWS: Options Compared and a Step-by-Step Path

Cloud & DevOps7 min readBy the Nexzem team

Amplify, serverless with OpenNext, containers on ECS Fargate, EC2 or static export: how to choose, and a step-by-step container deployment you can follow.

In this article
  1. 01Why Next.js on AWS takes a decision
  2. 02The options compared
  3. 03How to choose
  4. 04Step 1: Build a standalone output
  5. 05Step 2: Write a multi-stage Dockerfile
  6. 06Step 3: Push the image to ECR
  7. 07Step 4: Run it on ECS Fargate
  8. 08Step 5: Add CloudFront, HTTPS and a domain
  9. 09Step 6: Handle caching across multiple instances
  10. 10Step 7: Get environment variables right
  11. 11Pitfalls and cost traps
  12. 12Step 8: Automate deployments

Why Next.js on AWS takes a decision

A Next.js app is not just static files. Depending on the features you use, it may need a Node.js server for server-side rendering, route handlers, middleware, image optimization and cache revalidation. AWS has several ways to run that, each with different trade-offs in effort, cost and control. Choosing well up front avoids a migration later.

The options compared

These are the common routes, from least to most hands-on:

  • Static export to S3 and CloudFront: set output to export in next.config. Cheapest and simplest, but no server rendering, route handlers or middleware at request time
  • AWS Amplify Hosting: a managed service that builds and hosts Next.js apps with server rendering. Fast to start, less control over infrastructure details
  • Serverless with OpenNext, often deployed through SST: adapts the Next.js build to Lambda, S3 and CloudFront. Scales to zero, but adds moving parts
  • Containers on ECS Fargate behind an Application Load Balancer: a long-running Node.js server in Docker. Predictable, portable and easy to reason about
  • EC2 with Node.js and a process manager: full control, but you patch and scale the servers yourself

How to choose

For a marketing site or documentation with no request-time logic, static export wins. For a small team that wants managed hosting and accepts its conventions, Amplify is the quickest path. For apps with steady traffic, background work, WebSockets or a need to run the same container elsewhere, ECS Fargate is a solid default. Serverless suits spiky traffic and teams already invested in Lambda. Our serverless vs containers comparison goes deeper.

The rest of this guide follows the container path, because it behaves the same locally, in CI and in production and transfers to any cloud.

Step 1: Build a standalone output

In next.config, set output to "standalone". The build then produces a .next/standalone folder containing a minimal server.js and only the dependencies the server needs, which keeps images small. The standalone folder does not include static assets, so you copy .next/static and the public folder alongside it.

Step 2: Write a multi-stage Dockerfile

Use a multi-stage build so build tools never reach the production image. The first stage installs dependencies with npm ci. The second runs next build. The final stage starts from a slim Node.js base image, copies .next/standalone to the working directory, .next/static to .next/static and public to public, switches to a non-root user and runs node server.js.

  • Set NODE_ENV to production and expose port 3000
  • Set HOSTNAME to 0.0.0.0 so the server listens on all interfaces inside the container
  • Make sure sharp is available if you use next/image optimization
  • Add a .dockerignore for node_modules, .next and .git to speed up builds

Step 3: Push the image to ECR

Create a repository in Amazon ECR, authenticate Docker with aws ecr get-login-password piped to docker login, then build, tag and push the image. Tag images with the Git commit SHA rather than only latest, so every deployment is traceable and rollbacks are a matter of pointing to an earlier tag.

Step 4: Run it on ECS Fargate

Create an ECS cluster and a task definition that references the image, sets CPU and memory, maps container port 3000 and sends logs to CloudWatch with the awslogs driver. Load configuration from environment variables and secrets from AWS Secrets Manager or SSM Parameter Store, referenced in the task definition rather than baked into the image.

Create an ECS service with at least two tasks across availability zones, attached to an Application Load Balancer target group. Point the health check at a lightweight route that returns 200, and add target-tracking auto scaling on CPU or request count.

Step 5: Add CloudFront, HTTPS and a domain

Put a CloudFront distribution in front of the load balancer. Cache paths under /_next/static/ for a long time, since their file names change with every build, and forward requests for dynamic pages to the origin. Request a certificate in AWS Certificate Manager; certificates used by CloudFront must be issued in the us-east-1 region. Point your domain at the distribution with Route 53 or your DNS provider. Our explainer on the content delivery network covers caching basics.

Step 6: Handle caching across multiple instances

By default, Next.js stores incremental static regeneration and data cache entries on each server's file system. With two or more tasks, each has its own cache, so revalidation on one is not seen by the others. If you rely on revalidation, configure a custom cache handler in next.config that stores entries in a shared store such as Redis, or keep those pages fully dynamic.

Step 7: Get environment variables right

Next.js inlines variables prefixed with NEXT_PUBLIC_ into the browser bundle at build time. They are fixed inside the image and visible to anyone, so never put secrets there. Server-only variables are read at runtime from the task definition, which means one image can run in staging and production with different settings. If browser code needs a value that differs per environment, avoid NEXT_PUBLIC_ for it and pass it from the server instead, for example through a server component or an API route.

Pitfalls and cost traps

A few mistakes come up again and again in Next.js deployments on AWS:

  • Tasks in private subnets pulling images through a NAT gateway, which adds data charges; VPC endpoints for ECR and S3 avoid this
  • A CloudFront cache policy that drops cookies or headers your pages need, breaking sign-in
  • Caching personalized HTML at CloudFront, so one user sees another user's page
  • A health check route that queries the database, turning a database slowdown into failing tasks
  • Too little memory for image optimization, causing tasks to be killed under load

Step 8: Automate deployments

Build and push the image in CI on every merge to main, then update the ECS service with the new task definition revision. With GitHub Actions, use OpenID Connect to assume an AWS role instead of storing long-lived access keys. ECS rolling deployments with the deployment circuit breaker enabled roll back automatically if new tasks fail health checks.

Nexzem builds and runs Next.js applications on AWS; see our Next.js development and AWS development pages if you want a team to set this up with you.

Planning something similar?

Get a straight answer on scope, cost and timeline.

Talk to the team

Tell us what you're building.

A solutions consultant replies within one business day with next steps, a rough estimate and a suggested team.