Giới thiệu
7 phút đọc15 tháng 6, 2026320

Docker Compose: Gom hết stack vào một file, deploy bằng một lệnh

Quản lý multi-container apps không còn đau đầu khi bạn biết dùng Docker Compose đúng cách từ syntax cơ bản đến healthcheck, named volumes và networking.

N

Nguyễn Nhật Long

@nguyennhatlong1303

Docker Compose: Gom hết stack vào một file, deploy bằng một lệnh

Nếu bạn đã quen với Docker rồi, chắc chắn đã từng rơi vào cảnh này: app cần chạy 3-4 container cùng lúc, mỗi cái một lệnh docker run dài ngoằng với đủ thứ flag, port mapping, volume, env var... Mỗi lần setup lại là một lần tra tấn. Docker Compose sinh ra để giải quyết đúng cái vấn đề đó.

Docker Compose là gì, nói thẳng không vòng vo

Đơn giản mà nói: Docker Compose là tool cho phép bạn định nghĩa toàn bộ stack của mình bao nhiêu service, config ra sao, kết nối với nhau thế nào trong một file YAML duy nhất. Xong rồi chạy docker compose up là mọi thứ bật lên. Muốn tắt thì docker compose down. Vậy thôi.

Theo kinh nghiệm của mình, Compose đặc biệt có giá trị ở môi trường dev local. Thay vì onboarding member mới phải ngồi setup từng thứ một, bạn chỉ cần commit file docker-compose.yml vào repo, người ta clone về chạy một lệnh là có ngay môi trường y chang của bạn. Tiết kiệm cả buổi setup.

Lưu ý nhỏ: Từ Docker Compose V2, lệnh là docker compose (có space) thay vì docker-compose (có gạch ngang). Nếu bạn đang dùng Docker Desktop bản mới thì V2 đã được bundle sẵn rồi, không cần cài thêm gì.

Anatomy của một file docker-compose.yml

Hãy xem qua cấu trúc cơ bản trước:

YAML
1services:
2 app:
3 build: .
4 ports:
5 - "3000:3000"
6 environment:
7 - NODE_ENV=development
8 volumes:
9 - .:/app
10 depends_on:
11 - postgres
12
13 postgres:
14 image: postgres:16-alpine
15 ports:
16 - "5432:5432"
17 environment:
18 POSTGRES_DB: myapp
19 POSTGRES_USER: user
20 POSTGRES_PASSWORD: secret
21 volumes:
22 - postgres_data:/var/lib/postgresql/data
23
24volumes:
25 postgres_data:

Các key quan trọng bạn cần nắm:

KeyTác dụng
`build`Chỉ đường dẫn tới Dockerfile để build image
`image`Dùng image có sẵn từ registry
`ports`Map port `host:container`
`environment`Set env var cho container
`volumes`Mount volume hoặc bind mount
`depends_on`Khai báo thứ tự khởi động
`networks`Gắn service vào network cụ thể

Ví dụ thực tế: Next.js + PostgreSQL + Redis

Mình sẽ lấy một stack khá phổ biến để demo Next.js app, PostgreSQL làm primary database, Redis làm cache/session store. Đây là kiểu setup mình hay dùng cho các side project:

YAML
1services:
2 web:
3 build:
4 context: .
5 dockerfile: Dockerfile
6 ports:
7 - "3000:3000"
8 environment:
9 DATABASE_URL: postgresql://user:secret@postgres:5432/myapp
10 REDIS_URL: redis://redis:6379
11 NODE_ENV: development
12 volumes:
13 - .:/app
14 - /app/node_modules
15 - /app/.next
16 depends_on:
17 postgres:
18 condition: service_healthy
19 redis:
20 condition: service_started
21 networks:
22 - app_network
23
24 postgres:
25 image: postgres:16-alpine
26 environment:
27 POSTGRES_DB: myapp
28 POSTGRES_USER: user
29 POSTGRES_PASSWORD: secret
30 volumes:
31 - postgres_data:/var/lib/postgresql/data
32 healthcheck:
33 test: ["CMD-SHELL", "pg_isready -U user -d myapp"]
34 interval: 10s
35 timeout: 5s
36 retries: 5
37 networks:
38 - app_network
39
40 redis:
41 image: redis:7-alpine
42 volumes:
43 - redis_data:/data
44 networks:
45 - app_network
46
47volumes:
48 postgres_data:
49 redis_data:
50
51networks:
52 app_network:
53 driver: bridge

