June 2025
·2 min read
Dockerizing a Next.js App: Multi-Stage Builds for Tiny Images
Containerise your Next.js app with a multi-stage Dockerfile that produces a slim production image under 200MB — ready for ECS, Cloud Run, or any container host.
Introduction
- A naïve Next.js Docker build ships the entire
node_modules(200MB+) to production — wasteful and slow to pull. - Multi-stage builds use a full build environment in one stage and copy only the final artifacts to a minimal runtime image.
- This guide produces a production Next.js image under 200MB using the
standaloneoutput mode.
Prerequisites
- Next.js 14+ with App Router.
- Docker Desktop installed locally.
- Familiarity with
Dockerfilebasics.
Enable Standalone Output in Next.js
// next.config.ts
const nextConfig = {
output: 'standalone',
};
export default nextConfig;standalonemode traces dependencies and produces a self-contained server in.next/standalone/— no fullnode_modulesneeded at runtime.- Introduced in Next.js 12, production-stable.
The Multi-Stage Dockerfile
# ───────────────────────────────────────────────
# Stage 1: deps — install production dependencies
# ───────────────────────────────────────────────
FROM node:22-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
# ───────────────────────────────────────────────
# Stage 2: builder — build the Next.js app
# ───────────────────────────────────────────────
FROM node:22-alpine AS builder
WORKDIR /app
# Copy source and all node_modules (including devDeps for build)
COPY --from=deps /app/node_modules ./node_modules
COPY . .
# Build-time env vars (public only — never put secrets here)
ARG NEXT_PUBLIC_SITE_URL
ENV NEXT_PUBLIC_SITE_URL=$NEXT_PUBLIC_SITE_URL
RUN npm run build
# ───────────────────────────────────────────────
# Stage 3: runner — minimal production image
# ───────────────────────────────────────────────
FROM node:22-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV PORT=3000
# Create a non-root user for security
RUN addgroup --system --gid 1001 nodejs && \
adduser --system --uid 1001 nextjs
# Copy only the standalone output
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/public ./public
USER nextjs
EXPOSE 3000
CMD ["node", "server.js"]Stage Breakdown
| Stage | Base image | Purpose | What gets discarded |
|---|---|---|---|
deps | node:22-alpine | Install prod deps | dev dependencies |
builder | node:22-alpine | Run npm run build | source code, devDeps, full node_modules |
runner | node:22-alpine | Runtime only | everything except .next/standalone, static, public |
Building and Running Locally
# Build the image
docker build \
--build-arg NEXT_PUBLIC_SITE_URL=https://myapp.com \
-t my-nextjs-app:latest .
# Run locally
docker run -p 3000:3000 \
-e CONTENTFUL_SPACE_ID=xxx \
-e CONTENTFUL_ACCESS_TOKEN=yyy \
my-nextjs-app:latest
# Check image size
docker images my-nextjs-appPassing Runtime Secrets
- Never bake secrets into the image with
ENVduring build — they end up in every layer. - Pass secrets at runtime via
-eflags, Docker Secrets, AWS Secrets Manager, or Kubernetes Secrets. - In ECS: inject from Secrets Manager as environment variables in the task definition.
.dockerignore
node_modules
.next
.git
*.md
.env*- Prevent
node_modulesand.nextfrom being sent to the build context (speeds updocker buildsignificantly).
Health Check
HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:3000/api/health || exit 1- Add a
/api/healthRoute Handler that returns{ status: 'ok' }— used by ECS/ALB health checks.
CI/CD: Build and Push to ECR
# .github/workflows/docker.yml
- name: Build and push to ECR
run: |
docker build \
--build-arg NEXT_PUBLIC_SITE_URL=${{ vars.SITE_URL }} \
-t $ECR_REGISTRY/my-nextjs-app:${{ github.sha }} .
docker push $ECR_REGISTRY/my-nextjs-app:${{ github.sha }}- Tag with
git shafor traceability; also taglatestfor convenience. - Use OIDC for GitHub → AWS auth (no access keys; see IAM article).
Image Size Benchmarks
| Approach | Typical size |
|---|---|
| No multi-stage, full node_modules | 1.2GB+ |
| Multi-stage, no standalone | 400–600MB |
| Multi-stage + standalone (this guide) | 150–200MB |
Conclusion
output: 'standalone'+ multi-stage builds is the canonical way to Dockerize Next.js for production.- Run as a non-root user in the final stage — a container security baseline.
- Tag images by git SHA in CI — you can always roll back to a specific commit's image.