---
id: docker-compose
title: "Docker Compose"
track: docker
trackTitle: "Docker"
order: 7
level: core
status: published
estimatedMinutes: 40
prerequisites:
  - docker-env
next: docker-volume
reviewedAgainst: 2026-10-15
sources:
  - title: "KodeKloud Engineer — 100 Days of DevOps"
    url: https://engineer.kodekloud.com/practice
  - title: "Compose file reference"
    url: https://docs.docker.com/reference/compose-file/
  - title: "Compose version and name"
    url: https://docs.docker.com/reference/compose-file/version-and-name/
  - title: "Docker Hub — postgres"
    url: https://hub.docker.com/_/postgres
---

# Docker Compose

Lệnh đang dùng là `docker compose`, có một khoảng trắng. `docker-compose` là Compose V1, viết bằng Python, đã ngừng phát triển.

## Để làm gì

Khai báo vài container của môi trường dev trong một file, rồi up hoặc down bằng một lệnh. App và database cùng network, gọi nhau bằng tên service. File cũng ghi thứ tự chờ database healthy.

## Gồm những gì

- `compose.yaml` theo Compose Specification.
- Service, image hoặc build, port (`"host:container"`, cùng quy ước `-p`), volume, healthcheck, secret.
- Stack nhiều service trên một máy: app + database, cùng network, gọi bằng tên service.
- `docker compose up`, `ps`, `logs`, `down`. Plugin đi kèm gói `docker-compose-plugin`.

Khóa `version` đầu file còn lại từ Compose V1. Compose V2 bỏ qua khóa này và in cảnh báo obsolete. File trong bài không dùng khóa đó.

## Dùng khi nào

Môi trường local, hoặc lab có vài process phụ thuộc nhau. Cần nhiều node, rollout, hay Pod hỏng được thay tự động thì dùng Kubernetes. Compose chạy trên một máy.

Mật khẩu database trong lab không commit. Mẫu dưới đọc mật khẩu từ file secret nằm ngoài Git. Biến thường của app, và file `.env`, đã tách ở bài trước: `env_file` mới đưa file đó vào process. File `.env` cạnh yaml một mình chỉ để thay `${TÊN}` trong file.

## Kiến thức cốt lõi

Mỗi khóa dưới `services` là một container. `build: .` build Dockerfile ở thư mục hiện tại. `image` dùng image có sẵn.

Các service trong cùng file nối vào một network riêng. Service `web` gọi database bằng tên `db`. Đó là tên service. `localhost` trong container `web` là chính container đó.

`depends_on` kèm `condition: service_healthy` đợi healthcheck của database thành công rồi mới start service `web`. Không có condition thì Compose chỉ đợi container kia start. Database có thể chưa nhận kết nối.

Volume `pgdata` giữ dữ liệu PostgreSQL khi container bị thay. Xóa volume là xóa dữ liệu lab.

