Dockerize a Next.js App with Multi-Stage Docker Builds
While Vercel (previous lesson) offers deep, native Next.js integration, many organizations need to self-host their applications — due to compliance requirements, existing infrastructure investments, or simply a preference for platform independence. Docker, a tool for packaging an application and all its dependencies into a portable, consistent 'container', is the standard approach for self-hosting Next.js reliably across any environment supporting containers.
This lesson covers building an efficient Dockerfile for a Next.js application using a multi-stage build — a pattern that keeps the final production image small and fast by separating the build process from the final runtime environment — and Next.js's standalone output mode, specifically designed to minimize the files needed in that final production image.
Learning Objectives
- Explain what Docker containers are and why they're used for deployment.
- Understand the purpose and benefit of a multi-stage Docker build.
- Configure Next.js's standalone output mode for minimal production images.
- Write a complete, efficient Dockerfile for a Next.js application.
- Build and run a Next.js Docker container locally.
Core Definitions
- Docker: A platform for packaging an application and all its dependencies into a portable, consistent unit called a container, which runs reliably across any environment supporting Docker.
- Container: A lightweight, isolated, runnable package containing an application's code, runtime, dependencies, and configuration, built from a Docker image.
- Dockerfile: A text file containing step-by-step instructions for building a Docker image, specifying the base environment, dependencies, and how to run the application.
- Multi-stage build: A Dockerfile pattern using multiple, separate build stages, where only the final, necessary artifacts from earlier stages are copied into the final image, keeping it small.
- Standalone output: A Next.js build output mode (output: 'standalone' in next.config.js) that produces a minimal set of files needed to run the application, excluding unnecessary source files and unused dependencies.
Detailed Explanation
Docker solves a classic 'works on my machine' problem: by packaging an application together with its exact runtime, dependencies, and configuration into an image, that same image runs identically regardless of the underlying host machine's own configuration, whether that's a developer's laptop, a staging server, or a production cluster. A Dockerfile is the recipe for building this image — a sequence of instructions specifying a base environment (like a specific Node.js version), copying in application code, installing dependencies, and specifying the command to run the application.
A naive, single-stage Dockerfile for a Next.js application would typically include everything: the full source code, all development dependencies (like testing libraries and TypeScript's compiler, neither needed at runtime), and the full node_modules folder — resulting in an unnecessarily large final image, slower to build, push, pull, and deploy than necessary. A multi-stage build solves this by splitting the process into distinct stages: an early stage installs dependencies and runs the actual `next build` command (needing the full source code and dev dependencies to do so), while a later, final stage starts fresh from a clean base image and copies over ONLY the specific, minimal build output actually needed to run the application — discarding the source code, dev dependencies, and build tooling entirely from the final image.
Next.js's standalone output mode, enabled via `output: 'standalone'` in next.config.js, complements this pattern perfectly: rather than requiring the full node_modules folder (which can be enormous, including many packages only needed during development or build time) to be copied into the final Docker stage, standalone output produces a self-contained folder including only the exact, minimal set of files and dependencies actually needed to run the application in production — often reducing the final image size dramatically compared to a naive approach copying the entire node_modules folder.
The resulting multi-stage Dockerfile typically has three conceptual stages: a 'deps' stage installing dependencies, a 'builder' stage that copies those dependencies alongside the source code and runs `next build` (producing the standalone output), and a final, lean 'runner' stage that starts from a minimal base image and copies over only the standalone output from the builder stage, plus the public/ folder and static assets, setting the final command to start the application. Each stage's intermediate files (the full source code, dev dependencies, build tooling) never make it into this final image, which is what actually gets deployed and run in production — dramatically smaller, faster to transfer, and containing less surface area that could theoretically be exploited if a vulnerability were found in an unnecessary dependency.
A Multi-Stage Docker Build for Next.js: Three Stages
{"heading":"A Multi-Stage Docker Build for Next.js: Three Stages","description":"Visualize the three-stage Docker build process:\n\nSTAGE 1: 'deps' (install dependencies only)\n [Base Node.js image] --> [Copy package.json] --> [npm install] --> node_modules ready\n\nSTAGE 2: 'builder' (build the actual application)\n [Copy node_modules from 'deps'] --> [Copy full source code] --> [Run `next build`]\n --> Produces .next/standalone (minimal runtime output, thanks to output: 'standalone')\n\nSTAGE 3: 'runner' (the FINAL, lean production image)\n [Fresh, minimal base image] --> [Copy ONLY .next/standalone + public/ + static assets from 'builder']\n --> [CMD: node server.js]\n\nThe FINAL image contains NONE of the full source code, dev dependencies, or build tooling from stages 1-2 — only the minimal standalone output."}
Next.js Practical Example
// next.config.js — enabling standalone output
/** @type {import('next').NextConfig} */
const nextConfig = {
output: 'standalone',
};
module.exports = nextConfig;
// Dockerfile — a multi-stage build for Next.js
# ---- Stage 1: deps ----
FROM node:20-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
# ---- Stage 2: builder ----
FROM node:20-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build
# ---- Stage 3: runner (the FINAL, lean image) ----
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY --from=builder /app/public ./public
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
EXPOSE 3000
CMD ["node", "server.js"]
The deps stage installs dependencies using npm ci (a stricter, reproducible install for CI/production environments) based purely on package.json and the lockfile. The builder stage copies those installed dependencies plus the full source code, then runs the actual next build command — this is the stage requiring the most disk space and time, but critically, none of it ends up in the final image. The runner stage starts completely fresh from a clean node:20-alpine base image and copies over only three specific things from the builder stage: the public/ folder, the standalone output (thanks to output: 'standalone' in next.config.js, this is a minimal, self-contained runtime bundle), and the static assets folder — the final CMD simply runs `node server.js`, the entry point standalone output automatically generates. Everything else from the builder and deps stages (the full source code, node_modules' dev dependencies, build caches) is entirely discarded, never making it into this final, lean production image.
How Companies Use Docker for Next.js Deployment
- Enterprises with existing Kubernetes infrastructure containerize their Next.js applications specifically to integrate with their established deployment, scaling, and monitoring tooling built around container orchestration.
- Companies with strict data residency or compliance requirements self-host Next.js via Docker on their own infrastructure (on-premises or a specific cloud region) rather than relying on a third-party platform like Vercel.
- Teams running a mix of technologies (a Next.js frontend alongside separate backend services in other languages) commonly use Docker to package and deploy all these different services consistently within the same container orchestration system.
- Organizations prioritizing infrastructure cost control at scale sometimes self-host via Docker on their own cloud compute resources, trading Vercel's managed convenience for potentially lower costs at very high, predictable traffic volumes.
- Security-conscious teams specifically value multi-stage builds and standalone output for minimizing a production container's attack surface, since fewer unnecessary files and dependencies mean fewer potential vulnerabilities.
Common Mistakes to Avoid
- Writing a single-stage Dockerfile that includes the full source code and dev dependencies in the final production image, resulting in unnecessary bloat.
- Forgetting to enable output: 'standalone' in next.config.js, missing out on its significant final image size reduction benefit.
- Not using a lockfile-aware install command (like npm ci instead of npm install) in the Docker build, risking inconsistent dependency versions between builds.
- Copying unnecessary files (like .git folders or local environment files) into the Docker image, unintentionally bloating it or leaking sensitive local configuration.
- Not properly setting NODE_ENV=production in the final runner stage, potentially missing production-specific optimizations or behavior.
Interview Notes
- Docker packages an application with its exact dependencies and runtime into a portable, consistent container.
- A multi-stage Docker build separates the build process from the final runtime image, keeping the final image small.
- Next.js's standalone output mode (output: 'standalone') produces a minimal, self-contained runtime bundle, ideal for Docker deployment.
- A typical multi-stage Next.js Dockerfile has deps, builder, and runner stages, with only the runner stage's contents making up the final image.
- Self-hosting via Docker is a common alternative to Vercel for compliance, existing infrastructure, or platform-independence reasons.
Key Takeaways
- Multi-stage Docker builds and Next.js's standalone output mode together solve the common problem of unnecessarily large, slow-to-deploy production images.
- Understanding Docker gives you a genuine, platform-independent self-hosting option, complementing Vercel's managed hosting approach from the previous lesson.
- The deps/builder/runner stage pattern is a well-established, reusable structure applicable to most Next.js Dockerization needs.
- A smaller, more minimal production image isn't just about speed — it also reduces the surface area for potential security vulnerabilities.
Summary
Docker packages a Next.js application together with its exact runtime and dependencies into a portable, consistent container, ensuring it runs identically regardless of the underlying host environment — a common self-hosting alternative to Vercel's managed platform for organizations with specific compliance, infrastructure, or platform-independence needs. A multi-stage Docker build addresses the problem of unnecessarily large, naive single-stage images by separating the build process (requiring full source code and development dependencies) from the final runtime image, copying over only the specific, minimal artifacts actually needed to run the application. Next.js's standalone output mode, enabled via output: 'standalone' in next.config.js, complements this perfectly, producing a minimal, self-contained set of runtime files rather than requiring the full, often enormous node_modules folder. The resulting Dockerfile typically follows a three-stage pattern — deps (installing dependencies), builder (running the actual build), and runner (the final, lean image copying over only the standalone output, public folder, and static assets) — resulting in a dramatically smaller, faster-to-deploy, and more secure production image than a naive, single-stage approach would produce.