Files
2026-08-28 11:08:57 +02:00
..

Next.js Docker Example - Standalone Mode

A production-ready example demonstrating how to Dockerize Next.js applications using standalone mode. This example showcases best practices for containerizing Next.js apps with Docker.

Features

  • Multi-stage Docker build for optimal image size
  • Next.js standalone mode for minimal production builds
  • Security best practices (non-root user)
  • Slim Linux base image for optimal compatibility and smaller size
  • BuildKit cache mounts for faster builds
  • Production-ready configuration

Prerequisites

Quick Start with Docker

Using Docker Compose

The compose.yml includes both Node.js and Bun configurations. Run one service at a time to avoid port conflicts.

Node.js:

# Run with Node.js
docker compose up nextjs-standalone --build

Bun:

# OR run with Bun
docker compose up nextjs-standalone-with-bun --build

Stop the application:

docker compose down

Using Docker Build

Node.js:

# Build the image
docker build -t nextjs-standalone-image .

# Run the container
docker run -p 3000:3000 nextjs-standalone-image

Bun:

# Build the image
docker build -f Dockerfile.bun -t nextjs-standalone-bun-image .

# Run the container
docker run -p 3000:3000 nextjs-standalone-bun-image

Open your browser: Navigate to http://localhost:3000

In existing projects

To add Docker support to your existing Next.js project:

  1. Copy the Dockerfile (or Dockerfile.bun for Bun) to your project root.
  2. Copy the .dockerignore to your project root.
  3. Add the following to your next.config.js (or next.config.ts):
// next.config.js
module.exports = {
  output: "standalone",
};

This will build the project as a standalone app inside the Docker image.

Project Structure

nextjs-docker/
├── app/                    # Next.js App Router directory
│   ├── layout.tsx          # Root layout with metadata
│   ├── page.tsx            # Home page with example content
│   └── globals.css         # Global styles with Tailwind CSS v4
├── public/                 # Static assets
│   └── next.svg            # Next.js logo
├── Dockerfile              # Multi-stage Docker configuration (Node.js)
├── Dockerfile.bun          # Multi-stage Docker configuration (Bun)
├── compose.yml             # Docker Compose configuration (Node.js & Bun services)
├── next.config.ts          # Next.js configuration (standalone mode)
├── postcss.config.js       # PostCSS configuration for Tailwind CSS
├── tsconfig.json           # TypeScript configuration
├── package.json            # Dependencies and scripts
└── README.md               # This file

Configuration

Next.js Standalone Mode

The next.config.ts file is configured with output: "standalone":

import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  output: "standalone",
};

export default nextConfig;

The standalone output mode creates a minimal, self-contained production build optimized for containerized deployments. When enabled, Next.js generates a .next/standalone directory containing only the essential files needed to run your application, excluding unnecessary dependencies and files. This results in significantly smaller Docker images and faster container startup times.

Learn more about Next.js standalone output in the official documentation.

Dockerfile Highlights (Node.js)

  • Multi-stage build: Separates dependency installation (dependencies), build (builder), and runtime (runner) stages
  • Slim Linux: Uses slim image tag for optimal compatibility and smaller image size
  • BuildKit cache mounts: Speeds up builds by caching package manager stores (/root/.npm, /usr/local/share/.cache/yarn, /root/.local/share/pnpm/store). See the Dockerfile for an optional .next/cache mount to speed up rebuilds.
  • Non-root user: Runs as node user for security
  • Optimized layers: Leverages Docker layer caching effectively
  • Standalone output: Copies only the necessary files from .next/standalone and .next/static
  • Writable .next directory: The .next directory is created and owned by the node user so the server can write prerender cache and optimized images at runtime
  • Node.js version maintenance: Uses Node.js 24.13.0-slim (latest LTS at time of writing). Update the NODE_VERSION ARG to the latest LTS version for security updates.

Dockerfile.bun Highlights (Bun)

  • Multi-stage build: Same three-stage pattern optimized for Bun
  • Official Bun image: Uses oven/bun:1 for optimal Bun performance
  • Non-root user: Runs as built-in bun user for security
  • Frozen lockfile: Uses bun.lock for reproducible builds
  • Standalone output: Same optimized output as the Node.js version, with writable .next directory for runtime cache

