Bài toán thực tế
Một máy dev thường phải chạy nhiều project cùng lúc:
- project A cần MySQL + Redis
- project B cũng cần MySQL nhưng data khác
- project C lại có thêm MongoDB
Nếu cài trực tiếp lên host, rất nhanh sẽ dính:
- conflict port
- lệch version
- data của project này lẫn với project khác
Cách gọn nhất là mỗi project có một file docker-compose.yml riêng, trong đó app, MySQL và Redis đi cùng nhau.
File compose hoàn chỉnh cho local dev
Nếu mục tiêu của bạn là: "cho tôi một file dùng được ngay để mỗi project có MySQL, Redis riêng và không đụng nhau", thì có thể bắt đầu luôn bằng setup này:
# docker-compose.yml
services:
api:
build:
context: .
dockerfile: Dockerfile.dev
ports:
- "3000:3000"
volumes:
- .:/app
- /app/node_modules
environment:
NODE_ENV: development
DB_HOST: mysql
DB_PORT: 3306
DB_USER: root
DB_PASS: localroot
DB_NAME: mydb
REDIS_URL: redis://redis:6379
depends_on:
mysql:
condition: service_healthy
redis:
condition: service_started
mysql:
image: mysql:8
environment:
MYSQL_ROOT_PASSWORD: localroot
MYSQL_DATABASE: mydb
ports:
- "3307:3306"
volumes:
- mysql_data:/var/lib/mysql
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-plocalroot"]
interval: 5s
timeout: 5s
retries: 10
start_period: 20s
redis:
image: redis:7-alpine
ports:
- "6380:6379"
volumes:
- redis_data:/data
volumes:
mysql_data:
redis_data:
Dockerfile.dev:
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
CMD ["npm", "run", "start:dev"]
Chạy:
docker compose up -d
docker compose logs -f api
Với file này:
- app chạy ở
localhost:3000 - MySQL của project này chạy ở
localhost:3307 - Redis của project này chạy ở
localhost:6380 - data MySQL và Redis nằm trong volume riêng
Nếu project khác cũng dùng Compose, bạn chỉ cần đổi port host là đủ:
- project A:
3307,6380 - project B:
3308,6381 - project C:
3309,6382
Đó là phần quan trọng nhất để tránh conflict port mà vẫn giữ mỗi project có database/cache riêng.
Giải thích từng phần trong file compose trên
Vì sao api dùng DB_HOST=mysql thay vì localhost?
Vì bên trong network của Docker Compose, các service gọi nhau qua tên service.
Trong file trên:
apinói chuyện với MySQL quamysql:3306apinói chuyện với Redis quaredis:6379
localhost chỉ đúng khi bạn kết nối từ máy host vào container.
Vì sao MySQL là 3307:3306 và Redis là 6380:6379?
Format là:
HOST_PORT:CONTAINER_PORT
Tức là:
- máy host dùng
3307 - bên trong container MySQL vẫn là
3306
Làm vậy giúp mỗi project có thể giữ config bên trong giống nhau, chỉ khác cổng ngoài host.
Ví dụ project khác:
mysql:
ports:
- "3308:3306"
redis:
ports:
- "6381:6379"
Vì sao api mount cả project bằng - .:/app?
Với local dev, cách này tiện hơn mount riêng ./src:/app/src vì:
- code đổi là container thấy ngay
- nhiều framework cần cả config file ngoài
src - đỡ sót thư mục như
prisma,migrations,uploads,nest-cli.json
Phần:
- /app/node_modules
giúp node_modules trong container không bị thư mục host đè lên.
Vì sao MySQL và Redis cần volume riêng?
Vì nếu không có volume, mỗi lần xóa container là mất data.
Phần này giữ data bền hơn:
volumes:
- mysql_data:/var/lib/mysql
- redis_data:/data
Điều này rất hợp cho local dev nhiều project, vì:
- mỗi project có data riêng
- reset project A không ảnh hưởng project B
depends_on và healthcheck để làm gì?
Không ít project bị lỗi lúc start vì app lên trước MySQL.
Trong file này:
mysqlcóhealthcheckapichờmysqlhealthy rồi mới start
Như vậy đỡ gặp lỗi kiểu:
- connect ECONNREFUSED
- database not ready
- migration chạy quá sớm
Các command nên chạy sau khi có file compose
Sau khi có file hoàn chỉnh, phần command mới thực sự dễ hiểu hơn:
# khởi động stack
docker compose up -d
# xem log app
docker compose logs -f api
# xem container nào đang chạy
docker compose ps
# vào shell của app
docker compose exec api sh
# dừng stack
docker compose down
# dừng và xóa luôn volume
docker compose down -v
docker compose down -v rất hữu ích khi bạn muốn reset sạch database của đúng project đó, thay vì dọn thủ công trên máy host.
Khi nào nên expose MySQL, Redis ra host?
Trong local dev, thường chỉ expose để:
- dùng TablePlus, DBeaver
- debug Redis bằng GUI hoặc CLI trên host
Nếu không cần, bạn có thể bỏ hẳn ports của MySQL và Redis. App vẫn kết nối được nội bộ qua tên service.
Ví dụ an toàn hơn:
mysql:
image: mysql:8
volumes:
- mysql_data:/var/lib/mysql
redis:
image: redis:7-alpine
Lúc đó:
- app vẫn dùng được
mysql:3306,redis:6379 - nhưng host không truy cập trực tiếp vào được nữa
Khi nào nên tách riêng MySQL, Redis cho từng project?
Nên tách khi:
- mỗi project có schema khác nhau
- bạn muốn
down -vproject này mà không ảnh hưởng project khác - version MySQL hoặc Redis giữa các project khác nhau
Không nên cố dùng chung một MySQL local cho tất cả project nếu mục tiêu là dev nhanh và dễ reset. Lúc đầu có vẻ gọn, nhưng về sau thường rất dễ lẫn data và lệch config.
File Compose Cho Production
Production cần thêm một số điểm khác:
- Không bind mount source code (build image hoàn chỉnh)
- Không expose port database ra ngoài host
- Thêm restart policy
- Resource limits
# docker-compose.prod.yml
services:
api:
image: myapp:latest # dùng image đã build sẵn, không build lại
ports:
- "3000:3000"
environment:
NODE_ENV: production
DB_HOST: mysql
DB_PASS: ${DB_PASSWORD}
REDIS_URL: redis://redis:6379
restart: unless-stopped
depends_on:
- mysql
- redis
mysql:
image: mysql:8
environment:
MYSQL_ROOT_PASSWORD: ${DB_PASSWORD}
MYSQL_DATABASE: mydb
volumes:
- mysql_data:/var/lib/mysql
# Không expose port ra ngoài host trong production
restart: unless-stopped
redis:
image: redis:7-alpine
volumes:
- redis_data:/data
restart: unless-stopped
nginx:
image: nginx:alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
- ./certs:/etc/nginx/certs:ro
depends_on:
- api
restart: unless-stopped
volumes:
mysql_data:
redis_data:
Deploy:
# Build image mới
docker build -t myapp:latest .
# Pull image mới (nếu dùng registry)
docker compose -f docker-compose.prod.yml pull
# Restart với image mới, không downtime cho các service khác
docker compose -f docker-compose.prod.yml up -d --no-deps api
Các Lệnh Hay Dùng Nhất
# Khởi động tất cả service (detach mode)
docker compose up -d
# Dừng tất cả
docker compose down
# Dừng và xóa cả volume (xóa data)
docker compose down -v
# Xem log một service
docker compose logs -f api
# Restart một service
docker compose restart api
# Vào shell trong container
docker compose exec api sh
# Xem trạng thái các service
docker compose ps
# Build lại image (khi thay đổi Dockerfile)
docker compose build api
# Scale một service (chạy nhiều instance)
docker compose up -d --scale api=3
Healthcheck — Đợi Service Sẵn Sàng Mới Start
API thường bị crash khi khởi động nếu database chưa ready. Dùng healthcheck + depends_on condition để xử lý:
mysql:
image: mysql:8
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-p${MYSQL_ROOT_PASSWORD}"]
interval: 5s
timeout: 5s
retries: 10
start_period: 30s
api:
depends_on:
mysql:
condition: service_healthy # chờ mysql healthy rồi mới start api
Tổng Kết
Docker Compose không chỉ là tool tiện lợi để chạy database nhanh — nó là cách chuẩn để quản lý multi-service application từ dev đến production:
| Cài trực tiếp | Docker Compose | |
|---|---|---|
| Setup machine mới | Cài lại từ đầu | docker compose up |
| Nhiều project cùng lúc | Conflict port, version | Mỗi project độc lập |
| Đồng bộ team | "Máy tao chạy được" | Cùng một config |
| Dọn dẹp | Còn sót file rải rắc | docker compose down -v |
| Production | Config riêng | Cùng file, khác biến môi trường |
Một khi đã quen với Compose, sẽ rất khó quay về cài service trực tiếp trên máy.