Dockerfile nâng cao: Multi-stage Build và tối ưu

Photo of author

Văn Ngọc Tân

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

0 0 đánh giá
Đánh giá bài viết
Theo dõi
Thông báo của
guest
0 Góp ý
Cũ nhất
Mới nhất Được bỏ phiếu nhiều nhất
Phản hồi nội tuyến
Xem tất cả bình luận