Skip to content

Module 2: The Art of Building (Nghệ thuật xây dựng Image)

🎓 Instructor Profile

Kỹ sư Raizo (Phó CTO HPN) - người bị ám ảnh bởi việc tối ưu từng KB dung lượng Image, đồng hành cùng Giáo sư Tom - chuyên gia phân tích nguyên lý.

Chào mừng đến với "nhà máy" Docker. Viết Dockerfile không khó, nhưng viết Dockerfile để Build nhanh, Image nhẹAn toàn là cả một nghệ thuật (Engineering Art).

Trong module này, chúng ta sẽ giải phẫu Dockerfile và học các kỹ thuật tối ưu mà 90% Junior Developer thường bỏ qua.


🧬 Phần 1: Anatomy of a Dockerfile (Giải phẫu cơ bản)

Dockerfile không chỉ là một kịch bản (script) cài đặt. Nó là Bản thiết kế hạ tầng (Infrastructure Blueprint). Một Dockerfile chuẩn sẽ kể câu chuyện về cách ứng dụng của bạn được hình thành.

Các từ khóa cốt lõi (Keywords)

KeywordGiải thíchBest Practice
FROMChọn nền móng cho ngôi nhà.Ưu tiên alpine (siêu nhẹ, ~5MB) hoặc slim thay vì bản full (hàng trăm MB).
WORKDIRThiết lập thư mục làm việc.Luôn dùng. Đừng vứt code bừa bãi ở thư mục gốc /. Giúp cô lập không gian ứng dụng.
COPYSao chép file từ máy host vào image.Dùng COPY thay vì ADD. COPY chỉ làm một việc là copy.
RUNThực thi lệnh trong quá trình build.Gộp các lệnh RUN bằng && để giảm số lượng layer (sẽ giải thích sau).

⚠️ COPY vs ADD

Tại sao các chuyên gia bảo mật khuyên dùng COPY?

  • ADD quá "thông minh": Nó có thể tự động giải nén file tarball hoặc tải file từ URL độc hại. Điều này tạo ra bề mặt tấn công không cần thiết.
  • COPY đơn giản, minh bạch và dễ kiểm soát. Keep It Simple.

🚀 Phần 2: The Layer Caching Strategy (Chiến lược bộ đệm)

Đây là kiến thức Core Knowledge phân định Senior và Junior.

Cơ chế UnionFS & Layers

Hãy tưởng tượng Docker Image như một chiếc Bánh Crepe ngàn lớp. Mỗi dòng lệnh trong Dockerfile (RUN, COPY, FROM) sẽ tạo ra một lớp (layer) mới đè lên lớp cũ. Docker sử dụng Layer Caching: Nếu nội dung của một layer và các layer trước nó không thay đổi, Docker sẽ dùng lại "bánh cũ" thay vì nướng lại từ đầu.

Quy tắc vàng: Sắp xếp theo tần suất thay đổi

Sắp xếp lệnh từ "Ít thay đổi nhất" đến "Hay thay đổi nhất".

Bad Practice (Cách làm non tay)

dockerfile
WORKDIR /app
# Copy toàn bộ code (Hay thay đổi) vào trước
COPY . . 
# Cài dependencies (Ít thay đổi) sau
RUN npm install

Hậu quả: Chỉ cần bạn sửa 1 dấu phẩy trong code (COPY . . thay đổi), Docker sẽ hủy cache từ dòng đó trở đi. Lệnh npm install nặng nề sẽ phải chạy lại mỗi lần build. Lãng phí thời gian khủng khiếp.

Good Practice (Chuẩn Engineering)

dockerfile
WORKDIR /app
# 1. Copy file định nghĩa dependency trước
COPY package.json package-lock.json ./
# 2. Cài đặt dependency (Tận dụng Cache tối đa)
RUN npm install
# 3. Mới copy source code vào sau
COPY . .

Lợi ích: Code sửa thoải mái, npm install vẫn được cache lại (trừ khi bạn cài thêm thư viện mới). Tốc độ build lại giảm từ phút xuống giây.


🎭 Phần 3: CMD vs ENTRYPOINT (Cuộc chiến định danh)

Hai lệnh này đều dùng để chạy ứng dụng khi container khởi động, nhưng ngữ nghĩa khác nhau hoàn toàn.

Metaphor: Máy xay sinh tố

  • ENTRYPOINT: Là Cái máy xay. Nó là thành phần cố định, khó thay đổi.
  • CMD: Là Hoa quả bỏ vào máy. Nó là tham số mặc định, dễ dàng thay thế.

Công thức: Container Runtime = ENTRYPOINT + CMD

Ví dụ thực chiến

