docker

Dockerfile for Node.js

Production-ready Dockerfile for Node.js applications with proper layer caching, security, and signal handling.

Overview

A well-structured Dockerfile for Node.js applications that follows best practices: non-root user, proper layer caching, minimal image size with Alpine, and correct signal handling for graceful shutdowns.

Configuration

# Dockerfile

# ── Base Stage ──
FROM node:20-alpine AS base

# Install security updates and tini for signal handling
RUN apk update && apk upgrade && apk add --no-cache tini

# Create non-root user
RUN addgroup -S appgroup && adduser -S appuser -G appgroup

# Set working directory
WORKDIR /app

# ── Dependencies Stage ──
FROM base AS deps

# Copy package files first (layer caching)
COPY package.json package-lock.json ./

# Install production dependencies only
RUN npm ci --omit=dev

# ── Build Stage ──
FROM base AS build

# Copy package files and install ALL dependencies (including devDependencies)
COPY package.json package-lock.json ./
RUN npm ci

# Copy source code
COPY . .

# Build the application (TypeScript, bundling, etc.)
RUN npm run build

# ── Production Stage ──
FROM base AS production

# Copy production dependencies from deps stage
COPY --from=deps /app/node_modules ./node_modules

# Copy built application from build stage
COPY --from=build /app/dist ./dist
COPY --from=build /app/package.json ./package.json

# Set environment
ENV NODE_ENV=production
ENV PORT=3000

# Switch to non-root user
USER appuser

# Expose the application port
EXPOSE 3000

# Use tini as entrypoint for proper signal handling
ENTRYPOINT ["/sbin/tini", "--"]

# Start the application
CMD ["node", "dist/index.js"]

Key Options Explained

  • node:20-alpine — Alpine-based image is ~50MB vs ~350MB for the full Debian image. Contains everything Node.js needs.
  • npm ci — Installs exact versions from package-lock.json and removes existing node_modules. Faster and more deterministic than npm install.
  • --omit=dev — Skips devDependencies in the production stage, reducing image size and attack surface.
  • Layer caching — Copying package.json before source code means dependencies are only reinstalled when package.json changes, not on every code change.
  • tini — A minimal init system that properly forwards signals (SIGTERM, SIGINT) to Node.js. Without this, docker stop sends SIGTERM to PID 1 which Node.js may not handle correctly.
  • USER appuser — Runs the application as a non-root user, limiting the impact of potential security vulnerabilities.

Common Modifications

  • Add native dependencies: If you need packages like sharp or bcrypt, install build tools: RUN apk add --no-cache python3 make g++ in the deps stage.
  • Use .dockerignore: Create a .dockerignore with node_modules, .git, .env, *.md, Dockerfile to speed up context transfer.
  • Health check: Add HEALTHCHECK --interval=30s --timeout=5s CMD node -e "fetch('http://localhost:3000/health')" before the CMD.
  • Monorepo support: Adjust COPY commands to include workspace root package.json and specific package directories.
  • Environment variables: Use ARG for build-time variables and ENV for runtime. Never bake secrets into the image.