Làm ngay trong 5 phút: Stack hoàn chỉnh chạy được liền
Thay vì giải thích lý thuyết, mình sẽ bắt đầu bằng thứ thực tế nhất — một stack app + database chạy được ngay. Tạo file docker-compose.yml:
version: '3.8'
services:
db:
image: mysql:8.0
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: rootpass
MYSQL_DATABASE: myapp
MYSQL_USER: app_user
MYSQL_PASSWORD: app_pass
volumes:
- db_data:/var/lib/mysql
app:
image: wordpress:latest
restart: unless-stopped
ports:
- "8080:80"
environment:
WORDPRESS_DB_HOST: db
WORDPRESS_DB_NAME: myapp
WORDPRESS_DB_USER: app_user
WORDPRESS_DB_PASSWORD: app_pass
depends_on:
- db
volumes:
db_data:
Chạy stack:
docker compose up -d
Kiểm tra trạng thái:
docker compose ps
docker compose logs -f app
Mở http://localhost:8080 — WordPress đang chạy với MySQL. Cả quá trình mất chưa đầy 30 giây. So sánh với cách thủ công: một lệnh docker run cho MySQL với 5-6 flag, một lệnh khác cho WordPress với 8-10 flag, rồi tự tạo network để hai container nhìn thấy nhau… Compose gói tất cả vào một file, một lệnh.
Giải thích chi tiết: Hiểu để không bị bẫy
depends_on không phải là “chờ service sẵn sàng”
Cái bẫy này mình đã tự bước vào lần đầu setup production. depends_on chỉ đảm bảo container khởi động theo thứ tự — không đảm bảo service bên trong đã sẵn sàng nhận kết nối. MySQL thường cần 15-30 giây để init và chấp nhận connection, nhưng app container đã start xong trước đó rồi.
Fix đúng cách bằng healthcheck:
services:
db:
image: mysql:8.0
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
app:
depends_on:
db:
condition: service_healthy
Networks: Containers “nhìn thấy” nhau thế nào
Docker Compose tự tạo một bridge network riêng cho mỗi project. Các container giao tiếp qua tên service — không phải IP. Đó là lý do bạn dùng WORDPRESS_DB_HOST: db thay vì IP address cứng: IP có thể thay đổi mỗi lần restart, tên service thì không.
Khi stack lớn hơn, bạn sẽ muốn tách traffic. Ví dụ nginx cần nói chuyện với cả frontend lẫn backend, nhưng database không được phép ra ngoài internet:
networks:
frontend:
driver: bridge
backend:
driver: bridge
internal: true # Không có internet access
services:
nginx:
networks:
- frontend
- backend
api:
networks:
- backend
db:
networks:
- backend
internal: true là mẹo hay để isolate database — container db không thể tự outbound ra ngoài internet, giảm attack surface đáng kể.
Volumes: Named vs Bind Mount
services:
app:
volumes:
# Named volume — Docker quản lý, persist khi container bị xóa
- app_data:/var/www/html/uploads
# Bind mount — mount thư mục từ host (tiện khi dev)
- ./config/nginx.conf:/etc/nginx/nginx.conf:ro
# tmpfs — chỉ trong RAM, mất khi container stop (dùng cho cache)
- type: tmpfs
target: /tmp/cache
volumes:
app_data:
Quy tắc đơn giản: bind mount cho config files khi dev — sửa nginx.conf trên host là apply ngay, không cần rebuild image. Named volume cho data cần persist như database và uploads — Docker quản lý, tồn tại ngay cả khi bạn xóa và tạo lại container.
Nâng cao: Patterns dùng hàng ngày trên production
Tách môi trường với .env file
Không bao giờ hardcode credentials trong docker-compose.yml. Dùng .env và thêm nó vào .gitignore ngay:
# .env
MYSQL_ROOT_PASSWORD=super_secret_pass
MYSQL_DATABASE=production_db
APP_PORT=8080
APP_ENV=production
# docker-compose.yml
services:
db:
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
MYSQL_DATABASE: ${MYSQL_DATABASE}
app:
ports:
- "${APP_PORT}:80"
environment:
APP_ENV: ${APP_ENV}
Override files cho dev và prod
Pattern này cực kỳ tiện khi team vừa dev vừa deploy production từ cùng một codebase. Giữ config chung trong docker-compose.yml, tạo file override riêng cho từng môi trường:
# docker-compose.override.yml (tự động load khi chạy 'docker compose up')
services:
app:
volumes:
- .:/var/www/html # Mount source code khi dev
environment:
APP_DEBUG: "true"
# docker-compose.prod.yml
services:
app:
deploy:
resources:
limits:
cpus: '0.5'
memory: 512M
restart: always
# Deploy production
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d
Resource limits — bảo vệ cả cluster
Trên cluster chạy 30+ container, mình hay gặp tình huống này: một service bị memory leak âm thầm ăn hết RAM, kéo cả node xuống. Không ai hay vì không có gì ngăn nó cả. Apply resource limits xong, chuyện đó không tái diễn — và resource usage tổng giảm khoảng 40% nhờ mỗi service chỉ được dùng đúng phần của mình.
services:
api:
deploy:
resources:
limits:
cpus: '1.0'
memory: 256M
reservations:
cpus: '0.25'
memory: 128M
Profiles: Chỉ chạy service khi cần
services:
app: # Không có profile = luôn chạy
db: # Không có profile = luôn chạy
adminer:
image: adminer
profiles: ["tools"] # Chỉ chạy khi gọi với --profile tools
ports:
- "8081:8080"
# Chạy app + db bình thường
docker compose up -d
# Bật thêm adminer khi cần debug database
docker compose --profile tools up -d
Tips thực tế: Những lệnh hay dùng mà ít ai nhớ
Lệnh hữu ích trong công việc hàng ngày
# Xem log nhiều service cùng lúc, filter theo keyword
docker compose logs -f --tail=100 app db | grep ERROR
# Exec vào container đang chạy
docker compose exec app bash
# Scale service mà không down các service khác
docker compose up -d --scale worker=3
# Rebuild và redeploy chỉ 1 service
docker compose up -d --no-deps --build app
# Xem resource usage realtime
docker compose stats
# Pull image mới nhất và restart
docker compose pull && docker compose up -d
Đặt tên project — tránh conflict sau 3 tháng
Docker Compose đặt tên container theo pattern {project}_{service}_{replica}. Project name mặc định là tên thư mục. Vấn đề: hai project khác nhau cùng đặt thư mục là app sẽ share network và conflict container name — một lỗi rất khó debug khi nó xảy ra lúc 2 giờ sáng. Khai báo name ngay đầu file là xong:
name: myapp-production
services:
...
Cleanup định kỳ để không hết disk
# Xóa containers và networks của project (giữ volumes)
docker compose down
# Xóa luôn volumes — cẩn thận, mất data!
docker compose down -v
# Dọn dẹp toàn bộ Docker: stopped containers, unused images, volumes
docker system prune -a --volumes
Mình đặt cron chạy docker system prune -f hàng tuần trên server — tiết kiệm được cả chục GB disk space mỗi tháng mà không cần can thiệp thủ công.

