Improve with-docker example env var usage documentation. Closes: https://github.com/vercel/next.js/issues/97959
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:
- Copy the
Dockerfile(orDockerfile.bunfor Bun) to your project root. - Copy the
.dockerignoreto your project root. - Add the following to your
next.config.js(ornext.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
slimimage 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/cachemount to speed up rebuilds. - Non-root user: Runs as
nodeuser for security - Optimized layers: Leverages Docker layer caching effectively
- Standalone output: Copies only the necessary files from
.next/standaloneand.next/static - Writable
.nextdirectory: The.nextdirectory is created and owned by thenodeuser 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_VERSIONARG 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:1for optimal Bun performance - Non-root user: Runs as built-in
bunuser for security - Frozen lockfile: Uses
bun.lockfor reproducible builds - Standalone output: Same optimized output as the Node.js version, with writable
.nextdirectory 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_VERSIONARG 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/standalonebyoutput: "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
-
Install the Google Cloud SDK so you can use
gcloudon the command line. -
Run
gcloud auth loginto log in to your account. -
Create a new project in Google Cloud Run (e.g.
nextjs-docker). Ensure billing is turned on. -
Build your container image using Cloud Build:
gcloud builds submit --tag gcr.io/PROJECT-ID/nextjs-docker --project PROJECT-IDThis will also enable Cloud Build for your project.
-
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.
- You will be prompted for the service name: press Enter to accept the default name,
Learn More
- Next.js Documentation - Comprehensive Next.js documentation
- Next.js Templates - Browse and deploy Next.js templates
- Next.js Examples - Discover boilerplate example projects
- Deploy to Vercel - Instantly deploy your Next.js site
- Learn Docker - Get started with Docker fundamentals, containerization, and deployment
- Docker Documentation - Comprehensive Docker documentation and reference guides
- React.js Docker Guide - Official Docker guide for React.js applications following best practices for containerization