Docs
/
Docker Kubernetes
Chapter 2

02 — Dockerfile & Images

Dockerfile Basics

# Base image
FROM node:20-alpine

# Set working directory
WORKDIR /app

# Copy dependency files first (layer caching)
COPY package*.json ./

# Install dependencies
RUN npm ci --only=production

# Copy application code
COPY . .

# Expose port (documentation)
EXPOSE 3000

# Default command
CMD ["node", "dist/server.js"]

Instructions

InstructionPurposeExample
FROMBase imageFROM node:20-alpine
WORKDIRSet working directoryWORKDIR /app
COPYCopy files from hostCOPY . .
ADDCopy + extract archives + URLsADD app.tar.gz /app
RUNExecute command (build time)RUN npm ci
CMDDefault command (run time)CMD ["node", "app.js"]
ENTRYPOINTFixed command (run time)ENTRYPOINT ["node"]
ENVSet environment variableENV NODE_ENV=production
ARGBuild-time variableARG VERSION=1.0
EXPOSEDocument portEXPOSE 3000
VOLUMECreate mount pointVOLUME /data
USERSet non-root userUSER node
HEALTHCHECKContainer health checkHEALTHCHECK CMD curl -f http://localhost:3000/health

CMD vs ENTRYPOINT

# CMD: can be overridden
CMD ["node", "app.js"]
# docker run my-app                → node app.js
# docker run my-app node test.js   → node test.js (overridden)

# ENTRYPOINT: fixed command, CMD provides default args
ENTRYPOINT ["node"]
CMD ["app.js"]
# docker run my-app                → node app.js
# docker run my-app test.js        → node test.js (only args overridden)

Multi-Stage Builds

Reduce final image size by separating build and runtime.

# Stage 1: Build
FROM node:20-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# Stage 2: Production (only runtime dependencies)
FROM node:20-alpine AS production
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY --from=build /app/dist ./dist

USER node
EXPOSE 3000
CMD ["node", "dist/server.js"]
Without multi-stage: ~800MB (includes devDependencies, source, build tools)
With multi-stage:    ~150MB (only production deps + built output)

React/Angular Multi-Stage

# Build
FROM node:20-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# Serve with Nginx
FROM nginx:alpine
COPY --from=build /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80

Layer Caching

Each Dockerfile instruction creates a layer. Docker caches layers that haven't changed.

# ❌ Bad — changing ANY source file invalidates npm install cache
COPY . .
RUN npm ci

# ✅ Good — package.json rarely changes, so npm ci layer is cached
COPY package*.json ./
RUN npm ci
COPY . .            # Only this layer rebuilds when source changes
Layer caching order (most stable first):
  1. FROM (base image)         — rarely changes
  2. System packages (apt-get) — rarely changes
  3. Dependency files           — changes occasionally
  4. Install dependencies       — cached if deps unchanged
  5. Application code           — changes frequently

.dockerignore

Exclude files from the build context (faster builds, smaller images).

node_modules
npm-debug.log
.git
.gitignore
.env
.env.local
dist
coverage
.vscode
*.md
docker-compose*.yml
Dockerfile

Health Checks

HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
  CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1
docker ps
# CONTAINER ID   STATUS
# abc123         Up 5 min (healthy)
# def456         Up 2 min (unhealthy)

Security Best Practices

# ✅ Use specific version tags (not :latest)
FROM node:20.11-alpine

# ✅ Run as non-root user
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser

# ✅ Use alpine base images (smaller attack surface)
FROM node:20-alpine    # ~180MB vs node:20 ~1GB

# ✅ Don't store secrets in image
# Use ENV at runtime, not in Dockerfile
# docker run -e SECRET=value my-app

# ✅ Use COPY instead of ADD (no auto-extraction)
COPY app.tar.gz /app/    # Just copies the file

Tagging Strategies

# Semantic versioning
docker build -t my-app:1.0.0 .
docker build -t my-app:1.0 .
docker build -t my-app:latest .

# Git SHA
docker build -t my-app:$(git rev-parse --short HEAD) .

# Date-based
docker build -t my-app:2024-01-15 .

# Best practice: tag with both version and git SHA
docker build -t my-app:1.0.0 -t my-app:abc1234 .

Key Takeaways

  • Order Dockerfile instructions from least to most frequently changing for layer caching
  • Use multi-stage builds — separate build and runtime for smaller images
  • Always use specific version tags (node:20-alpine, not node:latest)
  • Run as non-root user and use alpine base images for security
  • Use .dockerignore to exclude node_modules, .git, .env
  • COPY package*.json before COPY . to cache npm ci layer
  • Use HEALTHCHECK for orchestrators to know container health