Mark Lowel Montealto — Full Stack Developer & DevOps Engineer

Mark Lowel
Montealto

Full Stack Developer & DevOps Engineer

Blog

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 standalone output mode.

Prerequisites

  • Next.js 14+ with App Router.
  • Docker Desktop installed locally.
  • Familiarity with Dockerfile basics.

Enable Standalone Output in Next.js

// next.config.ts
const nextConfig = {
  output: 'standalone',
};
 
export default nextConfig;
  • standalone mode traces dependencies and produces a self-contained server in .next/standalone/ — no full node_modules needed 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

StageBase imagePurposeWhat gets discarded
depsnode:22-alpineInstall prod depsdev dependencies
buildernode:22-alpineRun npm run buildsource code, devDeps, full node_modules
runnernode:22-alpineRuntime onlyeverything 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-app

Passing Runtime Secrets

  • Never bake secrets into the image with ENV during build — they end up in every layer.
  • Pass secrets at runtime via -e flags, 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_modules and .next from being sent to the build context (speeds up docker build significantly).

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/health Route 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 sha for traceability; also tag latest for convenience.
  • Use OIDC for GitHub → AWS auth (no access keys; see IAM article).

Image Size Benchmarks

ApproachTypical size
No multi-stage, full node_modules1.2GB+
Multi-stage, no standalone400–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.