Giao diện
Bài 6: Multi-stage Builds, Hadolint & .dockerignore
🎓 Instructor Profile
Kỹ sư Raizo (Phó CTO HPN) — người từng giảm image production của Fintech từ 1.4GB xuống 45MB. Đồng hành cùng Giáo sư Tom — chuyên gia phân tích nguyên lý hệ thống.
Bạn đã biết viết Dockerfile, hiểu layer caching và UnionFS. Nhưng hãy trả lời câu hỏi này: Tại sao image production của bạn vẫn nặng hàng trăm MB?
Câu trả lời nằm ở ba vũ khí mà mọi Senior DevOps đều thành thạo: Multi-stage Builds để cắt bỏ "mỡ thừa", Hadolint để bắt lỗi trước khi build, và .dockerignore để kiểm soát những gì Docker được phép nhìn thấy.
Trong bài này, chúng ta sẽ đi theo lộ trình: Khái niệm → Cú pháp → Thực hành → Cạm bẫy → Bên trong cơ chế.
🏗️ Phần 1: Multi-stage Builds (Kiến trúc đa tầng)
1.1 Khái niệm: Build Stage vs Runtime Stage
Tư duy kiến trúc sư: Khi bạn xây một ngôi nhà, bạn cần cần cẩu, giàn giáo, máy trộn bê tông. Nhưng sau khi xây xong, bạn có mang cần cẩu vào nhà ở không? Tất nhiên là không. Bạn chỉ giữ lại ngôi nhà hoàn thiện.
Multi-stage Build trong Docker cũng vậy:
- Stage 1 (Build Stage): Dùng image "béo" chứa đầy đủ compiler, build tools, SDK để biên dịch source code thành artifact (binary, bundle, dist).
- Stage 2 (Runtime Stage): Dùng image "gầy" tối thiểu, chỉ copy artifact từ Stage 1 sang. Toàn bộ source code, compiler, dev dependencies bị bỏ lại.
Sơ đồ kiến trúc Multi-stage
┌──────────────────────────────────────────────────────────────────┐
│ MULTI-STAGE BUILD PIPELINE │
│ │
│ ┌─────────────────────────┐ ┌─────────────────────────────┐ │
│ │ STAGE 1: builder │ │ STAGE 2: runtime │ │
│ │ ───────────────── │ │ ───────────────── │ │
│ │ │ │ │ │
│ │ ┌───────────────────┐ │ │ ┌───────────────────────┐ │ │
│ │ │ Base Image (fat) │ │ │ │ Base Image (slim) │ │ │
│ │ │ golang:1.22-alpine│ │ │ │ scratch / alpine │ │ │
│ │ │ node:20-alpine │ │ │ │ distroless │ │ │
│ │ └───────────────────┘ │ │ └───────────────────────┘ │ │
│ │ │ │ │ │ │ │
│ │ ▼ │ │ ▼ │ │
│ │ ┌───────────────────┐ │ │ ┌───────────────────────┐ │ │
│ │ │ + Source Code │ │ │ │ + Artifact ONLY │ │ │
│ │ │ + Dependencies │ │ │ │ (binary / dist) │ │ │
│ │ │ + Build Tools │──┼────┼──│ │ │ │
│ │ │ + Compiler / SDK │ │COPY│ │ ❌ Không có source │ │ │
│ │ └───────────────────┘ │from│ │ ❌ Không có compiler │ │ │
│ │ │ │ │ │ ❌ Không có dev deps │ │ │
│ │ ▼ │ │ └───────────────────────┘ │ │
│ │ ┌───────────────────┐ │ │ │ │ │
│ │ │ OUTPUT: artifact │ │ │ ▼ │ │
│ │ │ (binary/bundle) │ │ │ ┌───────────────────────┐ │ │
│ │ └───────────────────┘ │ │ │ FINAL IMAGE │ │ │
│ │ │ │ │ Size: 10-50MB 🎯 │ │ │
│ │ Size: 500MB-1.5GB ⚠️ │ │ └───────────────────────┘ │ │
│ └─────────────────────────┘ └─────────────────────────────┘ │
│ │
│ BỎ LẠI ← (Stage 1 không có GIỮ LẠI → (Chỉ có │
│ trong final image) thứ cần để chạy app) │
└──────────────────────────────────────────────────────────────────┘So sánh kích thước: Trước và sau Multi-stage
| Ngôn ngữ | Single-stage (image đầy đủ) | Multi-stage (tối ưu) | Giảm |
|---|---|---|---|
| Go | ~1.2GB (golang base + source) | ~12MB (scratch + binary) | 99% |
| Node.js | ~1.1GB (node full + devDeps) | ~180MB (alpine + prod deps) | 84% |
| Python | ~900MB (python full + pip cache) | ~120MB (slim + venv) | 87% |
| Java | ~800MB (JDK + Maven + source) | ~200MB (JRE + jar) | 75% |
💡 Case Study thực tế
Một công ty Fintech tại Việt Nam đã áp dụng multi-stage builds cho toàn bộ hệ thống microservices. Kết quả:
- Image container giảm từ 1.4GB → 45MB
- Thời gian quét bảo mật (vulnerability scan) giảm từ 15 phút → 30 giây
- Deploy time giảm từ 8 phút → 45 giây (pull image nhanh hơn)
- Chi phí bandwidth trên container registry giảm 70% mỗi tháng
1.2 Cú pháp: Multi-stage cho 3 ngôn ngữ phổ biến
a) Go — Giảm kịch tính nhất (1.2GB → 12MB)
Go biên dịch thành static binary — không cần runtime, không cần thư viện hệ thống. Đây là ngôn ngữ lý tưởng nhất cho multi-stage.
dockerfile
# ================================================================
# STAGE 1: BUILD — Biên dịch source code thành binary
# ================================================================
FROM golang:1.22-alpine AS builder
WORKDIR /app
# Copy file dependency trước (tận dụng layer cache)
COPY go.mod go.sum ./
RUN go mod download
# Copy toàn bộ source code
COPY . .
# Build static binary — CGO_ENABLED=0 để không phụ thuộc thư viện C
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o server .
# ================================================================
# STAGE 2: RUNTIME — Image siêu nhẹ chỉ chứa binary
# ================================================================
FROM scratch
# Copy binary từ stage builder
COPY --from=builder /app/server /server
# scratch không có shell, user, hay bất kỳ thứ gì khác
ENTRYPOINT ["/server"]Giải thích chi tiết:
FROM scratch— Image rỗng hoàn toàn, 0 byte. Không có OS, không có shell, không có gì cả ngoài binary của bạn.CGO_ENABLED=0— Tắt CGo để binary không link với thư viện C → chạy được trênscratch.-ldflags="-s -w"— Strip debug symbols, giảm thêm ~30% kích thước binary.- Kết quả: Image cuối cùng chỉ nặng bằng đúng kích thước file binary (~10-15MB).
⚠️ Khi nào KHÔNG dùng scratch?
scratch không có:
- CA certificates — Nếu app gọi HTTPS, cần copy cert:
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ - Timezone data — Nếu app xử lý múi giờ, cần copy từ builder stage
- Shell — Không thể
docker exec -it ... shđể debug
Nếu cần debug: dùng alpine:3.19 thay vì scratch (thêm ~7MB).
b) Node.js — Tách dev dependencies khỏi production
Node.js không compile thành binary, nhưng multi-stage giúp loại bỏ devDependencies, TypeScript compiler, test files và source code gốc.
dockerfile
# ================================================================
# STAGE 1: BUILD — Cài đặt deps + build TypeScript/Bundle
# ================================================================
FROM node:20-alpine AS builder
WORKDIR /app
# Copy file dependency trước (layer cache optimization)
COPY package.json package-lock.json ./
# npm ci: install chính xác theo lockfile (deterministic)
RUN npm ci
# Copy source code và build
COPY . .
RUN npm run build
# ================================================================
# STAGE 2: RUNTIME — Chỉ production dependencies + build output
# ================================================================
FROM node:20-alpine
WORKDIR /app
# Chỉ install production dependencies
COPY package.json package-lock.json ./
RUN npm ci --only=production && npm cache clean --force
# Copy build output từ stage 1
COPY --from=builder /app/dist ./dist
# Non-root user (security best practice)
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser
EXPOSE 3000
CMD ["node", "dist/server.js"]Tại sao không COPY --from=builder /app/node_modules?
- Cách đó copy cả
devDependencies(TypeScript, Jest, ESLint...) — hàng trăm MB không cần thiết. - Tốt hơn: install lại chỉ
productiondeps ở Stage 2. Tốn thêm vài giây build nhưng image nhẹ hơn rất nhiều.
📊 So sánh chi tiết Node.js image size
| Chiến lược | Kích thước | Ghi chú |
|---|---|---|
node:20 + all deps | ~1.1GB | ❌ Quá nặng |
node:20-alpine + all deps | ~400MB | ⚠️ Vẫn có devDeps |
| Multi-stage + prod deps only | ~180MB | ✅ Chuẩn production |
| Multi-stage + distroless | ~130MB | 🔒 Maximum security |
c) Python — Virtual Environment qua hai stage
Python không compile thành binary, nhưng dùng virtual environment để đóng gói dependencies sạch sẽ, rồi copy toàn bộ venv sang stage runtime.
dockerfile
# ================================================================
# STAGE 1: BUILD — Tạo venv + cài dependencies
# ================================================================
FROM python:3.12-slim AS builder
WORKDIR /app
# Tạo virtual environment tại /opt/venv
RUN python -m venv /opt/venv
# Kích hoạt venv bằng cách thêm vào PATH
ENV PATH="/opt/venv/bin:$PATH"
# Cài dependencies vào venv (không cache pip)
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# ================================================================
# STAGE 2: RUNTIME — Copy venv + source code
# ================================================================
FROM python:3.12-slim
# Copy toàn bộ virtual environment từ builder
COPY --from=builder /opt/venv /opt/venv
# Set PATH để Python sử dụng venv
ENV PATH="/opt/venv/bin:$PATH"
WORKDIR /app
COPY . .
# Non-root user
RUN useradd --create-home --shell /bin/bash appuser
USER appuser
EXPOSE 8000
CMD ["python", "main.py"]Tại sao dùng venv trong Docker?
- Cách ly sạch sẽ: Dependencies nằm gọn trong
/opt/venv, dễ copy giữa các stage. - Không ô nhiễm system Python: Stage 2 có Python sạch + chỉ thư viện bạn cần.
- Reproducible:
pip installtrong venv tạo ra kết quả nhất quán.
1.3 Named Stages & Build Targets
Đặt tên stage với AS
Mỗi FROM tạo một stage mới. Đặt tên bằng AS để tham chiếu rõ ràng:
dockerfile
FROM golang:1.22-alpine AS builder # Stage 0, tên "builder"
FROM alpine:3.19 AS tester # Stage 1, tên "tester"
FROM scratch AS production # Stage 2, tên "production"COPY --from=<stage> — Copy artifact giữa các stage
dockerfile
# Copy từ stage có tên
COPY --from=builder /app/server /server
# Copy từ stage theo index (ít dùng, khó đọc)
COPY --from=0 /app/server /server
# Copy từ image bên ngoài (không cần stage trong cùng Dockerfile)
COPY --from=nginx:alpine /etc/nginx/nginx.conf /etc/nginx/nginx.conf--target=<stage> — Build đến một stage cụ thể
Rất hữu ích trong CI/CD khi bạn muốn chạy test ở stage trung gian mà không build production image:
bash
# Chỉ build đến stage "tester" để chạy test trong CI
docker build --target tester -t myapp:test .
# Build full production image
docker build --target production -t myapp:latest .
# Build stage builder để debug build process
docker build --target builder -t myapp:debug .Ứng dụng trong CI pipeline:
yaml
# .github/workflows/docker.yml
jobs:
test:
steps:
- name: Run tests in Docker
run: docker build --target tester -t app:test .
build:
needs: test
steps:
- name: Build production image
run: docker build --target production -t app:latest .1.4 Multi-stage nâng cao: 3+ stages
Với dự án lớn, bạn có thể tách thành nhiều stage chuyên biệt:
dockerfile
# --- Stage 1: Dependencies ---
FROM node:20-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
# --- Stage 2: Build ---
FROM node:20-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build
RUN npm run test
# --- Stage 3: Production Dependencies ---
FROM node:20-alpine AS prod-deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --only=production && npm cache clean --force
# --- Stage 4: Runtime ---
FROM node:20-alpine AS runtime
WORKDIR /app
RUN addgroup -S app && adduser -S app -G app
COPY --from=prod-deps /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
COPY package.json ./
USER app
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1
CMD ["node", "dist/server.js"]Lợi ích của 4 stages:
deps— Cache toàn bộ dependencies (dev + prod), chỉ rebuild khipackage.jsonđổibuilder— Build + test, sử dụng deps từ stage 1prod-deps— Chỉ production dependencies, tách riêng để final image nhẹruntime— Image cuối cùng: prod deps + build output + security hardening
⚠️ Cạm bẫy
COPY --from=builder chỉ copy files và directories. Nó KHÔNG copy:
- ❌
ENVvariables — Phải khai báo lại ở stage mới - ❌
WORKDIR— Phải set lại - ❌
USER— Phải tạo lại user và switch - ❌
EXPOSE— Phải khai báo lại - ❌ Installed packages (apt/apk) — Phải install lại nếu cần
Ví dụ lỗi phổ biến:
dockerfile
# ❌ SAI: ENV từ builder KHÔNG tồn tại ở stage 2
FROM python:3.12-slim AS builder
ENV APP_VERSION=2.0
WORKDIR /app
# ... build ...
FROM python:3.12-slim
COPY --from=builder /app /app
# APP_VERSION ở đây là undefined!
# WORKDIR vẫn là / (chưa set lại)dockerfile
# ✅ ĐÚNG: Khai báo lại mọi thứ cần thiết
FROM python:3.12-slim AS builder
ENV APP_VERSION=2.0
WORKDIR /app
# ... build ...
FROM python:3.12-slim
ENV APP_VERSION=2.0
WORKDIR /app
COPY --from=builder /app .🔍 Phần 2: Hadolint — Linter cho Dockerfile
2.1 Khái niệm: Hadolint là gì?
Phép so sánh: Nếu ESLint giúp bạn viết JavaScript sạch, Pylint giúp bạn viết Python chuẩn, thì Hadolint là công cụ tương tự cho Dockerfile.
Hadolint phân tích Dockerfile của bạn và bắt:
- ❌ Không pin version base image (
FROM node:latest) - ❌ Không pin version apt packages (
apt-get install curl) - ❌ Quên set
SHELLcho pipefail - ❌ Dùng
ADDthay vìCOPY - ❌ Chạy container với quyền root
- ❌ Không clean apt cache
Hadolint kết hợp hai engine: Dockerfile parser + ShellCheck (lint cho bash scripts trong RUN).
2.2 Cú pháp: Cài đặt và sử dụng
Cách 1: Chạy qua Docker (không cần cài đặt)
bash
# Lint một Dockerfile
docker run --rm -i hadolint/hadolint < Dockerfile
# Lint với output format JSON (cho CI)
docker run --rm -i hadolint/hadolint hadolint --format json - < Dockerfile
# Lint và bỏ qua một số rules cụ thể
docker run --rm -i hadolint/hadolint hadolint --ignore DL3008 --ignore DL3013 - < DockerfileCách 2: Cài trực tiếp (nhanh hơn)
bash
# macOS
brew install hadolint
# Linux (tải binary)
wget -O /usr/local/bin/hadolint \
https://github.com/hadolint/hadolint/releases/latest/download/hadolint-Linux-x86_64
chmod +x /usr/local/bin/hadolint
# Windows (scoop)
scoop install hadolint
# Chạy trực tiếp
hadolint Dockerfile2.3 Các rules quan trọng nhất
| Rule ID | Mức độ | Mô tả | Ví dụ vi phạm |
|---|---|---|---|
| DL3007 | ⚠️ Warning | Dùng latest tag cho base image | FROM node:latest |
| DL3008 | ⚠️ Warning | Không pin version apt packages | apt-get install curl |
| DL3009 | 🔴 Error | Không xóa apt cache sau install | Thiếu rm -rf /var/lib/apt/lists/* |
| DL3013 | ⚠️ Warning | Không pin version pip packages | pip install flask |
| DL3018 | ⚠️ Warning | Không pin version apk packages | apk add curl |
| DL3025 | 🔴 Error | Dùng JSON form cho CMD/ENTRYPOINT | CMD npm start thay vì CMD ["npm", "start"] |
| DL4006 | ⚠️ Warning | Không set SHELL cho pipefail | RUN curl ... | tar ... |
| SC2086 | 🔴 Error | Biến không được quote (ShellCheck) | RUN echo $VAR thay vì RUN echo "$VAR" |
DL3007 — Pin version base image
dockerfile
# ❌ Vi phạm DL3007: "latest" không reproducible
FROM node:latest
FROM python:latest
# ✅ Tuân thủ: Pin cụ thể version + variant
FROM node:20.11-alpine3.19
FROM python:3.12.1-slim-bookwormTại sao? latest hôm nay là Node 20, nhưng tuần sau có thể là Node 21. Build sẽ không reproducible — cùng Dockerfile nhưng tạo ra image khác nhau theo thời gian.
DL3008 — Pin version apt packages
dockerfile
# ❌ Vi phạm DL3008: Không pin version
RUN apt-get update && apt-get install -y \
curl \
wget
# ✅ Tuân thủ: Pin version cụ thể
RUN apt-get update && apt-get install -y --no-install-recommends \
curl=7.88.1-10+deb12u5 \
wget=1.21.3-1+b2 \
&& rm -rf /var/lib/apt/lists/*DL4006 — Set SHELL cho pipefail
dockerfile
# ❌ Vi phạm DL4006: Pipe có thể fail silent
RUN curl -fsSL https://example.com/install.sh | bash
# ✅ Tuân thủ: Set pipefail để pipe fail đúng cách
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
RUN curl -fsSL https://example.com/install.sh | bashTại sao? Mặc định shell là /bin/sh, nếu curl fail nhưng bash nhận empty input vẫn exit 0 → build thành công nhưng app bị lỗi. pipefail đảm bảo pipe fail nếu bất kỳ command nào trong pipe fail.
2.4 Cấu hình .hadolint.yaml
Tạo file .hadolint.yaml ở root dự án để tùy chỉnh rules:
yaml
# .hadolint.yaml — Cấu hình Hadolint cho dự án
---
# Bỏ qua một số rules (có lý do chính đáng)
ignored:
- DL3008 # Không pin apt version (quá khó maintain cho internal tools)
- DL3013 # Không pin pip version (dùng requirements.txt pin rồi)
# Override mức độ nghiêm trọng
override:
warning:
- DL3025 # CMD dạng string → chỉ warning thay vì error
error:
- DL3007 # FROM latest → nâng lên error (bắt buộc phải sửa)
# Trusted registries (không cảnh báo khi dùng image từ đây)
trustedRegistries:
- docker.io
- gcr.io
- ghcr.io2.5 Lab thực hành: Lint và sửa Dockerfile
Bước 1: Dockerfile "tệ" để test
Tạo file Dockerfile.bad với đầy lỗi:
dockerfile
FROM node:latest
ADD . /app
WORKDIR /app
RUN apt-get update
RUN apt-get install -y curl python3
RUN cd /app && npm install
RUN npm run build
EXPOSE 3000
CMD npm startBước 2: Chạy Hadolint
bash
$ hadolint Dockerfile.bad
Dockerfile.bad:1 DL3007 warning: Using latest is prone to errors...
Dockerfile.bad:2 DL3020 error: Use COPY instead of ADD for files and folders
Dockerfile.bad:4 DL3009 info: Delete the apt-get lists after installing...
Dockerfile.bad:5 DL3008 warning: Pin versions in apt get install...
Dockerfile.bad:5 DL3015 info: Avoid additional packages by specifying --no-install-recommends
Dockerfile.bad:6 DL3003 warning: Use WORKDIR to switch to a directory
Dockerfile.bad:9 DL3025 warning: Use arguments JSON notation for CMDBước 3: Sửa từng lỗi
dockerfile
# ✅ Dockerfile đã sửa — Pass hadolint 100%
FROM node:20.11-alpine3.19
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --only=production
COPY . .
RUN npm run build
RUN addgroup -S app && adduser -S app -G app
USER app
EXPOSE 3000
CMD ["node", "dist/server.js"]2.6 Tích hợp Hadolint vào CI/CD
GitHub Actions
yaml
# .github/workflows/dockerfile-lint.yml
name: Lint Dockerfile
on:
pull_request:
paths:
- '**/Dockerfile*'
- '**/.hadolint.yaml'
jobs:
hadolint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run Hadolint
uses: hadolint/hadolint-action@v3.1.0
with:
dockerfile: Dockerfile
failure-threshold: warning
# Mọi warning trở lên sẽ fail CIGitLab CI
yaml
# .gitlab-ci.yml
hadolint:
stage: lint
image: hadolint/hadolint:latest-debian
script:
- hadolint Dockerfile
rules:
- changes:
- Dockerfile
- .hadolint.yaml💡 Best Practice CI
Luôn chạy Hadolint trước khi build image trong CI pipeline. Không có lý do gì để tốn 5-10 phút build rồi mới phát hiện Dockerfile có lỗi cơ bản.
📦 Phần 3: .dockerignore — Kiểm soát Build Context
3.1 Khái niệm: Build Context là gì?
Khi bạn chạy docker build ., Docker đóng gói toàn bộ thư mục hiện tại (gọi là "build context") và gửi đến Docker daemon. Daemon mới là nơi thực sự build image.
Vấn đề: Nếu thư mục của bạn chứa:
node_modules/— 500MB.git/— 200MB lịch sử commitcoverage/— 50MB test reports.env— 🔴 Chứa database password, API keys!
Thì Docker sẽ gửi tất cả 750MB+ đến daemon, dù Dockerfile của bạn chỉ COPY vài file.
bash
# Bạn sẽ thấy dòng này khi build
$ docker build .
Sending build context to Docker daemon 750.3MB # 😱 QUÁ NẶNG!Sơ đồ: Có vs Không có .dockerignore
KHÔNG CÓ .dockerignore: CÓ .dockerignore:
┌──────────────────────┐ ┌──────────────────────┐
│ Project Directory │ │ Project Directory │
│ ┌──────────────────┐ │ │ ┌──────────────────┐ │
│ │ src/ 3MB │ │──── ALL ────▶ │ │ src/ 3MB │ │─── FILTER ──▶
│ │ node_modules 500MB│ │ 750MB+ │ │ ██████████████████│ │ ~5MB
│ │ .git/ 200MB│ │ sent to │ │ ██████████████████│ │ sent to
│ │ coverage/ 50MB│ │ daemon │ │ ██████████████████│ │ daemon
│ │ .env 1KB│ │ 🐌 CHẬM │ │ ██████████████████│ │ ⚡ NHANH
│ │ Dockerfile 1KB│ │ 🔓 KHÔNG │ │ Dockerfile 1KB│ │ 🔒 AN TOÀN
│ │ package.json 2KB│ │ AN TOÀN │ │ package.json 2KB│ │
│ └──────────────────┘ │ │ └──────────────────┘ │
└──────────────────────┘ └──────────────────────┘
██ = Bị .dockerignore chặn3.2 Cú pháp: Viết file .dockerignore
.dockerignore hoạt động giống .gitignore — sử dụng glob patterns để loại trừ files/directories khỏi build context.
Template chuẩn cho mọi dự án
text
# ===========================================
# .dockerignore — Kiểm soát build context
# ===========================================
# --- Version Control ---
.git
.gitignore
.gitattributes
# --- Dependencies (sẽ install lại trong Dockerfile) ---
node_modules
vendor/
__pycache__
*.pyc
.venv
# --- Build Output (sẽ build lại trong Dockerfile) ---
dist
build
coverage
.nyc_output
# --- IDE & Editor ---
.vscode
.idea
*.swp
*.swo
*~
# --- Environment & Secrets ---
.env
.env.*
.env.local
.env.production
*.key
*.pem
*.p12
secrets/
# --- Docker files (không cần trong image) ---
Dockerfile*
docker-compose*.yml
.dockerignore
# --- Documentation & Misc ---
*.md
LICENSE
CHANGELOG
docs/
# --- OS files ---
.DS_Store
Thumbs.db
# --- Logs ---
*.log
logs/
npm-debug.log*Cú pháp nâng cao
text
# Wildcard: * match mọi thứ trong 1 level
*.log # Tất cả file .log
temp_* # Tất cả file bắt đầu bằng temp_
# Double wildcard: ** match mọi thứ ở mọi level
**/*.test.js # Mọi file .test.js ở bất kỳ thư mục con nào
**/node_modules # node_modules ở mọi nơi
# Negation: ! để "giữ lại" file đã bị exclude
*.md # Loại tất cả .md
!README.md # Nhưng giữ lại README.md
# Comment
# Đây là comment, Docker sẽ bỏ qua dòng này3.3 Đo lường tác động: Build Context Size
Trước khi có .dockerignore
bash
# Kiểm tra kích thước thư mục
$ du -sh .
752M .
$ docker build -t myapp .
Sending build context to Docker daemon 752.4MB # 😱
# Build time: 2 phút 30 giây (phần lớn là gửi context)Sau khi thêm .dockerignore
bash
$ docker build -t myapp .
Sending build context to Docker daemon 2.1MB # ⚡
# Build time: 15 giây (gần như toàn bộ là build thực sự)Kết quả:
| Metric | Không có .dockerignore | Có .dockerignore | Cải thiện |
|---|---|---|---|
| Build context size | 752MB | 2.1MB | 99.7% nhỏ hơn |
| Context transfer time | ~45 giây | <1 giây | 45x nhanh hơn |
| Rủi ro leak secrets | 🔴 Cao | 🟢 Thấp | Loại bỏ .env |
| Tổng build time | 2 phút 30s | 15 giây | 10x nhanh hơn |
🚫 Anti-pattern nghiêm trọng: .env trong build context
Tình huống: Bạn quên thêm .env vào .dockerignore. File .env chứa:
DATABASE_URL=postgres://admin:SuperSecret@db.company.com:5432/prod
AWS_SECRET_KEY=AKIA1234567890ABCDEF
STRIPE_SECRET=sk_live_abcdefghijklmnopHậu quả:
COPY . .trong Dockerfile sẽ copy.envvào image.envbị ghi vĩnh viễn vào một image layer- Dù bạn
RUN rm .envở layer sau, file vẫn tồn tại trong layer cũ (UnionFS!) - Bất kỳ ai pull image đều có thể extract secrets:
bash
# Hacker extract secrets từ image layers
docker save myapp:latest | tar -xf -
# Duyệt qua từng layer để tìm .env
find . -name "*.env" -o -name ".env"Giải pháp:
- Luôn có
.envtrong.dockerignore— không có ngoại lệ - Inject secrets lúc runtime qua
docker run --env-file .envhoặc Docker Secrets - Dùng multi-stage build và chỉ copy artifacts cần thiết (không
COPY . .ở final stage) - Audit image layers:
docker history --no-trunc myapp:latest
⚡ Phần 4: Bài tập nhanh — Scenario Challenge
🎯 Scenario Choice — 🎯 Tình huống: Image 800MB cần tối ưu
Bối cảnh: Team bạn có một ứng dụng Node.js/TypeScript. Dockerfile hiện tại tạo ra image 800MB. Deploy mỗi lần mất 8 phút vì pull image chậm. CI/CD vulnerability scan mất 12 phút.
Dockerfile hiện tại:
dockerfile
FROM node:20
WORKDIR /app
COPY . .
RUN npm install
RUN npm run build
EXPOSE 3000
CMD ["npm", "start"]Câu hỏi: Bạn sẽ áp dụng những kỹ thuật nào để giảm kích thước?
💡 Bấm để xem lời giải chi tiết
Bước 1: Thêm .dockerignore (giảm build context 99%)
text
node_modules
.git
*.md
.env
coverage
distBước 2: Multi-stage build (loại bỏ devDeps + source)
dockerfile
FROM node:20-alpine AS builder
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:20-alpine
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --only=production && npm cache clean --force
COPY --from=builder /app/dist ./dist
RUN addgroup -S app && adduser -S app -G app
USER app
EXPOSE 3000
CMD ["node", "dist/server.js"]Bước 3: Chạy Hadolint để kiểm tra thêm lỗi
Kết quả: 800MB → ~150MB (giảm ~81%)
| Kỹ thuật | Tác động |
|---|---|
node:20-alpine thay node:20 | -750MB → ~150MB |
| Multi-stage (loại devDeps) | -50MB thêm |
.dockerignore | Build nhanh hơn, không leak secrets |
| Hadolint | Bắt lỗi version pinning |
🐛 Spot the Bug
🐛 Spot-the-Bug — 🔎 Tìm lỗi trong Dockerfile sau
dockerfile
FROM python:3.12-slim AS builder
WORKDIR /build
ENV DATABASE_URL=postgres://localhost/mydb
RUN python -m venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"
COPY requirements.txt .
RUN pip install -r requirements.txt
FROM python:3.12-slim
COPY --from=builder /opt/venv /opt/venv
COPY . .
CMD ["python", "main.py"]Có ít nhất 4 lỗi. Bạn tìm được bao nhiêu?
💡 Đáp án
Lỗi 1: ENV DATABASE_URL=... — Secrets bị bake vào image layer. Dù ở builder stage, docker history vẫn hiển thị. → ✅ Sửa: Xóa dòng này, inject lúc runtime.
Lỗi 2: PATH không được set lại ở stage 2 — Python sẽ dùng system packages thay vì venv. → ✅ Sửa: Thêm ENV PATH="/opt/venv/bin:$PATH" ở stage 2.
Lỗi 3: Không set WORKDIR ở stage 2 — code sẽ nằm ở / (root directory). → ✅ Sửa: Thêm WORKDIR /app trước COPY . ..
Lỗi 4: pip install không có --no-cache-dir — pip cache nằm trong image, tăng kích thước vô ích. → ✅ Sửa: RUN pip install --no-cache-dir -r requirements.txt.
Lỗi 5 (bonus): Không có non-root user ở stage 2 — container chạy với quyền root. → ✅ Sửa: Thêm RUN useradd -r appuser và USER appuser.
🧠 Under the Hood: Multi-stage hoạt động thế nào?
Build Cache được chia sẻ giữa các stages
Điều quan trọng cần hiểu: Tất cả stages trong một multi-stage build chia sẻ cùng một build cache.
docker build .
│
├── Stage "builder" (FROM golang:1.22-alpine)
│ ├── Layer 1: Base image ──────────── [Cache HIT ✅]
│ ├── Layer 2: COPY go.mod go.sum ──── [Cache HIT ✅]
│ ├── Layer 3: RUN go mod download ─── [Cache HIT ✅]
│ ├── Layer 4: COPY . . ───────────── [Cache MISS ❌] (code đổi)
│ └── Layer 5: RUN go build ────────── [Rebuild 🔄]
│
├── Stage "runtime" (FROM scratch)
│ └── Layer 1: COPY --from=builder ─── [Rebuild 🔄] (phụ thuộc stage trước)
│
└── FINAL IMAGE = Chỉ layers của stage cuối cùng
├── scratch (0 bytes)
└── /server binary (~10MB)
= TOTAL: ~10MBIntermediate stages KHÔNG có mặt trong final image
Đây là điều then chốt: Docker chỉ giữ lại các layers của stage cuối cùng.
bash
# Kiểm chứng: xem layers của final image
$ docker history myapp:latest --no-trunc
IMAGE CREATED SIZE COMMENT
abc123 2 minutes ago 10.2MB COPY /server from builder
<missing> 2 minutes ago 0B scratch (empty base)
# Chỉ 2 layers! Toàn bộ stage builder (golang SDK, source code,
# dependencies) đã bị loại bỏ hoàn toàn.BuildKit và Parallel Stage Building
Từ Docker 18.09+, BuildKit (builder mặc định) có khả năng build các stages song song nếu chúng không phụ thuộc nhau:
dockerfile
# Stage A và Stage B build SONG SONG
FROM golang:1.22 AS backend
# ... build backend ...
FROM node:20-alpine AS frontend
# ... build frontend ...
# Stage C phụ thuộc A và B → đợi cả hai hoàn thành
FROM nginx:alpine
COPY --from=backend /app/api /usr/share/nginx/api
COPY --from=frontend /app/dist /usr/share/nginx/htmlTimeline (BuildKit):
Stage A (backend): ████████████░░░░░░░░
Stage B (frontend): ████████░░░░░░░░░░░░ ← Song song với A!
Stage C (final): ░░░░░░░░░░░░████████ ← Đợi A+B xong
──────────────────────▶ Thời gianNếu không có BuildKit (legacy builder), các stages build tuần tự → chậm hơn.
bash
# Bật BuildKit (nếu chưa mặc định)
export DOCKER_BUILDKIT=1
docker build .💡 Distroless: Nhỏ hơn cả Alpine
Nếu Alpine (~7MB) vẫn chưa đủ nhỏ, hãy xem xét Google Distroless Images:
| Image | Kích thước | Có Shell? | Use Case |
|---|---|---|---|
scratch | 0 bytes | ❌ | Go static binaries |
alpine:3.19 | ~7MB | ✅ sh | Cần debug, script |
gcr.io/distroless/static | ~2MB | ❌ | Go, Rust static binaries |
gcr.io/distroless/base | ~20MB | ❌ | C/C++ apps cần glibc |
gcr.io/distroless/nodejs20 | ~130MB | ❌ | Node.js production |
gcr.io/distroless/python3 | ~50MB | ❌ | Python production |
Lợi ích Distroless:
- Không có package manager → hacker không cài thêm tools
- Không có shell → không
execvào được → khó tấn công - CVE scan nhanh cực kỳ (ít packages → ít vulnerabilities)
Nhược điểm: Khó debug — không thể docker exec -it container sh. Giải pháp: dùng debug variant trong staging, distroless trong production.
📝 Quiz: Kiểm tra kiến thức
🧠 Quiz
Câu 1: Multi-stage build giúp giảm kích thước image bằng cách nào?
- [ ] A. Nén tất cả layers thành một layer duy nhất
- [x] B. Chỉ giữ lại layers của stage cuối cùng, loại bỏ build tools và source
- [ ] C. Tự động xóa file không sử dụng trong image
- [ ] D. Dùng thuật toán nén đặc biệt cho Docker layers
💡 Giải thích: Multi-stage build tạo nhiều stages, nhưng final image chỉ chứa layers của stage cuối cùng. Build tools (compiler, SDK), source code, và dev dependencies từ các stage trước bị loại bỏ hoàn toàn — không có mặt trong image cuối cùng.
🧠 Quiz
Câu 2: Hadolint rule DL3007 cảnh báo về vấn đề gì?
- [ ] A. Dockerfile thiếu HEALTHCHECK instruction
- [ ] B. Dùng ADD thay vì COPY
- [x] C. Dùng tag
latestcho base image — không reproducible - [ ] D. Không set WORKDIR trước khi COPY
💡 Giải thích: DL3007 cảnh báo khi bạn dùng
:latesttag.latestthay đổi theo thời gian — cùng Dockerfile nhưng tạo ra image khác nhau vào các thời điểm khác nhau. Luôn pin version cụ thể:node:20.11-alpine3.19.
🧠 Quiz
Câu 3: .dockerignore có tác dụng gì?
- [ ] A. Ngăn Docker daemon tải các image không cần thiết
- [ ] B. Loại bỏ files khỏi image sau khi build xong
- [x] C. Loại trừ files/directories khỏi build context trước khi gửi đến daemon
- [ ] D. Tự động xóa cache sau mỗi lần build
💡 Giải thích:
.dockerignorehoạt động ở bước đầu tiên — trước khi thư mục được gửi đến daemon. Nó giảm kích thước build context (từ hàng trăm MB xuống vài MB), tăng tốc build, và ngăn chặn secrets (.env) bị vô tình copy vào image.
🧠 Quiz
Câu 4: COPY --from=builder có copy những gì?
- [ ] A. Files, ENV variables, và WORKDIR
- [ ] B. Toàn bộ filesystem và metadata của stage builder
- [x] C. Chỉ files và directories — không copy ENV, WORKDIR, USER
- [ ] D. Chỉ executable files có permission +x
💡 Giải thích:
COPY --fromchỉ copy files và directories. ENV, WORKDIR, USER, EXPOSE, và các metadata khác không được copy. Bạn phải khai báo lại chúng ở stage mới. Đây là cạm bẫy phổ biến nhất khi viết multi-stage Dockerfile.
✅ Checklist trước khi áp dụng
✅ Checklist triển khai
Multi-stage & Optimization Checklist
- [ ] Multi-stage build: tách build stage và runtime stage
- [ ] Runtime stage dùng image nhẹ nhất có thể (alpine, slim, distroless, scratch)
- [ ]
COPY --from=builderchỉ copy artifacts cần thiết - [ ] ENV, WORKDIR, USER được set lại ở runtime stage
- [ ]
.dockerignoreloại trừ:.git,node_modules,.env,coverage,*.md - [ ] Hadolint pass không có error (warning chấp nhận được nếu có lý do)
- [ ] Base image pin version cụ thể (không dùng
latest) - [ ] Production stage có non-root USER
- [ ] Không có secrets trong bất kỳ stage nào
- [ ] Build context size kiểm tra:
docker buildhiển thị < 10MB