Mình thấy cái này hay ở chỗ: trong code Next.js, bạn connect tới PostgreSQL bằng hostname postgres và Redis bằng hostname redis đúng tên service trong file Compose. Docker tự lo phần DNS resolution giữa các container trong cùng network. Không cần biết IP, không cần hardcode gì cả.

Environment variables đừng hardcode vào file Compose

Hardcode password vào docker-compose.yml rồi commit lên Git là một trong những sai lầm kinh điển. Cách đúng là dùng .env file:

File .env:

ENV
1POSTGRES_PASSWORD=supersecretpassword
2POSTGRES_USER=myuser
3POSTGRES_DB=production_db
4NEXT_PUBLIC_API_URL=https://api.example.com

Docker Compose tự động đọc file .env ở cùng thư mục. Bạn dùng variable substitution trong file Compose như sau:

YAML
1services:
2 postgres:
3 image: postgres:16-alpine
4 environment:
5 POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
6 POSTGRES_USER: ${POSTGRES_USER}
7 POSTGRES_DB: ${POSTGRES_DB}

Hoặc nếu muốn load cả file env vào container, dùng env_file:

YAML
1services:
2 web:
3 build: .
4 env_file:
5 - .env
6 - .env.local # override cho local dev

Anh em lưu ý: nhớ add .env vào .gitignore, chỉ commit file .env.example với các key placeholder để người khác biết cần set những gì.

depends_on + healthcheck: cái combo không thể thiếu

Đây là phần nhiều người bỏ qua rồi sau đó hỏi "tại sao app mình bị lỗi connect database lúc mới start?"

depends_on mặc định 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. PostgreSQL cần vài giây để init xong trước khi accept connections. Nếu app của bạn connect ngay lập tức, nó sẽ fail.

Giải pháp là kết hợp healthcheck với condition: service_healthy:

YAML
1services:
2 postgres:
3 image: postgres:16-alpine
4 healthcheck:
5 test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
6 interval: 10s # check mỗi 10 giây
7 timeout: 5s # timeout sau 5 giây
8 retries: 5 # thử lại tối đa 5 lần
9 start_period: 30s # cho postgres 30s để init trước khi bắt đầu check
10
11 web:
12 depends_on:
13 postgres:
14 condition: service_healthy # đợi postgres healthy hẳn mới start

Với config này, Docker sẽ chạy healthcheck liên tục, và chỉ start web khi postgres báo healthy. Mình đã bị bug này mấy lần trước khi hiểu ra giờ project nào mình cũng setup healthcheck từ đầu.

Named volumes dữ liệu không mất khi restart

Bạn để ý trong file Compose ở trên có block volumes: ở level cao nhất:

YAML
1volumes:
2 postgres_data:
3 redis_data:

Đây là named volumes. Khác với bind mount (mount thư mục từ host vào container), named volumes được Docker quản lý hoàn toàn. Data được lưu trong Docker's storage area trên host, tồn tại độc lập với container lifecycle.

Khi bạn chạy docker compose down, container bị xóa nhưng named volumes vẫn còn. Data của PostgreSQL vẫn nguyên vẹn. Chỉ khi bạn chạy docker compose down -v thì volumes mới bị xóa theo.

LệnhContainersVolumes
`docker compose down`Bị xóaGiữ nguyên
`docker compose down -v`Bị xóaBị xóa
`docker compose stop`Dừng lạiGiữ nguyên

Các lệnh CLI cần nhớ

