Deploying a Next.js 15 App to AWS ECS with Docker
A production-minded guide to containerizing a Next.js 15 app with standalone output and running it on AWS ECS Fargate — Dockerfile, task definition, and the gotchas that bite in production.
On this page
Vercel is the path of least resistance for Next.js, and for most projects it is the right call. But sometimes you need to run inside your own AWS account — to sit next to a private database, satisfy compliance requirements, or consolidate infrastructure. This guide walks through deploying a Next.js 15 app to ECS Fargate with Docker, the way I have done it for client work.
We will focus on the parts that are specific to Next.js and the parts that quietly break in production.
Step 1: Enable standalone output
The single most important change is telling Next.js to produce a self-contained build. In next.config.ts:
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
output: "standalone",
};
export default nextConfig;With standalone, Next traces exactly which files and node_modules your server needs and copies them into .next/standalone. Your final image can be a fraction of the size of one that bundles the whole dependency tree.
Step 2: A multi-stage Dockerfile
Multi-stage builds keep the runtime image small and free of build tooling:
# 1. Install dependencies only when needed
FROM node:22-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
# 2. Build the app
FROM node:22-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build
# 3. Minimal production image
FROM node:22-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV PORT=3000
RUN addgroup --system --gid 1001 nodejs \
&& adduser --system --uid 1001 nextjs
COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
CMD ["node", "server.js"]Note the last three COPY lines. Standalone output does not include public/ or .next/static — you have to copy them in yourself. Forgetting .next/static is the classic "my site loads but has no CSS or JS" bug.
Step 3: Build and push to ECR
Create a repository and push:
aws ecr create-repository --repository-name bytes-web
aws ecr get-login-password --region us-east-1 \
| docker login --username AWS --password-stdin \
<account>.dkr.ecr.us-east-1.amazonaws.com
docker build -t bytes-web .
docker tag bytes-web:latest \
<account>.dkr.ecr.us-east-1.amazonaws.com/bytes-web:latest
docker push <account>.dkr.ecr.us-east-1.amazonaws.com/bytes-web:latestIf you build on an Apple Silicon Mac and deploy to Fargate, build for the right architecture or your task will crash-loop:
docker build --platform linux/amd64 -t bytes-web .Step 4: The ECS task definition
The task definition is where Next.js apps most often go wrong. A minimal, working container definition:
{
"family": "bytes-web",
"networkMode": "awsvpc",
"requiresCompatibilities": ["FARGATE"],
"cpu": "512",
"memory": "1024",
"containerDefinitions": [
{
"name": "web",
"image": "<account>.dkr.ecr.us-east-1.amazonaws.com/bytes-web:latest",
"portMappings": [{ "containerPort": 3000, "protocol": "tcp" }],
"environment": [{ "name": "PORT", "value": "3000" }],
"healthCheck": {
"command": ["CMD-SHELL", "wget -q -O /dev/null http://localhost:3000/ || exit 1"],
"interval": 30,
"timeout": 5,
"retries": 3,
"startPeriod": 20
},
"logConfiguration": {
"logDriver": "awslogs",
"options": {
"awslogs-group": "/ecs/bytes-web",
"awslogs-region": "us-east-1",
"awslogs-stream-prefix": "web"
}
}
}
]
}Put this behind an Application Load Balancer with a target group pointing at port 3000, and the ALB health check hitting /.
The gotchas that bite in production
These are the ones I have actually been burned by:
- Server env vars vs. build-time env vars. Anything prefixed
NEXT_PUBLIC_is inlined at build time, so it must be present duringdocker build, not just at runtime. Server-only secrets should come from the task definition (or Secrets Manager) at runtime — never bake them into the image. - The health check
startPeriod. Next.js needs a moment to boot. Without astartPeriod, ECS kills the task before it is ready and you get an endless deploy loop. output: standaloneand the copy steps. Say it again: copypublic/and.next/staticexplicitly. Nothing errors — the page just renders unstyled.- Right-size the task. A Next.js server idles comfortably in 512 CPU / 1024 MB, but a build-heavy startup or image optimization can spike memory. Watch CloudWatch for the first few days.
Was it worth it versus Vercel?
For most projects, no — Vercel's zero-config deploys and edge network are hard to beat, and you should reach for it by default. But when the app has to live inside a VPC next to private resources, this ECS setup is stable, boring, and fully under your control. And "boring and under your control" is exactly what you want production infrastructure to be.