Are you an LLM? You can read better optimized documentation at /next-dynenv/guide/docker.md for this page in Markdown format
Docker Deployment
Build one Docker image, deploy it everywhere—the way containers were meant to be used.
The Goal
Build once, deploy anywhere with different runtime configurations:
bash
# Same image, different environments
docker run -e NEXT_PUBLIC_API_URL=https://staging-api.com my-app
docker run -e NEXT_PUBLIC_API_URL=https://prod-api.com my-appThis is the fundamental promise of containers—environment-agnostic build artifacts.
Basic Dockerfile
dockerfile
# Dockerfile
FROM node:22-alpine AS base
# Install dependencies
FROM base AS deps
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN corepack enable pnpm && pnpm install --frozen-lockfile
# Build
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN corepack enable pnpm && pnpm build
# Production
FROM base AS runner
WORKDIR /app
ENV NODE_ENV=production
# Create non-root user
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
ENV PORT=3000
CMD ["node", "server.js"]Next.js Configuration
Enable standalone output for smaller Docker images:
js
// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
output: 'standalone',
}
module.exports = nextConfigWhy Standalone? Standalone output creates a minimal production server with only the dependencies you need. This
dramatically reduces Docker image size (often 10x smaller). :::
Application Setup
tsx
// app/layout.tsx
import { PublicEnvScript } from 'next-dynenv'
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<head>
<PublicEnvScript />
</head>
<body>{children}</body>
</html>
)
}Running the Container
With Environment Variables
bash
# Build the image
docker build -t my-nextjs-app .
# Run with environment variables
docker run -p 3000:3000 \
-e NEXT_PUBLIC_API_URL=https://api.example.com \
-e NEXT_PUBLIC_APP_NAME=MyApp \
-e NEXT_PUBLIC_DEBUG=false \
my-nextjs-appWith Environment File
bash
# .env.production
NEXT_PUBLIC_API_URL=https://api.example.com
NEXT_PUBLIC_APP_NAME=MyApp
NEXT_PUBLIC_ANALYTICS_ID=UA-123456
# Run with env file
docker run -p 3000:3000 --env-file .env.production my-nextjs-appDocker Compose
yaml
# docker-compose.yml
version: '3.8'
services:
app:
build: .
ports:
- '3000:3000'
environment:
- NEXT_PUBLIC_API_URL=https://api.example.com
- NEXT_PUBLIC_APP_NAME=MyApp
- NEXT_PUBLIC_DEBUG=false
- DATABASE_URL=postgres://db:5432/myapp
depends_on:
- db
db:
image: postgres:15
environment:
POSTGRES_DB: myapp
POSTGRES_USER: user
POSTGRES_PASSWORD: password
volumes:
- postgres_data:/var/lib/postgresql/data
volumes:
postgres_data:Multiple Environments
yaml
# docker-compose.staging.yml
version: '3.8'
services:
app:
build: .
environment:
- NEXT_PUBLIC_API_URL=https://staging-api.example.com
- NEXT_PUBLIC_ENV=stagingyaml
# docker-compose.production.yml
version: '3.8'
services:
app:
build: .
environment:
- NEXT_PUBLIC_API_URL=https://api.example.com
- NEXT_PUBLIC_ENV=productionbash
# Run staging
docker compose -f docker-compose.yml -f docker-compose.staging.yml up
# Run production
docker compose -f docker-compose.yml -f docker-compose.production.yml upKubernetes
ConfigMap
yaml
# configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
data:
NEXT_PUBLIC_API_URL: 'https://api.example.com'
NEXT_PUBLIC_APP_NAME: 'MyApp'
NEXT_PUBLIC_ENV: 'production'Deployment
yaml
# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: nextjs-app
spec:
replicas: 3
selector:
matchLabels:
app: nextjs-app
template:
metadata:
labels:
app: nextjs-app
spec:
containers:
- name: app
image: my-registry/my-nextjs-app:latest
ports:
- containerPort: 3000
envFrom:
- configMapRef:
name: app-config
env:
# Secrets should come from Secret resources
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: app-secrets
key: database-urlSecret
yaml
# secret.yaml
apiVersion: v1
kind: Secret
metadata:
name: app-secrets
type: Opaque
stringData:
database-url: 'postgres://user:pass@db:5432/myapp'Health Checks
Add a health endpoint:
ts
// app/api/health/route.ts
import { env } from 'next-dynenv'
export function GET() {
return Response.json({
status: 'healthy',
environment: env('NEXT_PUBLIC_ENV', 'unknown'),
timestamp: new Date().toISOString(),
})
}Docker health check:
dockerfile
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:3000/api/health || exit 1Multi-Stage Build Optimization
dockerfile
# Optimized Dockerfile
FROM node:22-alpine AS base
RUN apk add --no-cache libc6-compat
FROM base AS deps
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN corepack enable pnpm && pnpm install --frozen-lockfile --prod
FROM base AS builder
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN corepack enable pnpm && pnpm install --frozen-lockfile
COPY . .
ENV NEXT_TELEMETRY_DISABLED=1
RUN pnpm build
FROM base AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
ENV PORT=3000
ENV HOSTNAME="0.0.0.0"
CMD ["node", "server.js"]Next Steps
- Vercel Deployment - Deploy to Vercel
- Other Platforms - Railway, Fly.io, and more
- Security - Security best practices