Next.js, when containerized with Docker, doesn’t just run your app; it fundamentally changes how you think about deployment by allowing you to package your entire application environment, dependencies and all, into a portable, self-contained unit.

Let’s see this in action. Imagine a simple Next.js app.

npx create-next-app@latest my-docker-app
cd my-docker-app

Now, create a Dockerfile in the root of your project:

# Stage 1: Build the Next.js application
FROM node:20-alpine AS builder

WORKDIR /app

COPY package.json package-lock.json ./
RUN npm ci

COPY . .
RUN npm run build

# Stage 2: Serve the Next.js application with a production server
FROM node:20-alpine AS runner

WORKDIR /app

ENV NODE_ENV production
# Uncomment the following line if you want to disable telemetry during runtime.
# ENV NEXT_TELEMETRY_DISABLED 1

COPY --from=builder /app/.next ./.next
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./package.json
COPY --from=builder /app/public ./public

EXPOSE 3000

CMD ["npm", "start"]

And a .dockerignore file to keep things clean:

node_modules
.next
npm-cache
Dockerfile
.dockerignore

To build the Docker image, run:

docker build -t my-nextjs-app .

And to run the container:

docker run -p 3000:3000 my-nextjs-app

Now, if you navigate to http://localhost:3000 in your browser, you’ll see your Next.js application running inside a Docker container.

The core problem Next.js Docker solves is environment consistency. Without Docker, deploying a Next.js app often involves meticulously configuring servers, installing Node.js versions, managing dependencies, and ensuring build tools are present on each deployment target. This is error-prone and time-consuming. Docker eliminates this by bundling everything needed to run your app into a single image.

The Dockerfile above uses a multi-stage build, a crucial pattern for optimizing Docker images. Stage 1, builder, is where the app is compiled. It starts from a Node.js image, copies your package.json and package-lock.json, installs dependencies with npm ci (which is faster and more reliable for CI/CD than npm install), and then copies the rest of your source code to run npm run build. This stage produces the optimized .next directory containing your statically generated pages and serverless functions.

Stage 2, runner, is where the actual production server runs. It starts from another lean Node.js image (node:20-alpine is significantly smaller than a full Debian-based image). It then only copies the necessary artifacts from the builder stage: the .next directory, your node_modules, package.json, and the public folder. This results in a much smaller final image because it doesn’t include build tools or development dependencies. The CMD ["npm", "start"] instruction tells Docker how to start your Next.js application using its production server. EXPOSE 3000 documents that the container listens on port 3000, which we then map to our host machine’s port 3000 with docker run -p 3000:3000.

The mental model here is about creating immutable infrastructure. Your Docker image is a snapshot of your application and its environment. When you deploy, you’re deploying this image, not just code. This means every environment (development, staging, production) can be identical, drastically reducing "it works on my machine" issues.

You control the deployment environment entirely through the Dockerfile. Want a specific Node.js version? Change FROM node:20-alpine to FROM node:18-alpine. Need to set environment variables for your production build? Add ENV MY_API_KEY your_secret_value in the runner stage. Want to optimize further? You can use RUN npm prune --production in the runner stage to remove any remaining unused dependencies from node_modules.

The CMD ["npm", "start"] is technically running next start under the hood. However, you can also directly run next start in the CMD instruction: CMD ["node_modules/.bin/next", "start"]. This bypasses the npm script and can slightly reduce image size and startup time by removing one layer of indirection.

Once you’ve mastered self-hosting, the next logical step is to explore how Next.js integrates with serverless platforms like AWS Lambda or Vercel, which abstract away much of the container management for you while leveraging similar build principles.

Want structured learning?

Take the full Nextjs course →