Giao diện
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ẹ và 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)
| Keyword | Giải thích | Best Practice |
|---|---|---|
FROM | Chọ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). |
WORKDIR | Thiế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. |
COPY | Sao 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. |
RUN | Thự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?
ADDquá "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 installHậ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).
- Thực thi:
- Trường hợp 2: Chạy
docker run my-pinger google.com.- Thực thi:
/bin/ping google.com(google.comghi đèganvn.cn). Cái máy xay (ping) vẫn giữ nguyên.
- Thực thi:
💡 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.
- Stage 1 (Build): Dùng image đầy đủ công cụ để biên dịch code.
- 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
coverage2. 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ả:
ADDtự độ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ậtCOPY . .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 update và apt-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 wgetHậ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.jsonkhông đổi, layerRUN 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),CMDcung 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
- [ ]
.dockerignoreloạ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