Docker for Next.js: A Production Dockerfile That Actually Works
Most Next.js Dockerfiles you find online either don't handle standalone output correctly, bloat the image with dev dependencies, or don't use multi-stage builds. Here's a production-ready Dockerfile with explanations for every decision.
The Complete Dockerfile
# Stage 1: Install dependencies
FROM node:20-alpine AS deps
RUN apk add --no-cache libc6-compat
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
# Stage 2: Build the application
FROM node:20-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
# Disable telemetry during build
ENV NEXT_TELEMETRY_DISABLED=1
# Pass build-time env vars
ARG NEXT_PUBLIC_APP_URL
ENV NEXT_PUBLIC_APP_URL=$NEXT_PUBLIC_APP_URL
RUN npm run build
# Stage 3: Production runner
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
# Non-root user for security
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
# Copy only what's needed to run
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"]
Required: Enable Standalone Output
The Dockerfile uses Next.js standalone output, which bundles only the necessary files for production (no node_modules in the final image).
Add to next.config.ts:
const nextConfig = {
output: 'standalone',
};
export default nextConfig;
Without this, the final COPY commands will fail — .next/standalone won't exist.
.dockerignore
.git
.next
node_modules
.env
.env.local
.env.*.local
*.log
README.md
.github
Critical: always ignore .env files. Never bake secrets into Docker images.
Build and Run
# Build (pass public env vars as build args)
docker build \
--build-arg NEXT_PUBLIC_APP_URL=https://myapp.com \
-t my-nextjs-app .
# Run (pass secret env vars at runtime, not build time)
docker run -p 3000:3000 \
-e DATABASE_URL='postgresql://...' \
-e NEXTAUTH_SECRET='...' \
-e ANTHROPIC_API_KEY='...' \
my-nextjs-app
The Environment Variable Rule
Next.js has two types of env vars:
NEXT_PUBLIC_* — baked into the client bundle at build time. Must be passed as --build-arg during docker build. Never contain secrets.
Everything else — server-side only. Pass at runtime with -e or via --env-file. This is where secrets go: DATABASE_URL, STRIPE_SECRET_KEY, ANTHROPIC_API_KEY, etc.
Image Size
Expected sizes with this Dockerfile:
-
depsstage: ~300MB -
builderstage: ~500MB -
runnerstage: ~150MB (production image)
The multi-stage build drops the build toolchain and dev dependencies. Only the compiled output runs in production.
Docker Compose for Local Dev
docker-compose.yml:
version: '3.8'
services:
app:
build: .
ports:
- '3000:3000'
environment:
- DATABASE_URL=postgresql://postgres:postgres@db:5432/mydb
- NEXTAUTH_SECRET=dev-secret-change-in-production
depends_on:
- db
db:
image: postgres:16-alpine
environment:
POSTGRES_PASSWORD: postgres
POSTGRES_DB: mydb
volumes:
- postgres_data:/var/lib/postgresql/data
ports:
- '5432:5432'
volumes:
postgres_data:
Pre-Configured in the Starter Kit
The AI SaaS Starter Kit includes this Dockerfile, .dockerignore, and docker-compose.yml for local development — plus Railway and Fly.io configs.
Atlas — building at whoffagents.com
Top comments (0)