Image `postgres` chính thức đọc `POSTGRES_PASSWORD` hoặc `POSTGRES_PASSWORD_FILE`. Dạng file hợp lý hơn: mật khẩu không nằm trong `compose.yaml`. PostgreSQL 17 còn trong cửa sổ hỗ trợ lúc viết bài; bảng phiên bản ở [postgresql.org/support/versioning](https://www.postgresql.org/support/versioning/).

Lab này chưa cho Node nối SQL. Bài cần thấy hai process, một network, một port publish, một volume. Code app nối database làm sau, khi network đã đúng.

`ports: ["3000:3000"]` cùng quy ước `docker run -p`: host trước, container sau. Publish chỉ mở ra máy host. Service khác trong file không cần port đó — chúng gọi tên service trên network Compose tự tạo. Gói `docker-compose-plugin` (cùng lúc cài `docker-ce`) cho lệnh `docker compose`. Không có plugin thì `docker compose` báo unknown command, dù daemon đã chạy.

## Lệnh và code mẫu

Trong thư mục đã có Dockerfile và `server.js` từ bài trước, tạo `compose.yaml`:

```yaml
services:
  web:
    build: .
    ports:
      - "3000:3000"
    environment:
      PORT: "3000"
      DB_HOST: db
    depends_on:
      db:
        condition: service_healthy

  db:
    image: postgres:17-alpine
    environment:
      POSTGRES_USER: app
      POSTGRES_PASSWORD_FILE: /run/secrets/db-password
      POSTGRES_DB: appdb
    secrets:
      - db-password
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app -d appdb"]
      interval: 5s
      timeout: 5s
      retries: 10

volumes:
  pgdata:

secrets:
  db-password:
    file: ./secrets/db-password.txt
```

Tạo secret local và giữ nó ngoài Git:

```bash
mkdir -p secrets
printf 'lab-password-change-me\n' > secrets/db-password.txt
printf '\nsecrets/\n' >> .gitignore
```

Chạy stack:

```bash
docker compose up --detach --build
docker compose ps
curl -sS http://127.0.0.1:3000/healthz
docker compose logs db
docker compose down
```

`docker compose down` xóa container và network của project, giữ volume. `docker compose down --volumes` xóa cả `pgdata`. Chỉ thêm `--volumes` khi cố ý bỏ dữ liệu lab.

## Thực hành

1. Đặt `compose.yaml` cạnh Dockerfile của bài trước.
2. Tạo `secrets/db-password.txt` và dòng `.gitignore`.
3. Chạy `docker compose up --detach --build`.
4. Kiểm tra `/healthz`.
5. Chạy `docker compose exec db psql -U app -d appdb -c 'select current_database();'`.
6. Chạy `docker compose down`, rồi `up` lại. Volume còn thì PostgreSQL không tạo database trống lần nữa.

## Kết quả mong đợi

- `docker compose ps` có `web` và `db` đang chạy, `db` đã healthy.
- `curl` in `ok`.
- `psql` in `appdb`.
- File mật khẩu không xuất hiện trong `git status`.

## Checklist

- [ ] Viết file Compose không có khóa `version`.
- [ ] Gọi service khác bằng tên service trên network Compose.
- [ ] Đọc healthcheck và `condition: service_healthy`.
- [ ] Tách `down` khỏi `down --volumes`.
- [ ] `ports` viết `host:container`, cùng quy ước `docker run -p`.
- [ ] `docker compose` là plugin. Thiếu gói thì lệnh unknown, dù daemon đã chạy.

## Thẻ ôn

### docker-compose-version
kind: default
cue: Compose V2 bỏ qua khóa `version` và cảnh báo obsolete. File bắt đầu bằng `services:`, lệnh là `docker compose`.

Q: Một file Compose cũ vẫn để `version: "3.8"` ở dòng đầu. Bạn đang dùng Compose đi kèm Docker mới. Khóa đó còn quyết định hành vi không, và lệnh nên gõ là `docker compose` hay `docker-compose`?

A: Compose V2 không dùng khóa `version` để chọn cách chạy nữa. Nó bỏ qua khóa đó và in cảnh báo obsolete. File nên bắt đầu bằng `services:`. Lệnh hiện tại là `docker compose`, có một khoảng trắng, vì Compose là plugin của Docker CLI. `docker-compose` có gạch ngang là Compose V1, bản đã ngừng phát triển. Đừng giữ khóa `version` chỉ vì file mẫu cũ có nó.

### docker-compose-localhost
kind: trap
cue: `localhost` trong container `web` là chính container đó. Gọi Postgres bằng tên service, ví dụ `db`.

Q: File Compose có service `web` và service `db` chạy Postgres. Trong `web` bạn đặt `DB_HOST=localhost` vì trên máy dev database cũng ở localhost. Kết nối từ `web` thực ra đi tới đâu?

A: Mỗi container có network namespace riêng. `localhost` bên trong `web` là chính container `web`, không phải máy host và cũng không phải container `db`. Các service khai trong cùng một file Compose được nối vào một network riêng, và Docker DNS trả tên service thành IP của container đó. App phải gọi `db`, đúng tên service, thì mới tới Postgres.

### docker-compose-healthy
kind: distinguish
cue: `depends_on` không có condition chỉ đợi container kia start, chưa đợi database nhận kết nối.

Q: `web` khai `depends_on` trỏ tới database, nhưng không có `condition: service_healthy`. Compose start `web` vào lúc nào, và vì sao app vẫn có thể lỗi connection refused ngay sau khi lên?

A: Không có condition, Compose chỉ đợi container database được tạo và process của nó đã start. Postgres có thể đã start nhưng chưa mở cổng, chưa replay xong, nên chưa nhận kết nối. `web` lên sớm và gọi database lúc đó thì fail. Thêm healthcheck cho database, rồi `condition: service_healthy`, thì `web` chỉ start sau khi healthcheck thành công.

### docker-compose-down
kind: command
cue: `docker compose down` tắt stack và giữ volume. Thêm `--volumes` thì xóa luôn dữ liệu, chỉ dùng khi cố ý bỏ lab.

Q: Cuối ngày bạn muốn tắt stack dev cho đỡ tốn tài nguyên, nhưng sáng mai vẫn cần dữ liệu Postgres đang nằm trong volume `pgdata`. Lệnh nào làm được việc đó, và lệnh nào sẽ xóa mất dữ liệu?

A: `docker compose down` xóa container và network của project, rồi dừng. Volume không nằm trong lệnh đó, nên `pgdata` còn. `docker compose down --volumes` xóa cả volume đã khai trong file, dữ liệu lab mất theo. Chỉ thêm `--volumes` khi bạn cố ý làm sạch để chạy lại từ đầu.

### docker-compose-scope
kind: decide
cue: Compose chạy vài process trên một máy. Nhiều node và tự thay bản hỏng là việc của Kubernetes.

Q: App local đang chạy tốt bằng Compose. Sếp muốn triển khai lên nhiều máy, và khi một bản chết thì có bản khác được tạo lại. Compose có làm được việc đó không?

A: Không. Compose điều khiển container trên một Docker engine, tức một máy, để dựng môi trường local hoặc một lab vài process phụ thuộc nhau. Nó không xếp workload lên nhiều node và không có vòng lặp thay một bản hỏng bằng bản mới. Việc đó thuộc về Kubernetes. Đừng kéo Compose ra production nhiều máy rồi kỳ vọng nó cư xử như một orchestrator.

## Lỗi thường gặp

**Giữ `version: "3.8"` vì mẫu cũ.** Compose V2 coi khóa này là obsolete. Xóa khóa, file bắt đầu bằng `services:`.

**Đặt `DB_HOST=localhost` trong container `web`.** Kết nối đi vào chính `web`. Tên đúng theo file mẫu là `db`.

**Commit `secrets/db-password.txt`.** File này là credential. `.gitignore` phải có hiệu lực trước commit đầu tiên. Đã lỡ commit thì xoay mật khẩu. Xóa file ở commit sau không xóa bản đã push lên remote.

**`docker compose` unknown command trong khi `docker version` có Server.** Thiếu plugin. Cài `docker-compose-plugin` cùng `docker-ce`. Không quay lại binary `docker-compose` V1.

## Nguồn

1. [KodeKloud Engineer — 100 Days of DevOps](https://engineer.kodekloud.com/practice) — đề cương ngày Write a Docker Compose File, Deploy an App on Docker Containers. Trang practice cần đăng nhập. Bài này viết lại lệnh, không chép lab.
2. [Compose file reference](https://docs.docker.com/reference/compose-file/) — cấu trúc file hiện hành.
3. [Version and name](https://docs.docker.com/reference/compose-file/version-and-name/) — khóa `version` chỉ còn để tương thích, Compose cảnh báo obsolete.
4. [Compose history](https://docs.docker.com/compose/intro/history/) — `docker compose` của Compose V2 thay `docker-compose`.
5. [postgres image](https://hub.docker.com/_/postgres) — environment variable, secret file, dữ liệu trong `/var/lib/postgresql/data`.
6. [PostgreSQL versioning policy](https://www.postgresql.org/support/versioning/) — cửa sổ hỗ trợ của bản 17.

## Tiếp theo

[Docker volume](./04-volume.md)
