Docker provides a reliable and portable way to deploy Next.js applications. By containerizing your app, you eliminate environment inconsistencies and enable seamless deployments to any cloud provider.
In this guide, we'll containerize a Next.js app and deploy it to AWS using Docker, covering best practices for production-ready deployments.
Why Docker for Next.js?
Docker packages your app with all dependencies, providing:
- Portability - Deploy anywhere Docker runs (AWS, GCP, Azure, etc.)
- Consistency - Same environment in development and production
- Isolation - Dependencies don't conflict with host system
- Scalability - Easy horizontal scaling with container orchestration
Creating an Optimized Dockerfile
Use multi-stage builds to create lean production images:
Dockerfile
# Stage 1: Dependencies
FROM node:22-alpine AS deps
RUN apk add --no-cache libc6-compat
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN corepack enable pnpm && pnpm install --frozen-lockfile
# Stage 2: Build
FROM node:22-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
ENV NEXT_TELEMETRY_DISABLED 1
RUN corepack enable pnpm && pnpm build
# Stage 3: Production
FROM node:22-alpine AS runner
WORKDIR /app
ENV NODE_ENV production
ENV NEXT_TELEMETRY_DISABLED 1
# Create non-root user for security
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
# Copy only necessary files
COPY --from=builder /app/public ./public
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
ENV PORT 3000
ENV HOSTNAME "0.0.0.0"
CMD ["node", "server.js"]
Key Optimization: Use
node:22-alpineto reduce image size. The Alpine base image is ~5MB compared to ~900MB for the full Node image.
Enable standalone output in your Next.js config:
next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
output: 'standalone',
};
module.exports = nextConfig;
Docker Compose for Local Development
docker-compose.yml
version: '3.8'
services:
app:
build:
context: .
dockerfile: Dockerfile
ports:
- "3000:3000"
environment:
- DATABASE_URL=postgresql://user:pass@db:5432/mydb
depends_on:
- db
db:
image: postgres:17
environment:
POSTGRES_USER: user
POSTGRES_PASSWORD: pass
POSTGRES_DB: mydb
volumes:
- postgres_data:/var/lib/postgresql/data
ports:
- "5432:5432"
volumes:
postgres_data:
Deploying to AWS ECS
AWS Elastic Container Service (ECS) is a fully managed container orchestration service. Here's how to deploy:
1. Push to Amazon ECR
First, create an ECR repository and push your image:
Terminal
# Login to ECR
aws ecr get-login-password --region us-east-1 | \
docker login --username AWS --password-stdin \
123456789.dkr.ecr.us-east-1.amazonaws.com
# Build and tag
docker build -t my-nextjs-app .
docker tag my-nextjs-app:latest \
123456789.dkr.ecr.us-east-1.amazonaws.com/my-nextjs-app:latest
# Push
docker push 123456789.dkr.ecr.us-east-1.amazonaws.com/my-nextjs-app:latest
2. Create ECS Task Definition
task-definition.json
{
"family": "my-nextjs-app",
"networkMode": "awsvpc",
"requiresCompatibilities": ["FARGATE"],
"cpu": "256",
"memory": "512",
"executionRoleArn": "arn:aws:iam::123456789:role/ecsTaskExecutionRole",
"containerDefinitions": [
{
"name": "my-nextjs-app",
"image": "123456789.dkr.ecr.us-east-1.amazonaws.com/my-nextjs-app:latest",
"portMappings": [
{
"containerPort": 3000,
"protocol": "tcp"
}
],
"logConfiguration": {
"logDriver": "awslogs",
"options": {
"awslogs-group": "/ecs/my-nextjs-app",
"awslogs-region": "us-east-1",
"awslogs-stream-prefix": "ecs"
}
}
}
]
}
GitHub Actions CI/CD
Automate deployments with GitHub Actions:
.github/workflows/deploy.yml
name: Deploy to AWS ECS
on:
push:
branches: [main]
env:
AWS_REGION: us-east-1
ECR_REPOSITORY: my-nextjs-app
ECS_SERVICE: my-nextjs-service
ECS_CLUSTER: my-cluster
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ${{ env.AWS_REGION }}
- name: Login to Amazon ECR
id: login-ecr
uses: aws-actions/amazon-ecr-login@v2
- name: Build, tag, and push image
env:
ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
IMAGE_TAG: ${{ github.sha }}
run: |
docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG .
docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
- name: Deploy to ECS
run: |
aws ecs update-service \
--cluster $ECS_CLUSTER \
--service $ECS_SERVICE \
--force-new-deployment
Security: Never commit AWS credentials. Use GitHub Secrets and IAM roles with minimal required permissions.
Common Issues and Solutions
Port Not Accessible
Ensure Next.js listens on all interfaces:
package.json
{
"scripts": {
"start": "next start -H 0.0.0.0"
}
}
Missing .next Directory
Always run npm run build before npm start in your Dockerfile. The standalone output requires a complete build.
Alternative: AWS App Runner
For simpler deployments, consider AWS App Runner which handles infrastructure automatically:
Terminal
aws apprunner create-service \
--service-name my-nextjs-app \
--source-configuration \
'ImageRepository={ImageIdentifier=123456789.dkr.ecr.us-east-1.amazonaws.com/my-nextjs-app:latest,ImageRepositoryType=ECR}'
Conclusion
Docker and AWS provide a powerful, scalable platform for Next.js deployments. With multi-stage builds, you get small, secure images. With ECS or App Runner, you get managed container orchestration.
Start with App Runner for simplicity, then graduate to ECS when you need more control over networking, scaling, and costs.
