Engineering Note
Infrastructure

Fixing Next.js + Docker for Production

Hardening Build and Runtime Behavior

8 min read
BeginnerInfrastructure

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.

naive-dockerfile
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.

next.config.js
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.

Dockerfile
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.

runtime-env.docker
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.

.dockerignore
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.

Key Takeaways

Production Docker builds should be minimal and avoid unnecessary dependencies

Multi-stage builds significantly reduce final image size

Environment variables must be handled differently in build-time and runtime

Next.js standalone output simplifies deployment in containers

Incorrect Docker configuration can lead to large images and slow startup times

Future Improvements

Use distroless or alpine images to further reduce image size

Introduce container health checks for reliability

Implement CI/CD pipelines for automated Docker builds and deployments

Add caching layers for faster builds using Docker layer caching

Use container orchestration tools like Kubernetes for scaling