DevOps & CloudOctober 11, 2026•7 min read

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.

RB
Rohit Bhatu — Lead Systems Architect
Hardening Next.js Dockerfiles: Multi-Stage Builds, Alpine Sandboxing & Non-Root Security

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.

Ensure `output: 'standalone'` is enabled in your `next.config.ts` before compiling your production Docker target.

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.

Production-grade multi-stage Dockerfile for Next.jsdockerfile
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).

Never copy source directories with root ownership and drop privileges later without correctly chowning `.next/standalone`. Mismatched permissions can crash dynamic cache writers.

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.