Why Node.js slim image tag?: The slim variant provides optimal compatibility with npm packages and native dependencies while maintaining a smaller image size (~226MB). Slim uses glibc (standard Linux), ensuring better compatibility than Alpine's musl libc, which can cause issues with some npm packages. This makes it ideal for public examples where reliability and compatibility are priorities.

When to use Alpine?: Consider using node:24.11.1-alpine instead if:

  • Image size is critical: Alpine images are typically ~100MB smaller than slim variants (~110MB base vs ~226MB)
  • Your dependencies are compatible: Your npm packages don't require native binaries that depend on glibc
  • You've tested thoroughly: You've verified all your dependencies work correctly with musl libc
  • Security-focused deployments: Alpine's minimal attack surface can be beneficial for security-sensitive applications

To switch to Alpine, simply change the NODE_VERSION ARG in the Dockerfile to 24.11.1-alpine.

Important

Node.js Version Maintenance: This Dockerfile uses Node.js 24.13.0-slim, which was the latest LTS version at the time of writing. To ensure security and stay up-to-date, regularly check and update the NODE_VERSION ARG in the Dockerfile to the latest Node.js LTS version. Check the latest version at Nodejs official website and browse available Node.js images on Docker Hub.

Environment Variables

The .dockerignore in this example excludes .env, so a local development file — which usually holds real credentials — is never copied into the build context or the final image.

The consequence is worth knowing up front: a value you rely on from .env is undefined inside the container, even though the same code works with next build && next start locally. Use one of the following instead.

Secrets and server-only values: pass them at run time

Values read on the server at request time (Route Handlers, dynamically rendered Server Components, Server Actions) are read from the environment when the request happens, so they need nothing at build time:

docker run -p 3000:3000 -e MY_SECRET=value nextjs-standalone-image

or in compose.yml:

services:
  nextjs-standalone:
    environment:
      MY_SECRET: value
    # or, to read a file that is not committed:
    # env_file:
    #   - .env.production.local

Prefer this wherever it works: it keeps one image promotable across environments instead of baking values into a per-environment build.

Non-secret build-time configuration: use .env.production

.env.production is intentionally not ignored, so it is available to next build and loaded by the server at run time. Use it only for values that are safe to publish:

# .env.production
NEXT_PUBLIC_SITE_URL=https://example.com

Public values needed in the client bundle: use a build argument

NEXT_PUBLIC_* values referenced from Client Components are inlined into the JavaScript sent to the browser, so they have to be present while next build runs. Add them to the builder stage:

ARG NEXT_PUBLIC_SITE_URL
ENV NEXT_PUBLIC_SITE_URL=$NEXT_PUBLIC_SITE_URL
# ... before the build step
docker build \
  --build-arg NEXT_PUBLIC_SITE_URL=https://example.com \
  -t nextjs-standalone-image .

Important

Never pass secrets as build arguments. They are recoverable from the image history, and NEXT_PUBLIC_* values are sent to the browser by definition.

Important

Any env file that is present in the build context is also copied into .next/standalone by output: "standalone", and this Dockerfile copies that directory wholesale into the runner stage. The file therefore ships inside the image and is readable by anyone who can pull it. Keep credentials out of committed env files and pass them at run time.

To build a separate image per environment instead, see with-docker-multi-env.

Deployment

This example can be deployed to any container-based platform:

  • Google Cloud Run
  • AWS ECS/Fargate
  • Azure Container Instances
  • DigitalOcean App Platform
  • Any Kubernetes cluster

Deploying to Google Cloud Run

  1. Install the Google Cloud SDK so you can use gcloud on the command line.

  2. Run gcloud auth login to log in to your account.

  3. Create a new project in Google Cloud Run (e.g. nextjs-docker). Ensure billing is turned on.

  4. Build your container image using Cloud Build:

    gcloud builds submit --tag gcr.io/PROJECT-ID/nextjs-docker --project PROJECT-ID
    

    This will also enable Cloud Build for your project.

  5. Deploy to Cloud Run:

    gcloud run deploy --image gcr.io/PROJECT-ID/nextjs-docker --project PROJECT-ID --platform managed --allow-unauthenticated
    
    • You will be prompted for the service name: press Enter to accept the default name, nextjs-docker.
    • You will be prompted for region: select the region of your choice, for example us-central1.

Learn More