Hardening Next.js Dockerfiles: Multi-Stage Builds, Alpine Sandboxing & Non-Root Security
Production guide to trimming Next.js container images below 95MB, implementing unprivileged non-root users, and optimizing layer cache in standalone mode.

1. The Anatomy of Bloated Next.js Images
Deploying Next.js applications directly from a standard `node:22` image often yields container images exceeding 1.8GB. This bloat stems from bundling full development dependencies, local cache files, build tools, and operating system utilities that are completely unnecessary in production.
Large images cause slower CI/CD deployment pipelines, sluggish horizontal autoscaling in Kubernetes clusters, and an expanded attack surface containing unused system binaries with unpatched vulnerabilities.
The modern standard is using Next.js standalone output mode (`output: 'standalone'`), which extracts only the exact JavaScript files, server assets, and production node_modules necessary to execute the server.
2. Structuring a 3-Stage Multi-Stage Dockerfile
A multi-stage build divides container creation into distinct phases: `deps` (dependency installation), `builder` (source compilation), and `runner` (minimal Alpine execution environment).
By using separate stages, build tools like npm, bun, or yarn never enter the final container image, leaving only the compiled artifacts in a stripped Alpine Linux layer.
FROM node:22-alpine AS base
# 1. Install dependencies only when needed
FROM base AS deps
RUN apk add --no-cache libc6-compat
WORKDIR /app
COPY package.json package-lock.json* bun.lock* ./
RUN npm ci --prefer-offline --no-audit
# 2. Rebuild the source code only when changes occur
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
ENV NEXT_TELEMETRY_DISABLED=1
RUN npm run build
# 3. Production runner image, copy files and run next
FROM base AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
RUN addgroup --system --gid 1001 nodejs
RUN 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
ENV PORT=3000
ENV HOSTNAME="0.0.0.0"
CMD ["node", "server.js"]3. Non-Root Execution and Host Isolation
By default, Docker containers run commands as `root` (UID 0). If a remote code execution vulnerability is discovered within an application dependency, an attacker gains root privileges inside the container, substantially simplifying host escape vectors.
Our runner stage creates a dedicated system group `nodejs` (GID 1001) and system user `nextjs` (UID 1001). Setting `USER nextjs` guarantees that the runtime node process lacks permission to write to host directories, modify system certificates, or bind to privileged system ports (< 1024).
4. Layer Caching and Build Times
Notice the order of Docker instructions: `package.json` and lockfiles are copied before copying application source code. Because dependencies change far less frequently than UI code, Docker reuses cached dependency layers across CI runs.
This caching pattern cuts average deployment builds from 4 minutes down to under 25 seconds on modern GitHub Actions runners.