Tại sao cần Docker Compose?
Khi app của bạn có nhiều service (API, database, cache, worker, reverse proxy), chạy từng container bằng docker run sẽ gặp vài vấn đề:
- Phải nhớ chính xác tất cả flag, port, volume, env cho từng container
- Không có cách nào khai báo quan hệ giữa các service (API cần database chạy trước)
- Mỗi lần khởi động lại phải gõ lại toàn bộ lệnh, đúng thứ tự
Docker Compose giải quyết những vấn đề này: khai báo toàn bộ stack multi-container trong một file YAML duy nhất, rồi khởi động tất cả bằng một lệnh duy nhất.
graph LR
N[Nginx :80] --> A[API :3000]
A --> R[(Redis)]
A --> P[(Postgres)]
W[Worker] --> R
W --> P
Bài này mình sẽ đi từ zero đến thành thạo Docker Compose cơ bản: cấu trúc file, service, network, volume, biến môi trường, và các lệnh CLI thường dùng.
docker-compose.yml: Cấu trúc tổng quan
Một file docker-compose.yml điển hình trông như thế này:
services:
api:
build: .
ports:
- "3000:3000"
environment:
- DATABASE_URL=postgres://postgres:secret@db:5432/myapp
depends_on:
- db
- redis
worker:
build: ./worker
environment:
- REDIS_URL=redis://redis:6379
db:
image: postgres:16-alpine
volumes:
- pgdata:/var/lib/postgresql/data
environment:
POSTGRES_PASSWORD: secret
redis:
image: redis:7-alpine
volumes:
pgdata:
Ba thành phần chính:
| Thành phần | Vai trò |
|---|---|
services | Định nghĩa từng container (image, port, env, volume, network…) |
networks | Tùy chỉnh mạng ảo giữa các service (không bắt buộc — Compose tự tạo mặc định) |
volumes | Khai báo named volume hoặc bind mount để lưu dữ liệu |
docker compose thay vì docker-compose), field version: ở top-level không còn cần thiết nữa. Nếu bạn đọc tài liệu cũ thấy version: "3.8", cứ bỏ đi — Compose tự động nhận diện và xử lý đúng phiên bản schema.Service definition: Trái tim của Compose
Mỗi service là một container. Bạn khai báo nó chạy từ đâu, port nào, biến môi trường gì, mount volume gì, và restart policy ra sao.
image vs build
Có hai cách chỉ định source cho service:
# Dùng image có sẵn từ registry
redis:
image: redis:7-alpine
# Build từ Dockerfile cục bộ
api:
build:
context: ./api # thư mục chứa Dockerfile
dockerfile: Dockerfile.dev # tên Dockerfile (mặc định là Dockerfile)
Dùng image cho các service off-the-shelf (Redis, Postgres, Nginx). Dùng build cho code của team mình — nhất là trong môi trường dev, nơi bạn thay đổi code liên tục và cần rebuild nhanh.
build.context riêng. Compose gửi toàn bộ context directory lên Docker daemon (giống docker build). Nếu context của bạn chứa node_modules hay .git, thời gian build sẽ chậm đi đáng kể. Luôn dùng .dockerignore — mình đã bàn kỹ ở bài Dockerfile thực chiến — để loại bỏ những file không cần thiết khỏi build context.ports
Cú pháp "HOST:CONTAINER":
ports:
- "3000:3000" # host:container
- "127.0.0.1:5432:5432" # chỉ bind localhost, không expose ra ngoài
ports với database, hoặc chỉ bind 127.0.0.1. Database chỉ cần accessible từ trong internal network của Compose là đủ.volumes
Hai loại mount phổ biến:
services:
db:
volumes:
- pgdata:/var/lib/postgresql/data # named volume (dữ liệu persist)
api:
volumes:
- ./src:/app/src # bind mount (hot reload khi dev)
- /app/node_modules # anonymous volume (giữ node_modules trong container)
volumes:
pgdata: # khai báo named volume ở top-level
- Named volume: Compose quản lý, dữ liệu sống sót qua
docker compose down(trừ khi dùng-v). - Bind mount: Map trực tiếp thư mục host vào container. Lý tưởng cho dev vì code thay đổi trên host được reflect ngay trong container.
- Anonymous volume: Volume không tên, thường dùng để “che” một thư mục trong container khỏi bị bind mount ghi đè (như
/app/node_modulesở trên).
environment và env_file
services:
api:
environment:
- NODE_ENV=development
- DATABASE_URL=postgres://postgres:${DB_PASS}@db:5432/myapp # ${VAR} từ shell hoặc .env
env_file:
- .env.api # load biến từ file
Thứ tự ưu tiên: environment ghi đè env_file, biến shell ghi đè giá trị mặc định trong .env. Compose tự động đọc file .env trong cùng thư mục với docker-compose.yml.
depends_on
services:
api:
depends_on:
- db
- redis
depends_on đảm bảo thứ tự khởi động: db và redis start trước, api start sau. Nhưng — và đây là cái bẫy kinh điển — Compose không đợi service sẵn sàng (ready). Postgres có thể đã start container nhưng vẫn đang chạy init script, Redis có thể chưa listen port.
depends_on chỉ kiểm soát thứ tự start, không kiểm tra ready. API của bạn có thể crash vì connect DB quá sớm. Cách giải quyết: dùng healthcheck kết hợp với depends_on condition (Compose v2.1+) hoặc dùng script wait-for-it trong entrypoint.services:
db:
image: postgres:16-alpine
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 5s
retries: 5
api:
depends_on:
db:
condition: service_healthy # chỉ start khi db healthy
restart policy
services:
api:
restart: unless-stopped # luôn restart, trừ khi stop thủ công
worker:
restart: always # restart trong mọi trường hợp
redis:
restart: on-failure # chỉ restart nếu exit code != 0
Trong dev: thường không cần restart. Trong production: unless-stopped là lựa chọn an toàn nhất.
Networks: DNS tự động giữa các service
Khi bạn chạy docker compose up, Compose tự động tạo một network bridge riêng cho project. Tất cả service trong cùng Compose file tự động join network này — và điều tuyệt vời nhất: mỗi service có thể resolve service khác bằng chính tên service của nó.
# Không cần khai báo networks ở top-level — Compose tự làm
services:
api:
environment:
- REDIS_URL=redis://redis:6379 # "redis" chính là tên service
- DB_HOST=db # "db" chính là tên service
Không cần IP, không cần --link, không cần --network flag. Chỉ cần dùng tên service như hostname. Compose DNS resolver lo phần còn lại.
Nếu bạn muốn tạo network tùy chỉnh (ví dụ tách biệt frontend network và backend network):
networks:
frontend:
backend:
services:
api:
networks:
- backend
nginx:
networks:
- frontend
- backend # Nginx nối cả 2 network để proxy đến API
db:
networks:
- backend # DB chỉ nằm trong backend network, không expose ra frontend
frontend network, backend services (API, DB, Redis) nằm trong backend network. Service nào cần bridge cả hai (như Nginx reverse proxy) thì join cả hai network.Volumes: Dữ liệu không bay hơi
Named volume trong Compose được khai báo ở top-level volumes::
volumes:
pgdata:
driver: local # mặc định
logs:
driver: local
driver_opts:
type: none
device: /mnt/data/logs # mount thư mục cụ thể trên host
o: bind
Dữ liệu trong named volume sống sót qua docker compose down. Muốn xóa sạch:
docker compose down -v # -v xóa cả anonymous và named volume
Cho dev: bind mount là lựa chọn số một vì bạn muốn code change được reflect ngay. Cho production: named volume hoặc external volume (gắn vào ổ đĩa cụ thể trên host) cho database, log, upload.
Biến môi trường và substitution
Compose hỗ trợ variable substitution với cú pháp ${VAR}:
services:
api:
image: myapp:${TAG:-latest} # dùng $TAG từ env, fallback "latest" nếu không có
environment:
- NODE_ENV=${ENV:-development}
- DB_PASS=${DB_PASS:?err} # bắt buộc phải set, nếu không Compose báo lỗi
Compose đọc biến từ:
- Shell environment (
export TAG=v2.0.1) - File
.envtrong cùng thư mục vớidocker-compose.yml - File chỉ định qua
--env-fileflag
Cú pháp fallback:
${VAR:-default}— dùngdefaultnếuVARkhông set hoặc rỗng${VAR-default}— dùngdefaultnếuVARkhông set (nhưng vẫn dùng giá trị rỗng nếu set “”)${VAR:?err}— báo lỗi nếuVARkhông set hoặc rỗng${VAR?err}— báo lỗi nếuVARkhông set
.env vs env_file: File .env được Compose engine đọc để substitute ${VAR} trong chính docker-compose.yml. Còn env_file được inject vào container runtime — tương đương với --env-file flag của docker run. Đây là hai cơ chế khác nhau, đừng nhầm lẫn.CLI commands: Những lệnh dùng hàng ngày
Khởi động và dừng
# Start tất cả service ở chế độ detached (background)
docker compose up -d
# Start và rebuild image nếu code thay đổi
docker compose up -d --build
# Start với file khác tên mặc định
docker compose -f docker-compose.dev.yml up -d
# Dừng và xóa containers, networks (giữ volumes)
docker compose down
# Dừng + xóa cả volumes
docker compose down -v
# Dừng + xóa images đã build
docker compose down --rmi all
Xem trạng thái
# Xem status tất cả service
docker compose ps
# Xem log (theo dõi real-time, chỉ service cụ thể)
docker compose logs -f api
# Xem log nhiều service
docker compose logs -f api worker
# Xem log 100 dòng cuối
docker compose logs --tail 100
Tương tác với service đang chạy
# Chạy lệnh trong service đang chạy
docker compose exec api sh
docker compose exec db psql -U postgres
# Chạy lệnh one-off (tạo container mới, không ảnh hưởng service đang chạy)
docker compose run --rm api npm run migrate
# Restart một service
docker compose restart worker
# Scale service (chỉ với Compose v2, không dùng port mapping cứng)
docker compose up -d --scale worker=3
Build
# Build lại image cho service có build context
docker compose build
# Build không cache
docker compose build --no-cache
# Build service cụ thể
docker compose build api
docker compose up -d --build mỗi sáng — nó vừa rebuild code mới nhất vừa start lại service. Một lệnh, một lần gõ. Không cần nhớ xem hôm qua có thay đổi Dockerfile không.Dev vs Production: Hai mindset, hai Compose file
Dev Compose — Tối ưu cho tốc độ phát triển
# docker-compose.dev.yml
services:
api:
build:
context: .
dockerfile: Dockerfile.dev
ports:
- "3000:3000"
volumes:
- ./src:/app/src # hot reload
- /app/node_modules
environment:
- NODE_ENV=development
- DEBUG=*
command: npm run dev # nodemon / ts-node-dev với hot reload
db:
image: postgres:16-alpine
ports:
- "127.0.0.1:5432:5432" # dev cần connect trực tiếp bằng DB client
redis:
image: redis:7-alpine
ports:
- "127.0.0.1:6379:6379"
Đặc trưng của dev Compose:
- Bind mount để code thay đổi được reflect ngay
- Port mapping cho DB, Redis để connect bằng GUI client (DBeaver, RedisInsight)
- Hot reload qua
commandoverride - Debug flag bật sẵn
- Không cần
restartpolicy
Production Compose — Tối ưu cho ổn định và bảo mật
# docker-compose.prod.yml
services:
api:
image: registry.example.com/myapp:${TAG} # pre-built image, không build tại chỗ
ports:
- "3000:3000"
environment:
- NODE_ENV=production
- DB_PASS=${DB_PASS:?err}
restart: unless-stopped
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
db:
image: postgres:16-alpine
# KHÔNG expose port ra ngoài
volumes:
- pgdata:/var/lib/postgresql/data
restart: unless-stopped
environment:
POSTGRES_PASSWORD: ${DB_PASS:?err}
volumes:
pgdata:
driver: local
Đặc trưng của production Compose:
- Pre-built image từ registry (không
build:) - Không expose port cho database ra ngoài
- Restart policy
unless-stopped - Log rotation để tránh đầy disk
- Secret qua biến môi trường với fallback bắt buộc
${VAR:?err} - Named volume với driver cụ thể
docker-compose.yml làm base, rồi dùng docker-compose.override.yml cho dev, docker-compose.prod.yml cho production. Compose tự động merge override file. Khi chạy production: docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d.Ví dụ thực tế: Full-stack app với Compose
Dưới đây là Compose file cho một app web điển hình: Express API, React frontend, Postgres, Redis, và Nginx reverse proxy.
services:
nginx:
image: nginx:alpine
ports:
- "80:80"
volumes:
- ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf:ro
depends_on:
- api
api:
build:
context: ./backend
dockerfile: Dockerfile
environment:
- DATABASE_URL=postgres://app:${DB_PASS}@db:5432/myapp
- REDIS_URL=redis://redis:6379
- NODE_ENV=production
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
restart: unless-stopped
worker:
build:
context: ./backend
dockerfile: Dockerfile.worker
environment:
- REDIS_URL=redis://redis:6379
depends_on:
redis:
condition: service_healthy
restart: unless-stopped
db:
image: postgres:16-alpine
volumes:
- pgdata:/var/lib/postgresql/data
environment:
POSTGRES_USER: app
POSTGRES_PASSWORD: ${DB_PASS:?err}
POSTGRES_DB: myapp
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app -d myapp"]
interval: 10s
timeout: 5s
retries: 5
restart: unless-stopped
redis:
image: redis:7-alpine
volumes:
- redisdata:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
restart: unless-stopped
volumes:
pgdata:
redisdata:
Một lệnh duy nhất để khởi động toàn bộ stack:
docker compose up -d
So với việc mở 5 terminal và gõ 5 lệnh docker run, Compose đã tiết kiệm cho team mình hàng giờ debug “sao Redis không connect”, “sao port bị conflict”, “ủa container này chưa start à?”.
Tổng kết
Docker Compose là công cụ không thể thiếu khi làm việc với multi-container application. Ba điểm quan trọng nhất mình muốn bạn nhớ sau bài này:
- Một file, một lệnh — toàn bộ stack được khai báo trong
docker-compose.yml, khởi động bằngdocker compose up -d. Không còn cảnh copy-pastedocker runvới hàng tá flag. - DNS tự động — service giao tiếp với nhau qua tên service, không cần IP, không cần
--link. Compose network làm phần việc nặng nhọc cho bạn. - Tách biệt dev/production — dev Compose ưu tiên bind mount và hot reload; production Compose ưu tiên pre-built image, restart policy, và log rotation.
Compose không thay thế Kubernetes hay các orchestrator phức tạp — nhưng với phần lớn dự án từ 1-10 service, nó hoàn toàn đủ dùng và đơn giản hơn nhiều.
Câu hỏi hay gặp
Q: Khi nào dùng docker compose up và khi nào dùng docker compose run?
up start tất cả service trong Compose file và giữ chúng chạy. run chạy một lệnh one-off trong một service (tạo container mới, không ảnh hưởng service đang chạy). Dùng run cho migration, seed data, hay chạy test. Ví dụ: docker compose run --rm api npm run migrate.
Q: Làm sao để rebuild image khi Dockerfile thay đổi?
Dùng docker compose build --no-cache để build lại từ đầu, hoặc docker compose up -d --build để rebuild và restart. Compose cache layer giống docker build, nên nếu Dockerfile không đổi, lần build sau sẽ rất nhanh.
Q: depends_on có đợi service ready không?
Không. depends_on chỉ đảm bảo thứ tự start container. Để đợi service ready (ví dụ Postgres accept connection), bạn cần thêm healthcheck và dùng condition: service_healthy trong depends_on.
Q: Tôi có thể chạy nhiều Compose file cùng lúc không?
Có. Compose tự động merge khi bạn truyền nhiều -f flag: docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d. File sau ghi đè file trước. Pattern phổ biến: base file + override file cho từng môi trường.
Bài tiếp theo (Multi-container): Phần 10: Docker Compose nâng cao, profiles, extends, secrets, Compose v5 SDK, và dev/staging/prod parity.