Phần lớn workflow hàng ngày của bạn với Compose chỉ xoay quanh vài lệnh sau:

Terminal
1# Start toàn bộ stack ở background
2docker compose up -d
3
4# Start và rebuild image (dùng khi đổi Dockerfile)
5docker compose up -d --build
6
7# Xem logs của tất cả service
8docker compose logs -f
9
10# Xem logs của một service cụ thể
11docker compose logs -f web
12
13# Xem trạng thái các container
14docker compose ps
15
16# Chạy lệnh trong container đang chạy
17docker compose exec web sh
18docker compose exec postgres psql -U user -d myapp
19
20# Dừng và xóa containers (giữ volumes)
21docker compose down
22
23# Dừng và xóa cả volumes
24docker compose down -v
25
26# Restart một service cụ thể
27docker compose restart web
28
29# Scale service (chạy nhiều instance)
30docker compose up -d --scale web=3

Mình hay dùng docker compose logs -f web nhất cực kỳ tiện để debug khi app có vấn đề, không cần phải docker ps rồi copy container ID rồi docker logs nữa.

Networking tự động magic mà bạn nên hiểu

Khi bạn không khai báo networks gì cả, Docker Compose tự tạo một default network cho project và gắn tất cả services vào đó. Tên network được đặt theo pattern {tên_thư_mục}_default.

Trong cùng network này, các container giao tiếp với nhau qua tên service như hostname. Nên trong code Next.js, connection string sẽ là:

TEXT
1postgresql://user:secret@postgres:5432/myapp
2 ^^^^^^^^
3 tên service trong Compose

Nếu bạn có nhiều project Compose chạy cùng lúc và cần chúng giao tiếp với nhau, bạn phải define network explicitly và dùng external: true để share network. Nhưng đó là câu chuyện phức tạp hơn, để bài khác mình nói.

Một điều hay nữa: port bạn khai báo trong ports: là để host machine (máy bạn) access vào container. Còn các container trong cùng network nói chuyện với nhau qua internal port của container, không cần expose ra host. Ví dụ Redis dùng port 6379 nội bộ, bạn không cần ports: - "6379:6379" nếu chỉ có app trong cùng Compose stack dùng Redis thôi.

Một vài tip thực chiến

Sau mấy năm dùng Compose, mình rút ra vài thứ:

Override file cho từng môi trường: Bạn có thể có docker-compose.yml là base config, rồi docker-compose.override.yml cho dev local (tự động được merge), và docker-compose.prod.yml cho production. Chạy với -f flag để chỉ định:

Terminal
1docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d

Watch mode (Compose Watch): Docker Compose gần đây có thêm tính năng watch tự động sync file thay đổi vào container mà không cần restart. Nếu bạn đang dùng bind mount cho dev thì chưa cần, nhưng với production-like setup thì rất tiện.

Resource limits: Trong môi trường shared hoặc CI, nên set memory/CPU limits để tránh một service ăn hết resource:

YAML
1services:
2 web:
3 deploy:
4 resources:
5 limits:
6 memory: 512m
7 cpus: '0.5'

Docker Compose không phải là silver bullet cho production deployment Kubernetes hay Docker Swarm mới là câu chuyện đó. Nhưng cho dev environment và các dự án nhỏ đến vừa, Compose là tool không thể thiếu. Một file YAML, một lệnh, toàn bộ stack chạy ngon đơn giản vậy thôi mà hiệu quả cực kỳ.

NN

Nguyễn Nhật Long

@nguyennhatlong1303

Nguyễn Nhật Long is a Senior Frontend Engineer and Frontend Team Leader with 7 years of experience building real-time fintech platforms. Specializing in React, Next.js, TypeScript, and React Native, shipping 10+ products across Web, Mobile, Telegram Mini-Apps, and Web3.

Thấy hay? Chia sẻ cho bạn bè!

Docker Compose: Gom hết stack vào một file, deploy bằng một lệnh — Stacklog