Dockerfile Basics
A Dockerfile is a recipe for building an image.
# Base image
FROM node:20-alpine
# Set working directory
WORKDIR /app
# Copy dependency manifests first (cache layer)
COPY package*.json ./
# Install dependencies
RUN npm ci --only=production
# Copy source code
COPY . .
# Expose port
EXPOSE 3000
# Start command
CMD ["node", "dist/server.js"]Layer Caching
Docker caches each layer. Copy package.json before your source code so npm install doesn't re-run on every code change:
# ✅ Good — npm install only re-runs when package.json changes
COPY package*.json ./
RUN npm ci
COPY . .
# ❌ Bad — npm install re-runs on every source change
COPY . .
RUN npm ciMulti-Stage Builds
Use multiple FROM statements to keep the final image small:
# Stage 1: Build
FROM node:20 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Stage 2: Production image
FROM node:20-alpine AS production
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
EXPOSE 3000
USER node
CMD ["node", "dist/server.js"]The final image only contains the dist/ output and production dependencies — no dev tools, no source code.
.dockerignore
node_modules
.git
dist
*.md
.env
coverage
Security Best Practices
# Run as non-root user
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser
# Use specific tags, not :latest
FROM node:20.12.0-alpine3.19
# Don't store secrets in ENV
# Use Docker secrets or mount files at runtime insteadNever COPY .env into an image or set secrets via ENV. Images can be extracted from registries.