Bạn có biết?
Khi bạn build một ứng dụng Node.js, image hoàn chỉnh có thể nặng tới 1GB vì chứa cả source code, node_modules, dev dependencies và build tools. Nhưng thực tế, khi chạy production, bạn chỉ cần file đã build — vài MB mà thôi!
Multi-stage build giải quyết vấn đề này: tách quá trình build và runtime thành nhiều giai đoạn, chỉ giữ lại những gì cần thiết trong image cuối cùng.
Multi-stage Build là gì?
Multi-stage build cho phép bạn sử dụng nhiều FROM statements trong một Dockerfile. Mỗi FROM bắt đầu một stage mới, và bạn có thể copy kết quả từ stage trước sang stage sau.
# Stage 1: Build (nặng, chứa build tools)
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Stage 2: Production (nhẹ, chỉ chứa kết quả)
FROM node:18-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
CMD ["node", "dist/main.js"]
Kết quả: image cuối cùng không chứa source code, dev dependencies hay build tools — chỉ có file đã build!
So sánh image size
# Single-stage (không tối ưu)
$ docker build -t myapp:old .
$ docker images myapp:old
# REPOSITORY TAG SIZE
# myapp old 1.2GB
# Multi-stage (tối ưu)
$ docker build -t myapp:new .
$ docker images myapp:new
# REPOSITORY TAG SIZE
# myapp new 180MB
Các kỹ thuật tối ưu Dockerfile
1. Layer Caching — Tận dụng cache để build nhanh hơn
Docker cache từng layer riêng biệt. Nếu layer đầu tiên thay đổi, tất cả layer sau đó phải build lại. Thứ tự lệnh rất quan trọng!
# ❌ KHÔNG tối ưu — mỗi lần code đổi, cài lại TẤT CẢ dependencies
FROM node:18-alpine
WORKDIR /app
COPY . .
RUN npm install
CMD ["node", "index.js"]
# ✅ TỐI ƯU — dependencies được cache nếu package.json không đổi
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./ # Layer 1: Chỉ copy file config
RUN npm ci --production # Layer 2: Cài dependencies (cached!)
COPY . . # Layer 3: Copy source code (thay đổi thường xuyên)
CMD ["node", "index.js"]
Quy tắc: Đặt những thứ ít thay đổi (dependencies) lên trước, hay thay đổi (source code) xuống sau.
2. Build Args — Truyền biến khi build
ARG cho phép truyền biến từ bên ngoài vào Dockerfile khi build:
# Dockerfile
ARG NODE_VERSION=18
FROM node:${NODE_VERSION}-alpine
ARG BUILD_DATE
ARG VERSION=latest
LABEL build_date="${BUILD_DATE}"
LABEL version="${VERSION}"
WORKDIR /app
COPY . .
RUN npm ci --production
CMD ["node", "index.js"]
# Build với args
$ docker build \
--build-arg NODE_VERSION=20 \
--build-arg BUILD_DATE=$(date -u +%Y-%m-%dT%H:%M:%SZ) \
--build-arg VERSION=1.2.3 \
-t myapp:1.2.3 .
⚠️ Lưu ý: ARG chỉ có sẵn khi build, không có khi container chạy. Dùng ENV cho runtime variables.
3. Multi-stage với nhiều base images
Mỗi stage có thể dùng base image khác nhau — tối ưu cho từng mục đích:
# Stage 1: Build bằng Alpine (nhẹ)
FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY go.* ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o myapp .
# Stage 2: Chạy trên Distroless (bảo mật tối đa)
FROM gcr.io/distroless/static-debian12
COPY --from=builder /app/myapp /
CMD ["/myapp"]
Distroless — image siêu nhỏ (~2MB), không có shell, không có package manager → bề mặt tấn công tối thiểu.
4. COPY —from=stage_name
Copy file từ stage trước hoặc từ image bên ngoài:
# Copy từ stage khác
COPY --from=builder /app/dist ./dist
# Copy trực tiếp từ image có sẵn (không cần build stage!)
COPY --from=nginx:latest /etc/nginx/nginx.conf /etc/nginx/nginx.conf
# Copy binary từ Alpine
COPY --from=alpine:latest /bin/echo /bin/echo
Health Check trong Dockerfile
HEALTHCHECK instruction
Cho Docker biết cách kiểm tra container có đang hoạt động bình thường không:
# Kiểm tra HTTP endpoint
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD curl -f http://localhost:8080/health || exit 1
# Kiểm tra process
HEALTHCHECK --interval=10s \
CMD pgrep -f "node index.js" || exit 1
# Tắt healthcheck (kế thừa từ base image)
HEALTHCHECK NONE
Các tham số HEALTHCHECK
- –interval: Thời gian giữa các lần kiểm tra (mặc định 30s)
- –timeout: Thời gian tối đa cho mỗi lần kiểm tra (mặc định 30s)
- –start-period: Thời gian gia hạn khi container khởi động (mặc định 0s)
- –retries: Số lần thử lại trước khi đánh dấu unhealthy (mặc định 3)
Kiểm tra health status
# Xem health status
$ docker ps
# CONTAINER ID STATUS
# a1b2c3d4 Up 5 minutes (healthy)
# Xem chi tiết health log
$ docker inspect --format='{{json .State.Health}}' my-container | jq
USER — Chạy container với quyền hạn tối thiểu
Mặc định, container chạy với quyền root. Trong production, nên tạo user riêng:
# Tạo user không có quyền root
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
# Chuyển sang user trước khi chạy
USER appuser
CMD ["node", "index.js"]
Best Practices tổng hợp
1. Sử dụng .dockerignore
# .dockerignore
node_modules
.git
.env
*.md
docker-compose*.yml
.dockerignore
Dockerfile
2. Gộp lệnh RUN để giảm layers
# ❌ Nhiều layers
RUN apt-get update
RUN apt-get install -y curl
RUN apt-get clean
# ✅ Một layer
RUN apt-get update && \
apt-get install -y --no-install-recommends curl && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
3. Pin version cụ thể
# ❌ Không ổn định
FROM node:latest
FROM python:3
# ✅ Ổn định, tái tạo được
FROM node:20.11-alpine3.19
FROM python:3.12.2-slim-bookworm
4. Sử dụng ENTRYPOINT + CMD linh hoạt
# ENTRYPOINT = lệnh chính (không ghi đè)
ENTRYPOINT ["python"]
# CMD = tham số mặc định (có thể ghi đè)
CMD ["app.py"]
# Khi chạy: python app.py (mặc định)
$ docker run myapp
# Khi chạy: python test.py (ghi đè CMD)
$ docker run myapp test.py
Ví dụ hoàn chỉnh: Node.js API
# ============================================
# Stage 1: Dependencies
# ============================================
FROM node:20-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --production
# ============================================
# Stage 2: Build
# ============================================
FROM node:20-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build
# ============================================
# Stage 3: Production
# ============================================
FROM node:20-alpine AS runner
WORKDIR /app
# Tạo user không root
RUN addgroup -S app && adduser -S app -G app
# Copy chỉ những gì cần thiết
COPY --from=deps /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/package.json ./
# Chuyển sang user không root
USER app
# Expose port
EXPOSE 3000
# Health check
HEALTHCHECK --interval=30s --timeout=3s \
CMD wget -qO- http://localhost:3000/health || exit 1
# Chạy app
CMD ["node", "dist/main.js"]
So sánh Single-stage vs Multi-stage
| Tiêu chí | Single-stage | Multi-stage |
|---|---|---|
| Image size | Lớn (chứa build tools) | Nhỏ (chỉ runtime) |
| Security | Nhiều bề mặt tấn công | Tối thiểu attack surface |
| Build time | Nhanh hơn lần đầu | Tối ưu với cache |
| Complexity | Đơn giản | Phức tạp hơn |
| Production ready | Cần tối ưu thêm | Sẵn sàng |
Bước tiếp theo
Bạn đã nắm vững các kỹ thuật tối ưu Dockerfile! Tiếp theo, hãy học Docker Compose — cách quản lý nhiều container cùng lúc với một file YAML duy nhất.
👉 Đọc tiếp: Docker Compose: Quản lý Multi-container Application