Introduction
Running a Next.js application in production is not just about writing code. It is also about packaging the application efficiently so it can build reliably, start quickly, and run with only the files it actually needs.
Docker is commonly used for deployment, but a naive setup often creates large images, slow builds, unnecessary dependencies, and inefficient runtime behavior. A production-ready Docker setup should be lightweight, predictable, and easy to deploy across environments.
This note focuses on practical engineering decisions behind running Next.js in Docker, especially the patterns that reduce image size, improve deployment speed, and create a cleaner production runtime.
The Problem
A common mistake is using a single-stage Dockerfile that installs dependencies, builds the application, and ships everything into production. This works, but it is not efficient.
FROM node:18
WORKDIR /app
COPY . .
RUN npm install
RUN npm run build
CMD ["npm", "start"]
This approach includes development dependencies, source files, build cache, and unnecessary files in the final production image.
Common Failures
- Large Docker image size
- Slow build and deployment times
- Unnecessary files shipped to production
- Security risks from unused dependencies and tooling
Engineering Impact
- Deployments become slower and heavier
- Runtime containers contain more files than needed
- Production debugging becomes noisier
- Infrastructure costs can increase because images are larger
The problem is not Docker itself. The problem is shipping the build environment as if it were the runtime environment.
System Design / Approach
The solution is to separate build-time work from runtime execution. A multi-stage Docker build creates the application in one stage and copies only the required production output into the final image.
1. Build in a Dedicated Stage
Install dependencies and run the Next.js build in a builder stage so build tools do not need to exist inside the final runtime image.
2. Use Next.js Standalone Output
Standalone output traces the files needed to run the app and produces a smaller runtime bundle for container deployment.
3. Keep Runtime Minimal
The final image should contain only the compiled app, static assets, runtime dependencies, and environment configuration needed to start the server.
Implementation
Step 1: Enable Standalone Output
Configure Next.js to generate a standalone build. This creates a production output that can run without copying the full project and full dependency tree into the final image.
module.exports = {
output: "standalone",
};
Standalone output allows Docker to run the app with only the required server files and traced dependencies.
Step 2: Use a Multi-Stage Docker Build
Separate the build environment from the runtime environment. The builder stage installs dependencies and compiles the app, while the runner stage only receives the final production output.
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:18-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/public ./public
EXPOSE 3000
CMD ["node", "server.js"]
The final image only contains the compiled output, static files, public assets, and the minimal runtime needed to start the Next.js server.
Step 3: Handle Environment Variables Properly
Environment variables should be handled intentionally. Runtime secrets should not be hardcoded during the build, and public variables should be separated from server-only values.
ENV NODE_ENV=production
ENV PORT=3000
Server-only secrets should be injected through the deployment platform, Docker runtime configuration, or orchestration layer instead of being baked into the image.
Step 4: Ignore Unnecessary Files
A production image should not receive files that are only useful during local
development. A clean .dockerignore keeps the build context small
and avoids copying unnecessary files.
node_modules
.next
.git
.env
README.md
Dockerfile
docker-compose.yml
Reducing the Docker build context improves build speed and prevents local development files from leaking into the image.
Trade-offs
| Approach | Benefit | Cost |
|---|---|---|
| Multi-Stage Build | Smaller final image with cleaner production runtime | Requires more Dockerfile structure and build-stage planning |
| Standalone Output | Simplifies Next.js deployment inside containers | Requires Next.js configuration and correct static/public file copying |
| Minimal Runtime Image | Fewer files, smaller attack surface, and faster deployment | Debugging inside the container may require extra tooling |
Real-World Impact
Smaller Images
Docker image size is reduced because the final image only contains the production output required to run the application.
Faster Deployments
Deployment becomes faster because smaller images are easier to build, push, pull, and start.
Cleaner Production
The runtime environment becomes cleaner because development dependencies, source clutter, and unused files are left out of the final image.