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
| Instruction | Purpose | Example |
|---|---|---|
FROM | Base image | FROM node:20-alpine |
WORKDIR | Set working directory | WORKDIR /app |
COPY | Copy files from host | COPY . . |
ADD | Copy + extract archives + URLs | ADD app.tar.gz /app |
RUN | Execute command (build time) | RUN npm ci |
CMD | Default command (run time) | CMD ["node", "app.js"] |
ENTRYPOINT | Fixed command (run time) | ENTRYPOINT ["node"] |
ENV | Set environment variable | ENV NODE_ENV=production |
ARG | Build-time variable | ARG VERSION=1.0 |
EXPOSE | Document port | EXPOSE 3000 |
VOLUME | Create mount point | VOLUME /data |
USER | Set non-root user | USER node |
HEALTHCHECK | Container health check | HEALTHCHECK 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, notnode:latest) - Run as non-root user and use alpine base images for security
- Use
.dockerignoreto excludenode_modules,.git,.env COPY package*.jsonbeforeCOPY .to cachenpm cilayer- Use
HEALTHCHECKfor orchestrators to know container health