Lần đầu dùng Docker Compose cho dự án thực tế, mình mắc khá nhiều lỗi cơ bản mà bây giờ nghĩ lại thấy buồn cười — deploy xong thì site down cả tiếng đồng hồ vì quên drain traffic trước khi restart container. Team hỏi “sao down vậy?” mà mình không biết giải thích. Đó là điểm khởi đầu để mình đào sâu vào Docker Swarm nâng cao: Rolling Update có kiểm soát, Placement Constraints, và Docker Config.
Bài này dành cho bạn đã biết Swarm cơ bản và muốn cấu hình production thật sự — không phải demo lab, mà là hệ thống chạy 24/7 có user thật.
Bối cảnh: Tại sao cần thêm lớp cấu hình này?
Swarm mặc định đã khá ổn, nhưng khi đưa lên production bạn sẽ gặp ngay mấy vấn đề hay gặp:
- Rolling update mặc định gây downtime: Swarm stop replica cũ trước khi start replica mới, tạo ra khoảng hở thời gian không có instance nào phục vụ — user thấy 502.
- Container phân bổ không kiểm soát: Database nặng có thể bị schedule lên node RAM thấp, toàn bộ API replica có thể dồn vào 1 node rồi node đó down là xong.
- Config và secret trong environment variable: Dễ bị leak qua
docker inspect, log aggregation, hayps auxtrên host. - Không có rollback tự động: Deploy xong mới phát hiện lỗi, phải xử lý thủ công trong khi user đang thấy lỗi thật.
Ba tính năng — Placement Constraints, Docker Config/Secret, và Rolling Update với order: start-first — giải quyết đúng ba vấn đề trên.
Chuẩn bị: Gán label cho nodes
Label là nền tảng của Placement Constraints. Trước khi viết stack file, cần gán label cho từng node theo vai trò và đặc điểm phần cứng. Đây là bước nhiều tutorial bỏ qua, khiến người đọc loay hoay không hiểu tại sao constraint không hoạt động:
# Gán role label cho worker nodes
docker node update --label-add role=worker node-1
docker node update --label-add role=worker node-2
# Gán storage type — quan trọng cho database
docker node update --label-add storage=ssd node-1
docker node update --label-add storage=hdd node-2
# Gán availability zone nếu cluster multi-region
docker node update --label-add zone=az-1 node-1
docker node update --label-add zone=az-2 node-2
# Verify label đã được gán đúng
docker node inspect node-1 --format '{{json .Spec.Labels}}'
docker node ls --format 'table {{.Hostname}}\t{{.Status}}\t{{.ManagerStatus}}'
Sau khi gán label, kiểm tra lại bằng docker node ls -q | xargs docker node inspect --format '{{.Description.Hostname}}: {{.Spec.Labels}}' để chắc chắn tất cả nodes đều có label trước khi deploy stack.
Cấu hình chi tiết
Docker Config và Secret — quản lý cấu hình đúng cách
Thay vì truyền config qua environment variable, Docker Config lưu file cấu hình tĩnh (nginx.conf, app.yaml…) và Docker Secret lưu thông tin nhạy cảm (password, API key). Cả hai đều được mã hóa khi lưu trữ và chỉ decrypt trong RAM của container đang chạy:
# Tạo config từ file
docker config create nginx_conf ./nginx.conf
docker config create app_settings ./app.yaml
# Tạo secret từ file (khuyến nghị hơn stdin để tránh lưu vào shell history)
docker secret create db_password ./db_password.txt
docker secret create jwt_secret ./jwt_secret.txt
# Verify
docker config ls
docker secret ls
Khai báo và mount vào container trong stack file:
configs:
nginx_conf:
external: true
app_settings:
external: true
secrets:
db_password:
external: true
services:
nginx:
image: nginx:1.25-alpine
configs:
- source: nginx_conf
target: /etc/nginx/nginx.conf
mode: 0440 # Read-only cho owner và group, không cho others
api:
image: myapp/api:latest
configs:
- source: app_settings
target: /app/config/settings.yaml
secrets:
- source: db_password
target: db_password
mode: 0400 # Chỉ owner đọc được — secret nên strict hơn config
Secret được mount tại /run/secrets/<secret_name> trong container. App đọc file này thay vì đọc environment variable — cách này an toàn hơn hẳn vì secret không xuất hiện trong environment của process, không bị lộ qua docker inspect.
Placement Constraints — workload phân bổ đúng node
Với label đã gán ở trên, giờ có thể kiểm soát chính xác container nào chạy trên node nào. constraints là hard rule (phải thỏa mãn), còn preferences là soft rule (cố gắng thỏa mãn khi có thể):
services:
api:
deploy:
replicas: 4
placement:
constraints:
- node.role == worker # API không chạy trên manager node
- node.labels.role == worker # Double check qua custom label
preferences:
- spread: node.labels.zone # Phân tán đều ra các AZ, không dồn vào 1 chỗ
database:
deploy:
replicas: 1
placement:
constraints:
- node.labels.storage == ssd # DB chỉ chạy trên node có SSD
- node.role == worker # Không chạy trên manager
redis:
deploy:
replicas: 1
placement:
constraints:
- node.labels.zone == az-1 # Pin Redis vào zone cụ thể nếu cần
Rolling Update không downtime — cấu hình chi tiết
Phần này hay bị cấu hình sai nhất. Tham số mấu chốt là order: start-first — Swarm start replica mới, đợi healthcheck pass, rồi mới stop replica cũ. Ngược với mặc định stop-first gây downtime.
Nhưng order: start-first chỉ hoạt động đúng khi kết hợp với healthcheck được cấu hình hợp lý:
services:
api:
image: myapp/api:${VERSION:-latest}
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://localhost:3000/health || exit 1"]
interval: 10s
timeout: 5s
retries: 3
start_period: 30s # Cho app 30s để startup trước khi bắt đầu đánh giá health
deploy:
replicas: 4
update_config:
parallelism: 1 # Update 1 replica mỗi lần — conservative nhưng an toàn
delay: 15s # Đợi 15s giữa các batch update
order: start-first # START replica mới TRƯỚC, STOP replica cũ SAU
failure_action: rollback # Tự động rollback toàn bộ nếu update fail
monitor: 30s # Monitor 30s sau mỗi update để phát hiện lỗi delayed
max_failure_ratio: 0.3 # Cho phép tối đa 30% replica fail trước khi trigger rollback
rollback_config:
parallelism: 0 # 0 = rollback tất cả replicas đồng thời
delay: 0s # Không delay khi rollback — cần nhanh
failure_action: continue # Tiếp tục rollback dù gặp lỗi
order: stop-first # Khi rollback: stop phiên bản mới trước, restore cũ sau
start_period trong healthcheck là thứ mình mất nhiều thời gian nhất để tune đúng. Nếu app cần 20 giây để connect database, load config và warmup cache — set start_period: 25s để có buffer. Thiếu thông số này, Swarm đánh giá container fail ngay lúc startup — container cứ restart mãi và rolling update không bao giờ hoàn thành.
Kiểm tra và Monitoring
Deploy stack và theo dõi rolling update
# Deploy stack lần đầu
docker stack deploy -c docker-stack.yml myapp
# Xem tất cả services trong stack
docker stack services myapp
# Update API lên version mới — rolling update tự động chạy
VERSION=v2.1.0 docker stack deploy -c docker-stack.yml myapp
# Theo dõi quá trình rolling update real-time
# Quan sát: replica mới Running TRƯỚC khi replica cũ Shutdown
watch -n2 'docker service ps myapp_api --format "table {{.Name}}\t{{.Node}}\t{{.CurrentState}}\t{{.DesiredState}}"'
Khi update diễn ra đúng, bạn sẽ thấy thời điểm có replicas+1 tasks đang tồn tại: replica mới ở trạng thái Running trong khi replica cũ chưa chuyển sang Shutdown. Đó là bằng chứng zero-downtime đang hoạt động.
Rollback và kiểm tra placement
# Rollback service về version trước (nếu cần can thiệp thủ công)
docker service rollback myapp_api
# Verify database đang chạy đúng trên SSD node
docker service ps myapp_database --format 'table {{.Name}}\t{{.Node}}\t{{.CurrentState}}'
# Xem distribution của API replicas qua các nodes
docker service ps myapp_api --filter 'desired-state=running'
# Aggregate logs từ tất cả replicas của service
docker service logs -f --tail 100 myapp_api
Giám sát resource usage
# Resource usage của tất cả containers trong service
docker stats $(docker ps --filter 'name=myapp_api' -q)
# Xem resource limits đã cấu hình
docker service inspect myapp_api --pretty | grep -A 8 Resources
# Health status của tất cả tasks
docker service ps myapp_api --format 'table {{.Name}}\t{{.Node}}\t{{.CurrentState}}\t{{.Error}}'
Sau khi thiết lập xong, thứ mình hay làm là chạy một “dry” rolling update: tag lại image cùng code với version number mới, trigger update, rồi ngồi xem watch docker service ps. Nếu replica mới sang Running trước khi replica cũ sang Shutdown — zero-downtime deployment đang chạy đúng.
Ba thứ này — Placement Constraints để workload đúng chỗ, Docker Config/Secret để bảo mật cấu hình, Rolling Update với order: start-first để deploy không gián đoạn — là những gì cần có từ ngày đầu khi đưa Swarm lên production. Không phức tạp, nhưng thiếu một trong ba là kiểu gì cũng gặp incident. Mình học bài này theo cách khó hơn.