dockerfile
# Máy xay (Executable)
ENTRYPOINT ["/bin/ping"]
# Nguyên liệu mặc định (Default Argument)
CMD ["ganvn.cn"]
  • Trường hợp 1: Chạy docker run my-pinger.
    • Thực thi: /bin/ping ganvn.cn (Dùng CMD mặc định).
  • Trường hợp 2: Chạy docker run my-pinger google.com.
    • Thực thi: /bin/ping google.com (google.com ghi đè ganvn.cn). Cái máy xay (ping) vẫn giữ nguyên.

💡 Lời khuyên

Sử dụng ENTRYPOINT cho các ứng dụng đóng gói dạng CLI tool, và dùng CMD để cung cấp các cờ (flags) hoặc tham số mặc định.


🏗️ Phần 4: Multi-Stage Builds (Kỹ thuật "Vắt chanh bỏ vỏ")

Tư duy Architect: Tại sao bạn lại mang cả bộ compiler (GCC, Go, JDK), Maven, Gradle vào môi trường Production trong khi server chỉ cần đúng file binary để chạy?

Kỹ thuật Multi-Stage Builds cho phép bạn dùng nhiều FROM trong một Dockerfile.

  1. Stage 1 (Build): Dùng image đầy đủ công cụ để biên dịch code.
  2. Stage 2 (Run): Dùng image siêu nhẹ, chi copy kết quả (artifact) từ Stage 1 sang.

Ví dụ mô hình Golang

dockerfile
# --- Stage 1: Builder (Cồng kềnh) ---
FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
# Build ra file binary tên là 'myapp'
RUN go build -o myapp main.go

# --- Stage 2: Runner (Siêu nhẹ) ---
# Dùng Alpine trắng trơn
FROM alpine:latest  
WORKDIR /root/
# Chỉ copy file binary từ Stage 1. Bỏ lại toàn bộ source code và bộ Go SDK.
COPY --from=builder /app/myapp .

CMD ["./myapp"]

Kết quả: Image giảm từ ~800MB (Golang base) xuống còn ~10MB (Alpine + Binary). Tiết kiệm băng thông, deploy nhanh thần tốc và giảm bề mặt tấn công.

Production Example: Node.js Multi-Stage

Đây là Dockerfile production-grade cho ứng dụng Node.js/Express với 3 stages:

dockerfile
# --- Stage 1: Dependencies ---
FROM node:20-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --only=production && \
    cp -R node_modules /prod_modules && \
    npm ci

# --- Stage 2: Builder ---
FROM node:20-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build && npm run test

# --- Stage 3: Runner (Production) ---
FROM node:20-alpine AS runner
WORKDIR /app

# Security: Không chạy với root
RUN addgroup -g 1001 -S appgroup && \
    adduser -S appuser -u 1001 -G appgroup

# Chỉ copy production dependencies và build output
COPY --from=deps /prod_modules ./node_modules
COPY --from=builder /app/dist ./dist
COPY package.json ./

LABEL maintainer="team@company.com"
LABEL version="1.0"

USER appuser
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"]

Kết quả: Image từ ~1.1GB (node:20 full + devDependencies + source) giảm xuống ~180MB (Alpine + production deps + compiled output). Bao gồm health check, non-root user, và chỉ chứa đúng những gì production cần.


🛡️ Phần 5: Security & Optimization (An toàn & Tối ưu)

1. .dockerignore - Cái "thùng rác" thông minh

Đừng bao giờ để Docker copy mọi thứ vào Image. Tạo file .dockerignore ngay lập tức để loại bỏ rác và scret.

text
# .dockerignore
.git
node_modules
Dockerfile
.env        # ⚠️ QUAN TRỌNG: Không bao giờ build kèm file chứa mật khẩu
dist
coverage

2. User Permission - Đừng chạy với quyền Root!

Mặc định, Container chạy với quyền root của Linux. Nếu hacker chiếm được container, họ có quyền cao nhất. Hãy tạo một user thường (non-root) để chạy ứng dụng.

dockerfile
# Tạo group và user tên 'appuser'
RUN addgroup -S appgroup && adduser -S appuser -G appgroup

# Chuyển quyền sở hữu thư mục
RUN chown -R appuser:appgroup /app

# Kích hoạt user (Từ dòng này trở đi, lệnh chạy bởi appuser)
USER appuser

🛑 Security First

Nguyên tắc Least Privilege (Quyền hạn tối thiểu) là bắt buộc trong môi trường Production chuyên nghiệp. Đừng lười.


⚠️ Production Pitfalls (Bẫy thực chiến)

⚠️ Cạm bẫy

Vấn đề: Để build tools (gcc, make, webpack, TypeScript compiler) trong production image.

