Appearance
Buổi 6: Multi-stage Builds & Optimization
🎯 Mục tiêu
- Hiểu multi-stage build và cách sử dụng
- Tối ưu layer caching trong Dockerfile
- Sử dụng .dockerignore đúng cách
- Giảm image size đáng kể
- So sánh trước và sau khi tối ưu
1. Vấn đề: Image quá lớn
Dockerfile chưa tối ưu
dockerfile
# ❌ BAD: Image quá lớn!
FROM node:20
WORKDIR /app
COPY . .
RUN npm install
RUN npm run build
EXPOSE 3000
CMD ["node", "dist/server.js"]Vấn đề:
- Base image
node:20≈ 1GB - Chứa cả
devDependencies(webpack, typescript...) - Chứa source code gốc (không cần trong production)
- Chứa
node_modulesđầy đủ
┌─────────────────────────────────┐
│ node:20 base ~1.0 GB │
│ node_modules (dev) ~300 MB │
│ Source code ~50 MB │
│ Build output ~5 MB │
│ ───────────────────────────── │
│ TỔNG: ~1.35 GB │ ← 😱
└─────────────────────────────────┘2. Multi-stage Build
Khái niệm
Multi-stage build cho phép dùng nhiều FROM trong cùng một Dockerfile. Mỗi FROM là một "stage". Stage cuối chỉ chứa những gì cần cho production.
Dockerfile tối ưu
dockerfile
# ── Stage 1: Build ──────────────────────
FROM node:20-alpine AS builder
WORKDIR /app
# Copy package files trước (cache layer)
COPY package*.json ./
RUN npm ci
# Copy source và build
COPY . .
RUN npm run build
# ── Stage 2: Production ────────────────
FROM node:20-alpine AS production
WORKDIR /app
# Chỉ copy production dependencies
COPY package*.json ./
RUN npm ci --omit=dev
# Copy build output từ stage builder
COPY --from=builder /app/dist ./dist
# Chạy với user non-root
USER node
EXPOSE 3000
CMD ["node", "dist/server.js"]So sánh kết quả
┌────────────────────────────────────────────┐
│ TRƯỚC (single-stage) │
│ node:20 ~1.0 GB │
│ node_modules ~300 MB (all deps) │
│ Source + Build ~55 MB │
│ ─────────────────────── │
│ TỔNG: ~1.35 GB 😱 │
└────────────────────────────────────────────┘
┌────────────────────────────────────────────┐
│ SAU (multi-stage) │
│ node:20-alpine ~130 MB │
│ node_modules ~50 MB (prod only) │
│ dist/ ~5 MB │
│ ─────────────────────── │
│ TỔNG: ~185 MB ✅ (-86%) │
└────────────────────────────────────────────┘Multi-stage cho các ngôn ngữ khác
Go – Tối ưu cực đại:
dockerfile
# Build stage
FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o main .
# Production – chỉ chứa binary!
FROM scratch
COPY --from=builder /app/main /main
EXPOSE 8080
ENTRYPOINT ["/main"]
# Image size: ~10-15MB! 🎉Rust – Static binary:
dockerfile
FROM rust:1.77-alpine AS builder
WORKDIR /app
RUN apk add --no-cache musl-dev
COPY . .
RUN cargo build --release
FROM scratch
COPY --from=builder /app/target/release/myapp /myapp
EXPOSE 8080
ENTRYPOINT ["/myapp"]
# Image size: ~5-10MB! 🎉Python:
dockerfile
FROM python:3.12-slim AS builder
WORKDIR /app
COPY requirements.txt .
RUN pip install --user --no-cache-dir -r requirements.txt
FROM python:3.12-slim
WORKDIR /app
COPY --from=builder /root/.local /root/.local
COPY . .
ENV PATH=/root/.local/bin:$PATH
EXPOSE 8000
CMD ["python", "app.py"]3. Layer Caching
Cách Docker cache hoạt động
Dockerfile instruction │ Cache?
──────────────────────────┼─────────
FROM node:20-alpine │ ✅ Cached (nếu image không đổi)
WORKDIR /app │ ✅ Cached
COPY package*.json ./ │ ✅ Cached (nếu package.json không đổi)
RUN npm ci │ ✅ Cached (nếu layer trước cached)
COPY . . │ ❌ MISS (source code thay đổi)
RUN npm run build │ ❌ MISS (phải rebuild)Quy tắc: Khi một layer thay đổi, tất cả layer phía sau phải build lại.
Tối ưu thứ tự COPY
dockerfile
# ❌ BAD: Mỗi khi sửa code → npm install lại
FROM node:20-alpine
WORKDIR /app
COPY . . # ← Thay đổi mỗi khi sửa code
RUN npm install # ← Phải chạy lại mỗi lần!
CMD ["node", "server.js"]
# ✅ GOOD: Chỉ npm install khi package.json thay đổi
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./ # ← Ít khi thay đổi → cached
RUN npm install # ← Cached nếu package.json không đổi
COPY . . # ← Thay đổi khi sửa code
CMD ["node", "server.js"] # ← Vẫn phải copy lại, nhưng nhanhGộp RUN commands
dockerfile
# ❌ BAD: Mỗi RUN tạo một layer
RUN apt-get update
RUN apt-get install -y curl
RUN apt-get install -y wget
RUN apt-get clean
# ✅ GOOD: Gộp thành 1 layer + cleanup
RUN apt-get update && \
apt-get install -y --no-install-recommends \
curl \
wget && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*4. .dockerignore
Tại sao cần .dockerignore?
Khi build, Docker gửi toàn bộ build context (thư mục hiện tại) đến Docker daemon. Nếu không ignore, nó sẽ gửi cả node_modules, .git, v.v.
File .dockerignore mẫu
dockerignore
# Dependencies
node_modules
.npm
# Build output (sẽ build trong container)
dist
build
# Version control
.git
.gitignore
# IDE
.vscode
.idea
*.swp
*.swo
# OS files
.DS_Store
Thumbs.db
# Docker files (tránh recursive)
Dockerfile
docker-compose.yml
.dockerignore
# Environment files
.env
.env.local
.env.*.local
# Logs
logs
*.log
npm-debug.log*
# Tests
coverage
__tests__
*.test.js
*.spec.js
# Documentation
README.md
docs/
LICENSESo sánh build context
bash
# Không có .dockerignore
$ docker build -t myapp .
Sending build context to Docker daemon 500MB 😱
# Có .dockerignore
$ docker build -t myapp .
Sending build context to Docker daemon 2.5MB ✅5. Các kỹ thuật giảm image size
Bảng so sánh base images
| Base Image | Size | Dùng khi |
|---|---|---|
node:20 | ~1GB | Cần full OS |
node:20-slim | ~200MB | Production web apps |
node:20-alpine | ~130MB | Muốn nhẹ nhất |
python:3.12 | ~1GB | Cần compile C extensions |
python:3.12-slim | ~52MB | Production |
python:3.12-alpine | ~17MB | Siêu nhẹ (⚠️ musl libc) |
golang:1.22 | ~800MB | Build stage |
alpine:3.19 | ~7MB | Minimal runtime |
scratch | 0MB | Static binaries |
distroless | ~2MB | Google's minimal images |
Checklist tối ưu
| # | Kỹ thuật | Tiết kiệm |
|---|---|---|
| 1 | Dùng -alpine hoặc -slim | ~60-80% |
| 2 | Multi-stage build | ~50-90% |
| 3 | .dockerignore | Build nhanh hơn |
| 4 | Gộp RUN commands | ~5-10% |
| 5 | --no-cache-dir (pip) | ~10-20% |
| 6 | npm ci --omit=dev | ~30-50% |
| 7 | Xóa cache trong RUN | ~5-10% |
| 8 | Dùng scratch (Go/Rust) | ~95%+ |
6. Kiểm tra image size
bash
# Xem size tổng
$ docker images myapp
REPOSITORY TAG SIZE
myapp latest 185MB
# Xem chi tiết từng layer
$ docker history myapp:latest
IMAGE CREATED CREATED BY SIZE
a1b2c3d4e5f6 2 minutes ago CMD ["node" "dist/ser… 0B
b2c3d4e5f6a7 2 minutes ago EXPOSE 3000 0B
c3d4e5f6a7b8 2 minutes ago COPY dir:abc123 /app… 5.2MB
d4e5f6a7b8c9 3 minutes ago RUN npm ci --omit=dev 48MB
e5f6a7b8c9d0 5 minutes ago COPY package*.json ./ 1.2KB
...
# Tool phân tích chi tiết: dive
$ docker run --rm -it \
-v /var/run/docker.sock:/var/run/docker.sock \
wagoodman/dive myapp:latest🏋️ Bài tập thực hành
Bài 1: So sánh base images
- Build cùng một app với
node:20,node:20-slim,node:20-alpine - So sánh size:
docker images | grep myapp - Ghi nhận sự khác biệt
Bài 2: Multi-stage build
- Viết Dockerfile single-stage cho Node.js app (TypeScript)
- Viết Dockerfile multi-stage cho cùng app
- So sánh size hai images
- Chạy cả hai, kiểm tra hoạt động giống nhau
Bài 3: .dockerignore
- Build image KHÔNG có
.dockerignore– ghi nhận build context size - Tạo
.dockerignorephù hợp - Build lại – so sánh build context size và thời gian build
Bài 4: Tối ưu thực tế
- Lấy một Dockerfile "xấu" (dùng
node:20, không multi-stage, không ignore) - Áp dụng tất cả kỹ thuật tối ưu đã học
- Mục tiêu: giảm image size > 80%
- Kiểm tra app vẫn chạy đúng