Docker Best Practices: Từ Dev Đến Production
Danh Mục Bài Viết
- 1. Tại Sao Docker Best Practices Lại Quan Trọng?
- 2. Viết Dockerfile Tối Ưu: Những Nguyên Tắc Xương Máu
- 3. Bảo Mật Container: Đừng Chạy Bằng Root
- 4. Resource Limits: Container Không Phải Là Vô Hạn
- 5. Logging Và Monitoring: Nhìn Thấy Mới Sửa Được
- 6. Những Lỗi Hay Gặp Và Cách Tránh
- 7. FAQ
- 8. Docker Hygiene Là Đầu Tư, Không Phải Chi Phí
Cái câu "works on my machine" — bạn đã nghe bao nhiêu lần rồi? Docker sinh ra để giải quyết đúng vấn đề đó. Thế nhưng nghịch lý là, dù đã dùng Docker, nhiều team vẫn gặp đúng bài toán cũ: container chạy tốt ở dev, lên staging thì lạ, lên production thì vỡ. Lý do? Docker là công cụ mạnh, nhưng dùng sai thì vẫn là sai.
Tại Sao Docker Best Practices Lại Quan Trọng?
Docker giúp đóng gói ứng dụng cùng mọi dependency vào một container có thể chạy ở bất kỳ đâu. Nghe thì đơn giản. Nhưng khi ứng dụng có hàng chục service, traffic tăng đột biến, hay một CVE mới xuất hiện trong base image — đó là lúc bạn nhận ra sự khác biệt giữa "chạy được" và "chạy đúng cách".
Một Dockerfile viết tệ có thể tạo ra image nặng cả GB, chứa credential bị leak vào layer, hay build lại từ đầu mỗi khi chỉ thay đổi một dòng code. Một container thiếu giới hạn resource có thể nuốt toàn bộ RAM của host. Một image chạy bằng root là lỗ hổng bảo mật đang chờ bị khai thác. Những vấn đề này không hiếm — chúng xảy ra hằng ngày trong các team chưa đầu tư đúng mức vào Docker hygiene.
Viết Dockerfile Tối Ưu: Những Nguyên Tắc Xương Máu
Multi-stage builds là bắt buộc, không phải tùy chọn. Đây là kỹ thuật tách môi trường build và runtime, giúp image production chỉ chứa những gì cần thiết. Thay vì ship cả compiler, build tools, dev dependencies lên server — bạn chỉ ship binary hoặc artifact cuối cùng.
# Stage 1: Build
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
RUN npm run build
# Stage 2: Runtime (chỉ giữ những gì cần)
FROM node:20-alpine AS runtime
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
EXPOSE 3000
CMD ["node", "dist/index.js"]
Kết quả? Image từ ~800MB xuống còn ~120MB. Thời gian deploy nhanh hơn, bề mặt tấn công nhỏ hơn.
Thứ tự COPY rất quan trọng cho cache. Docker cache theo layer — một layer thay đổi thì mọi layer phía sau đều rebuild. Nguyên tắc: copy thứ ít thay đổi nhất lên trước.
# Sai: copy toàn bộ code trước khi install deps
COPY . .
RUN npm install
# Đúng: install deps trước (cached), copy code sau
COPY package*.json ./
RUN npm ci
COPY . .
Dùng .dockerignore nghiêm túc. File này tương tự .gitignore nhưng cho build context. Thiếu nó, bạn có thể vô tình copy node_modules, .git, file .env, hay credential vào image.
node_modules
.git
.env*
*.log
dist
coverage
Chọn base image nhỏ và cụ thể. node:20-alpine thay vì node:latest. Alpine Linux chỉ khoảng 5MB, giảm attack surface đáng kể. Và luôn pin phiên bản cụ thể — node:20.11.1-alpine3.19 tốt hơn node:20-alpine — để tránh surprises khi base image update.
Bảo Mật Container: Đừng Chạy Bằng Root
Theo mặc định, process trong container chạy bằng user root. Đây là rủi ro nghiêm trọng — nếu attacker thoát khỏi container (container escape), họ có toàn quyền trên host. Best practice là tạo non-root user ngay trong Dockerfile:
FROM node:20-alpine
WORKDIR /app
# Tạo user riêng
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
COPY --chown=appuser:appgroup . .
RUN npm ci --only=production
USER appuser # switch sang non-root
EXPOSE 3000
CMD ["node", "index.js"]
Read-only filesystem khi có thể. Chạy container với --read-only và mount riêng thư mục cần ghi. Điều này ngăn malware ghi file vào hệ thống.
docker run --read-only --tmpfs /tmp --mount type=volume,src=app-data,dst=/app/data myapp:latest
Không hardcode secrets vào image. Đây là lỗi phổ biến nhất mình thấy. Dùng environment variables, Docker Secrets (Swarm), hay secret management từ bên ngoài như AWS Secrets Manager hoặc HashiCorp Vault. Secrets trong ENV bị lưu vào image layer và có thể inspect được qua docker history.
Mẹo thực chiến: Chạy
docker scout cves myimage:latesthoặctrivy image myimage:latestđể scan CVE trước khi push lên registry. Tích hợp bước này vào CI pipeline — không phải làm thủ công.
Resource Limits: Container Không Phải Là Vô Hạn
Một container không có giới hạn resource có thể chiếm toàn bộ CPU và RAM của host, kéo sập mọi service khác. Trong production, luôn set limits:
docker run --memory="512m" --memory-swap="512m" --cpus="0.5" myapp:latest
Trong Docker Compose hoặc Kubernetes, config tương tự nhưng khai báo trong YAML. Quan trọng là phân biệt giữa --memory (hard limit — OOM kill khi vượt) và --memory-reservation (soft limit — scheduler hint). Đừng set cả hai bằng nhau nếu ứng dụng cần burst ngắn hạn.
Health checks nên được define trong Dockerfile, không phải sau khi deploy xong. Orchestrator như Kubernetes hay Docker Swarm cần health check để biết khi nào restart container:
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 CMD curl -f http://localhost:3000/health || exit 1
Logging Và Monitoring: Nhìn Thấy Mới Sửa Được
Container ephemeral — nó có thể restart, scale, hay bị terminate bất kỳ lúc nào. Nếu log nằm trong container filesystem, chúng biến mất cùng container. Best practice là ghi log ra stdout/stderr, để Docker logging driver xử lý.
# Cấu hình logging driver trong docker-compose.yml
services:
app:
image: myapp:latest
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
Hoặc dùng centralized logging: Fluent Bit → CloudWatch Logs, hoặc Loki → Grafana. Đừng để mỗi container tự quản lý log file — vừa khó aggregate, vừa tốn disk không kiểm soát được.
Về metrics, cAdvisor là lựa chọn phổ biến để collect container metrics (CPU, memory, network I/O) rồi đẩy vào Prometheus. Kết hợp với Grafana dashboard, bạn có một monitoring stack nhẹ nhàng mà hiệu quả cho môi trường Docker thuần hoặc Docker Swarm.
Những Lỗi Hay Gặp Và Cách Tránh
Image tag "latest" trong production. Tag này không có semantic version — khi ai đó push image mới với tag latest, bạn không biết mình đang chạy version nào. Dùng SHA digest hoặc semantic version: myapp:1.2.3 hoặc myapp@sha256:abc123...
Không dùng .dockerignore. Build context bự → build chậm → đôi khi copy cả credential vào image. Mình đã thấy case production image chứa cả file .env với database password. Không phải joke.
Chạy nhiều process trong một container. Container nên chạy một process chính (PID 1). Nếu cần process supervisor, dùng tini làm init để handle zombie processes đúng cách. Chạy app server + Nginx + cron trong cùng container là anti-pattern.
Bỏ qua OOM killer signals. Khi container bị OOM kill, ứng dụng nhận SIGKILL — không có graceful shutdown. Implement signal handling trong app (lắng nghe SIGTERM) và config --stop-timeout để Kubernetes/Docker chờ đủ lâu trước khi force kill.
FAQ
Q: Nên dùng Docker Compose hay Kubernetes cho production?A: Phụ thuộc quy mô. Docker Compose phù hợp cho single-host deployment, team nhỏ, ứng dụng không cần auto-scaling phức tạp. Kubernetes mạnh hơn nhiều về scheduling, self-healing, rolling updates — nhưng overhead vận hành lớn hơn đáng kể. Khởi đầu bằng Compose, chuyển sang K8s khi thực sự cần horizontal scaling và multi-host.
Q: Image bao nhiêu MB là "tốt"?A: Không có con số chuẩn, nhưng nguyên tắc là càng nhỏ càng tốt. Web app Node.js production thường nên dưới 150–200MB. Nếu image của bạn vượt 500MB mà không phải ML model hay data-heavy app, hãy xem lại Dockerfile — khả năng cao là có thứ gì đó không cần thiết đang bị pack vào.
Q: Container restart policy nên set như thế nào?A: Trong production,
--restart=on-failure:5thường hợp lý hơn--restart=always. Cái sau sẽ cứ restart kể cả khi container fail do bug logic — dẫn đến restart loop vô tận và tốn resource.on-failurechỉ restart khi exit code khác 0, và giới hạn số lần retry.
Docker Hygiene Là Đầu Tư, Không Phải Chi Phí
Khổ nỗi là nhiều team chỉ nghĩ đến Docker best practices khi đã có sự cố. Image bị exploit vì chạy root, deployment chậm vì image 2GB, log mất sạch sau khi container restart — những bài học này đều có giá.
Nói thật thì, phần lớn các practices ở đây không tốn nhiều effort để implement — viết Dockerfile tốt hơn, thêm .dockerignore, set resource limits, bật health check. Nhưng tác động dài hạn rất lớn: build nhanh hơn, deploy ổn định hơn, bảo mật tốt hơn, và ít mất ngủ hơn khi on-call.
Nghĩ mà xem — đầu tư vài giờ hôm nay để làm đúng, hay dành hàng đêm debug production incident sau này?