Hậu quả:

  • Image production nặng gấp 5-10 lần cần thiết
  • Attack surface mở rộng (attacker có sẵn compiler trong container)
  • Deploy chậm, pull image tốn thời gian và băng thông

Giải pháp: Luôn dùng Multi-Stage builds. Stage 1 build, Stage 2 chỉ copy artifact. Xem ví dụ Golang và Node.js ở Phần 4.

⚠️ Cạm bẫy

Vấn đề: Dùng ADD thay COPY vì nghĩ chúng giống nhau, hoặc đặt COPY . . trước RUN npm install.

Hậu quả:

  • ADD tự động giải nén tar hoặc tải file từ URL — hành vi không minh bạch, rủi ro bảo mật
  • COPY . . trước dependency install → mỗi lần sửa code, Docker rebuild lại toàn bộ dependencies

Giải pháp:

  • Luôn dùng COPY trừ khi bạn thực sự cần giải nén tar (rất hiếm)
  • Copy file lock (package-lock.json) trước → install → copy source code sau

⚠️ Cạm bẫy

Vấn đề: Tách apt-get updateapt-get install thành hai dòng RUN riêng biệt.

dockerfile
# ❌ BAD: apt-get update bị cache cũ, install có thể fail
RUN apt-get update
RUN apt-get install -y curl wget

Hậu quả: apt-get update bị cache → lần build sau, Docker dùng package list cũ → install có thể fail hoặc cài version lỗi thời chứa vulnerabilities.

Giải pháp: Gộp thành một RUN và clean up:

dockerfile
# ✅ GOOD: Gộp update + install + cleanup trong một layer
RUN apt-get update && \
    apt-get install -y --no-install-recommends curl wget && \
    rm -rf /var/lib/apt/lists/*

📝 Quiz: Kiểm tra kiến thức

🧠 Quiz

Câu 1: Lợi ích chính của Multi-Stage Build là gì?

  • [ ] A. Tăng tốc độ biên dịch code
  • [x] B. Giảm kích thước image production bằng cách loại bỏ build tools
  • [ ] C. Cho phép chạy nhiều ứng dụng trong một container
  • [ ] D. Tự động scale container khi traffic tăng

💡 Giải thích: Multi-Stage Build cho phép dùng image đầy đủ tools ở stage build, rồi chỉ copy artifact sang image nhẹ ở stage run. Kết quả: image production nhỏ hơn 5-50 lần, ít vulnerability, deploy nhanh hơn.

🧠 Quiz

Câu 2: Tại sao nên COPY package.json trước khi COPY source code?

  • [ ] A. Vì package.json nhỏ hơn, copy nhanh hơn
  • [ ] B. Vì Docker yêu cầu bắt buộc thứ tự này
  • [x] C. Để tận dụng layer caching — dependency install chỉ chạy lại khi package.json thay đổi
  • [ ] D. Vì source code phải được compile trước khi copy

💡 Giải thích: Docker cache theo layer. Nếu package.json không đổi, layer RUN npm install được cache → build chỉ mất vài giây thay vì vài phút. Đây là kỹ thuật tối ưu build time quan trọng nhất.

🧠 Quiz

Câu 3: Khi nào nên dùng ENTRYPOINT thay vì CMD?

  • [ ] A. Khi container cần chạy nhiều process cùng lúc
  • [ ] B. Khi muốn dễ dàng override lệnh khi docker run
  • [x] C. Khi container đóng vai trò executable cố định, CMD cung cấp default arguments
  • [ ] D. ENTRYPOINT và CMD hoàn toàn giống nhau, dùng cái nào cũng được

💡 Giải thích: ENTRYPOINT định nghĩa executable cố định (khó override), CMD cung cấp arguments mặc định (dễ override). Kết hợp: ENTRYPOINT ["python"] + CMD ["app.py"] → user có thể docker run myimage script.py để đổi argument mà giữ nguyên executable.


Dockerfile Production Checklist

✅ Checklist triển khai

Checklist trước khi merge Dockerfile vào main:

  • [ ] Base image dùng specific tag, không dùng latest
  • [ ] Multi-stage build tách build tools khỏi runtime
  • [ ] Layer ordering tối ưu cho caching (ít thay đổi → nhiều thay đổi)
  • [ ] Non-root USER cho production stage
  • [ ] .dockerignore loại bỏ .git, node_modules, .env, test files
  • [ ] HEALTHCHECK được cấu hình
  • [ ] Không có secrets hoặc credentials trong bất kỳ layer nào
  • [ ] Labels metadata (version, maintainer, description)
  • [ ] RUN instructions gộp để giảm layers, có cleanup
  • [ ] Image đã test local trước khi push lên registry