Dockerizing a Node.js/Next.js application: multi-stage builds, .dockerignore, health checks, non-root users, and layer caching strategies.
Dockerizing a Node.js/Next.js application: multi-stage builds, .dockerignore, health checks, non-root users, and layer caching strategies.
BeforeMerge offers hundreds of code review rules, guides, and detection patterns to help your team ship better code.
This tutorial covers containerizing a Node.js/Next.js app with production best practices.
# Stage 1: Install dependencies
FROM node:20-alpine AS deps
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN corepack enable pnpm && pnpm install --frozen-lockfile
# Stage 2: Build the application
FROM node:20-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN corepack enable pnpm && pnpm build
# Stage 3: Production image
FROM node:20-alpine 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 only production artifacts
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.config.ts
export default {
output: "standalone",
};The standalone output creates a minimal server.js with only necessary dependencies.
node_modules
.next
.git
.gitignore
.env*.local
Dockerfile
docker-compose.yml
README.md
.vscode
.husky
coverageA good .dockerignore speeds up builds by reducing the build context.
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:3000/api/health || exit 1Create a simple health endpoint:
// app/api/health/route.ts
export function GET() {
return Response.json({ status: "ok" });
}Docker caches each layer. Order instructions from least to most frequently changed:
# 1. Base image (rarely changes)
FROM node:20-alpine
# 2. System dependencies (rarely changes)
RUN apk add --no-cache libc6-compat
# 3. Package files (changes when deps change)
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile
# 4. Source code (changes frequently)
COPY . .
RUN pnpm build# docker-compose.yml
services:
app:
build:
context: .
target: deps # Stop at deps stage for dev
volumes:
- .:/app
- /app/node_modules # Prevent overwriting node_modules
ports:
- "3000:3000"
environment:
- DATABASE_URL=postgresql://postgres:postgres@db:5432/myapp
depends_on:
- db
db:
image: postgres:16-alpine
environment:
POSTGRES_PASSWORD: postgres
POSTGRES_DB: myapp
volumes:
- pgdata:/var/lib/postgresql/data
ports:
- "5432:5432"
volumes:
pgdata:-alpine images for smaller attack surfacelatestdocker scout cves or Trivy to find vulnerabilities