Next.js Docker Deployment

Vercel is not the only home for a Next.js app. Many companies run applications on their own servers, on cloud virtual machines, or inside container platforms. Docker packages your app and everything it needs into a single portable unit called an image. The same image runs the same way on a laptop, a test server, and a production cluster.

The Everyday View

A shipping container changed global trade because every port could handle the same box shape. The contents did not matter. Cranes, trucks, and ships all knew how to move the box.

A Docker image works the same way for software. The box holds your app, its runtime, and its files. Any machine with Docker installed can run the box without extra setup.

Key Terms

TermMeaning
ImageA read-only package that holds the app and its runtime
ContainerA running copy of an image
DockerfileA recipe file that describes how to build the image
RegistryA storage service for images

Step One: Enable Standalone Output

A regular Next.js build keeps the full node_modules folder, which is large. The standalone output option copies only the files that the production server needs. Add it to the config file.

// next.config.js
const nextConfig = {
  output: 'standalone',
};

module.exports = nextConfig;

After a build, the folder .next/standalone holds a minimal server with a server.js entry file.

Step Two: Write the Dockerfile

A multi-stage Dockerfile builds the app in one stage and copies only the finished output into a small final stage. The final image skips build tools and source files.

FROM node:22-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM node:22-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV PORT=3000
ENV HOSTNAME=0.0.0.0

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"]

Reading the Recipe

  • FROM picks a base image with Node.js installed. The alpine variant stays small.
  • WORKDIR sets the folder for later commands.
  • COPY package*.json before the rest of the code lets Docker reuse cached layers when only source files change.
  • RUN npm ci installs exact dependency versions from the lock file.
  • RUN npm run build produces the optimized app.
  • The second FROM starts a clean stage.
  • COPY --from=builder pulls only finished files across.
  • CMD starts the server.

Build Stages Diagram

+------------------------------+
| Stage 1: builder             |
|  source code                 |
|  node_modules (full)         |
|  build tools                 |
|  -> creates .next/standalone |
+------------------------------+
              |
              |  copy only finished files
              v
+------------------------------+
| Stage 2: runner              |
|  server.js                   |
|  .next/static                |
|  public                      |
|  (small final image)         |
+------------------------------+

Step Three: Ignore Unneeded Files

A .dockerignore file keeps junk out of the build context. Smaller context means faster builds.

node_modules
.next
.git
Dockerfile
.dockerignore
.env*

Listing .env* prevents secret files from landing inside the image.

Step Four: Build and Run

docker build -t my-next-app .
docker run -p 3000:3000 my-next-app

The first command builds an image named my-next-app. The second command starts a container and maps port 3000 inside the container to port 3000 on your machine. Open http://localhost:3000 in a browser to see the app.

Passing Environment Variables

Secrets and settings belong outside the image. Supply them when the container starts.

docker run -p 3000:3000 \
  -e DATABASE_URL="your-connection-string" \
  my-next-app

Server-side variables read at runtime work through this method. Variables that start with NEXT_PUBLIC_ are baked into the browser bundle at build time, so provide them during docker build as build arguments when the app needs them.

Using Docker Compose

A Compose file describes several containers together, such as the app and a database.

services:
  web:
    build: .
    ports:
      - "3000:3000"
    environment:
      - DATABASE_URL=postgres://user:pass@db:5432/app
    depends_on:
      - db
  db:
    image: postgres:16
    environment:
      - POSTGRES_PASSWORD=pass

Run docker compose up to start both services. Replace sample passwords with strong secrets for any real deployment.

Deployment Flow

Developer machine
      |  docker build
      v
  Image  ---- push ---->  Registry
                              |
                              |  pull
                              v
                      Production server
                       docker run
                              |
                              v
                       Visitors reach app

Production Reminders

  • Place a reverse proxy or load balancer in front of the container for HTTPS.
  • Run the container as a non-root user when your platform supports it.
  • Pin base image versions so builds stay predictable.
  • Scan images for known vulnerabilities on a schedule.
  • Store image tags that match release numbers.

Common Mistakes

Forgetting the Static Folder

The standalone output does not include .next/static or public automatically. Missing copies lead to broken styles and missing images.

Binding to Localhost

A server that listens only on localhost inside a container rejects outside traffic. The HOSTNAME=0.0.0.0 setting opens the door.

Baking Secrets Into the Image

Anyone with the image can read its files. Supply secrets at runtime.

Quick Summary

Docker packages a Next.js app into a portable image. Turn on standalone output, write a multi-stage Dockerfile, copy the static folders, and pass secrets at runtime. The result runs the same way on any server that supports containers.