Docker ensures your Next.js app runs identically across all environments. Here is a production-ready setup.
Enable Standalone Output
// next.config.ts
import type { NextConfig } from "next";
const config: NextConfig = {
output: "standalone",
};
export default config;
Multi-Stage Dockerfile
# Dockerfile
# Stage 1: Install dependencies
FROM node:22-alpine AS deps
RUN apk add --no-cache libc6-compat
WORKDIR /app
# Install pnpm
RUN corepack enable && corepack prepare pnpm@latest --activate
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile
# Stage 2: Build the application
FROM node:22-alpine AS builder
WORKDIR /app
RUN corepack enable && corepack prepare pnpm@latest --activate
COPY --from=deps /app/node_modules ./node_modules
COPY . .
# Set build-time environment variables
ARG NEXT_PUBLIC_SITE_URL
ENV NEXT_PUBLIC_SITE_URL=$NEXT_PUBLIC_SITE_URL
RUN pnpm build
# Stage 3: Production runner
FROM node:22-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
# Create non-root user
RUN addgroup --system --gid 1001 nodejs && \
adduser --system --uid 1001 nextjs
# Copy standalone output
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"]
Docker Ignore
# .dockerignore
node_modules
.next
.git
.gitignore
*.md
.env*.local
.vscode
.idea
coverage
cypress
tests
__tests__
Build and Run
# Build the image
docker build -t my-nextjs-app .
# Run the container
docker run -p 3000:3000 \
-e DATABASE_URL="postgres://user:pass@host:5432/db" \
my-nextjs-app
Docker Compose for Development
# docker-compose.yml
services:
app:
build:
context: .
dockerfile: Dockerfile.dev
ports:
- "3000:3000"
volumes:
- .:/app
- /app/node_modules
- /app/.next
environment:
- DATABASE_URL=postgres://postgres:postgres@db:5432/myapp
- REDIS_URL=redis://cache:6379
depends_on:
db:
condition: service_healthy
cache:
condition: service_started
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: myapp
volumes:
- pgdata:/var/lib/postgresql/data
ports:
- "5432:5432"
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 3s
retries: 5
cache:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
pgdata:
Development Dockerfile
# Dockerfile.dev
FROM node:22-alpine
WORKDIR /app
RUN corepack enable && corepack prepare pnpm@latest --activate
COPY package.json pnpm-lock.yaml ./
RUN pnpm install
COPY . .
CMD ["pnpm", "dev"]
Production Docker Compose
# docker-compose.prod.yml
services:
app:
build:
context: .
args:
NEXT_PUBLIC_SITE_URL: https://example.com
ports:
- "3000:3000"
environment:
- DATABASE_URL=${DATABASE_URL}
- REDIS_URL=redis://cache:6379
depends_on:
- cache
restart: unless-stopped
deploy:
resources:
limits:
memory: 512M
cpus: "0.5"
cache:
image: redis:7-alpine
restart: unless-stopped
volumes:
- redis-data:/data
volumes:
redis-data:
Health Check Endpoint
// app/api/health/route.ts
import { NextResponse } from "next/server";
export async function GET() {
const health = {
status: "ok",
timestamp: new Date().toISOString(),
uptime: process.uptime(),
memory: process.memoryUsage(),
};
return NextResponse.json(health);
}
Add it to the Dockerfile:
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:3000/api/health || exit 1
GitHub Actions for Docker Build
# .github/workflows/docker.yml
name: Build and Push Docker Image
on:
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: |
ghcr.io/${{ github.repository }}:latest
ghcr.io/${{ github.repository }}:${{ github.sha }}
cache-from: type=gha
cache-to: type=gha,mode=max
build-args: |
NEXT_PUBLIC_SITE_URL=${{ vars.SITE_URL }}
Need Docker and DevOps Setup?
We containerize and deploy web applications with production-grade infrastructure. Contact us to discuss your deployment needs.