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 frompackage-lock.jsonand removes existingnode_modules. Faster and more deterministic thannpm install.--omit=dev— Skips devDependencies in the production stage, reducing image size and attack surface.- Layer caching — Copying
package.jsonbefore source code means dependencies are only reinstalled whenpackage.jsonchanges, not on every code change. tini— A minimal init system that properly forwards signals (SIGTERM, SIGINT) to Node.js. Without this,docker stopsends 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
sharporbcrypt, install build tools:RUN apk add --no-cache python3 make g++in the deps stage. - Use
.dockerignore: Create a.dockerignorewithnode_modules,.git,.env,*.md,Dockerfileto 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.jsonand specific package directories. - Environment variables: Use
ARGfor build-time variables andENVfor runtime. Never bake secrets into the image.