Docker for Developers · Part 5 — Docker Compose Fundamentals
Run multi-container apps with one file and one command. The compose.yaml model: services, build vs image, ports, volumes, networks, depends_on, and the everyday compose workflow.
This is Part 5 of a 10-part series that takes you from “I’ve heard of Docker” to confidently building, running, and debugging containerized apps — Docker, Docker Compose, and Kubernetes {Đây là Phần 5 của series 10 bài đưa bạn từ “mới nghe nói về Docker” đến tự tin build, chạy và debug ứng dụng container — Docker, Docker Compose và Kubernetes}. You’ve already built images (Part 2), persisted data (Part 3), and wired containers on a user-defined network (Part 4) {Bạn đã build image (Phần 2), lưu dữ liệu (Phần 3), và nối container trên user-defined network (Phần 4)}. Every part ends with exercises; do them, don’t just read {Mỗi phần kết thúc bằng bài tập; hãy làm, đừng chỉ đọc}.
Running a realistic stack — web + database + cache — by hand means a pile of docker run flags, a docker network create, named volumes, and env vars you have to remember every time {Chạy stack thực tế — web + database + cache — bằng tay nghĩa là một đống flag docker run, docker network create, named volume và biến môi trường bạn phải nhớ mỗi lần}. Docker Compose replaces that ceremony with one declarative file (compose.yaml) and one command: docker compose up {Docker Compose thay nghi lễ đó bằng một file khai báo (compose.yaml) và một lệnh: docker compose up}.
The compose.yaml model {Mô hình compose.yaml}
A Compose file describes a project: a set of services (containers) that share a default network and optional named volumes {File Compose mô tả một project: tập service (container) dùng chung một mạng mặc định và volume đặt tên tùy chọn}. The modern default filename is compose.yaml (also accepted: docker-compose.yaml) {Tên file mặc định hiện đại là compose.yaml (cũng chấp nhận docker-compose.yaml)}.
Top level: services (each key = one container), optional volumes and networks {Cấp cao: services (mỗi key = một container), volumes và networks tùy chọn}. Per-service keys mirror docker run: image / build, ports, volumes, environment / env_file, depends_on (order only), command {Key trong service tương ứng docker run: image / build, ports, volumes, environment / env_file, depends_on (chỉ thứ tự), command}.
Compose automatically creates a user-defined bridge for the project — the same kind of network you created manually in Part 4 {Compose tự động tạo user-defined bridge cho project — cùng loại mạng bạn tạo tay ở Phần 4}. Containers reach each other by service name (db, not localhost) {Container gọi nhau bằng tên service (db, không phải localhost)}.
Use
docker compose(v2 plugin, space) — not the legacydocker-compose(v1 hyphen) binary unless you’re maintaining an old CI image {Dùngdocker compose(plugin v2, có dấu cách) — không phải binary cũdocker-compose(v1 có gạch ngang) trừ khi bạn bảo trì CI cũ}. Modern Compose files do not need a top-levelversion:key — Docker ignores it and may warn {File Compose hiện đại không cần keyversion:— Docker bỏ qua và có thể cảnh báo}.
A first compose.yaml {compose.yaml đầu tiên}
Three-service stack — web (build: . or image), Postgres + named volume, Redis (internal only) {Stack ba service — web (build: . hoặc image), Postgres + named volume, Redis (chỉ nội bộ)}:
services:
web:
build: . # or image: nginx:alpine for a quick smoke test
ports:
- "8080:3000"
environment:
DATABASE_URL: postgres://postgres:secret@db:5432/app
REDIS_URL: redis://redis:6379
depends_on: [db, redis]
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: secret
POSTGRES_DB: app
volumes:
- pgdata:/var/lib/postgresql/data
redis:
image: redis:7-alpine
volumes:
pgdata:
docker compose config && docker compose up -d
Connect to db:5432 and redis:6379 by service name, not localhost {Kết nối db:5432 và redis:6379 bằng tên service, không phải localhost}.
The everyday workflow {Quy trình hằng ngày}
These commands cover most daily Compose work {Các lệnh này bao phủ phần lớn Compose hằng ngày}:
docker compose up # create + start (foreground)
docker compose up -d # detached
docker compose up --build # rebuild images before start (after Dockerfile edits)
docker compose down # stop + remove containers & project network
docker compose down -v # also remove named volumes declared in the file
docker compose ps # running services in this project
docker compose logs web # logs for one service
docker compose logs -f # follow all services
docker compose exec db sh # shell inside a running service (use psql, redis-cli, etc.)
docker compose build # build images without starting
docker compose pull # pull external images only
downwithout-vkeeps named volumes;down -vwipes them {downkhông-vgiữ volume;down -vxóa volume}.
build vs image {build vs image}
Every service needs either a runnable image or instructions to build one {Mỗi service cần hoặc image chạy được hoặc hướng dẫn build}:
| Approach | When {Khi nào} |
|---|---|
image: postgres:16-alpine | Pull a published image — databases, brokers, official tools {Kéo image công bố — database, message broker, tool chính thức}. |
build: . (or build: ./api) | Build from a Dockerfile in that context — your application code {Build từ Dockerfile trong context đó — code ứng dụng của bạn}. |
build + image: myapp:dev | Build and tag with a fixed name so rebuilds and docker image ls stay predictable {Build và gắn tag cố định để rebuild và docker image ls dễ theo dõi}. |
services:
api:
build: { context: ., dockerfile: Dockerfile }
image: myshop-api:dev
db:
image: postgres:16-alpine
After Dockerfile edits, run docker compose up --build — plain up may reuse a cached image {Sau khi sửa Dockerfile, chạy docker compose up --build — up thường có thể dùng lại image cache}.
Service discovery & depends_on {Service discovery & depends_on}
On the project network, Docker’s embedded DNS resolves service name → container IP — same rule as container names on a user-defined bridge in Part 4 {Trên mạng project, DNS nhúng phân giải tên service → IP container — cùng quy tắc tên container trên user-defined bridge ở Phần 4}:
web container db container
┌────────────────────┐ ┌────────────────────┐
│ DATABASE_URL= │ TCP :5432 │ POSTGRES listening │
│ ...@db:5432/app │ ─────────────► │ hostname "db" │
└────────────────────┘ └────────────────────┘
▲ ▲
└──────── Compose DNS: "db" ──────────┘
depends_on tells Compose which containers to start first — it does not wait until Postgres accepts connections or Redis is ready {depends_on báo Compose container nào start trước — nó không đợi Postgres nhận kết nối hay Redis sẵn sàng}:
services:
web:
depends_on:
- db
- redis
If your web process crashes on “connection refused” at boot, that’s normal without readiness checks — Part 6 covers healthcheck and depends_on with condition: service_healthy {Nếu web crash vì “connection refused” lúc boot, đó là bình thường khi chưa có readiness — Phần 6 sẽ có healthcheck và depends_on với condition: service_healthy}.
Ports, volumes, env in Compose {Ports, volume, env trong Compose}
YAML equivalents of flags you already know {Tương đương YAML của flag bạn đã biết}:
services:
web:
ports: ["8080:3000"] # -p (browser on host)
expose: ["3000"] # other services only, not host
volumes: ["./src:/app/src"] # bind mount (Part 3)
environment:
NODE_ENV: development
API_KEY: ${API_KEY} # from shell or project .env
env_file: [.env.local]
db:
volumes: [pgdata:/var/lib/postgresql/data]
volumes:
pgdata: # declare named volumes here
Publish ports only for services the host must reach; DBs usually stay internal (db:5432 on the project network) {Chỉ publish ports cho service host cần gọi; DB thường nội bộ (db:5432 trên mạng project)}. Compose reads .env for ${VAR} substitution — don’t commit secrets (Part 3) {Compose đọc .env cho ${VAR} — không commit secret (Phần 3)}.
Common pitfalls {Các bẫy thường gặp}
depends_onis not “wait until ready” — only start order. Use healthchecks in Part 6 or retry logic in the app {depends_onkhông phải “đợi sẵn sàng” — chỉ thứ tự start. Dùng healthcheck ở Phần 6 hoặc retry trong app}.- Forgot
up --buildafter Dockerfile changes — you’ll run an old image layer cache surprise {Quênup --buildsau khi sửa Dockerfile — bạn chạy image cũ từ cache}. - Edited
compose.yamlbut didn’t recreate —docker compose up -dreconciles many changes; when in doubt:docker compose up -d --force-recreate{Sửacompose.yamlnhưng không tạo lại —docker compose up -dđồng bộ nhiều thay đổi; không chắc:docker compose up -d --force-recreate}. docker-composevsdocker compose— install the Compose V2 plugin; hyphenated v1 is legacy {docker-composevsdocker compose— cài plugin Compose V2; bản v1 có gạch ngang là legacy}.downleaves volumes;down -vwipes them — production data loss happens here {downgiữ volume;down -vxóa — mất dữ liệu production hay gặp ở đây}.localhostin app config — insideweb,localhostis the web container, notdb. Use service hostnames {localhosttrong config app — trongweb,localhostlà chính container web, không phảidb. Dùng hostname service}.- Publishing every port — you rarely need
- "5432:5432"unless a host GUI client needs it {Publish mọi cổng — hiếm khi cần- "5432:5432"trừ khi client GUI trên host cần}.
Cheat sheet {Bảng tra nhanh}
# lifecycle
docker compose up -d
docker compose up -d --build
docker compose down
docker compose down -v
# observe & debug
docker compose ps
docker compose logs -f web
docker compose exec db psql -U postgres -d app
docker compose config
# one-off commands on the project network
docker compose run --rm postgres:16-alpine psql -h db -U postgres -c 'SELECT 1'
Bài tập / Exercises
Create a folder compose-lab/, add the files below, and run everything from that directory {Tạo thư mục compose-lab/, thêm file dưới đây, chạy mọi thứ từ thư mục đó}.
1. Write a compose.yaml with web (nginx:alpine, port 8080:80), db (postgres:16-alpine, password secret, named volume pgdata), and redis (redis:7-alpine, no host ports) {Viết compose.yaml có web (nginx:alpine, cổng 8080:80), db (postgres:16-alpine, mật khẩu secret, named volume pgdata), và redis (redis:7-alpine, không publish cổng host)}.
Solution {Lời giải}
# compose-lab/compose.yaml
services:
web:
image: nginx:alpine
ports:
- "8080:80"
depends_on:
- db
- redis
db:
image: postgres:16-alpine
environment:
POSTGRES_PASSWORD: secret
volumes:
- pgdata:/var/lib/postgresql/data
redis:
image: redis:7-alpine
volumes:
pgdata:2. Bring the stack up detached, list services, open http://localhost:8080, and prove db resolves on the project network with a one-off psql client {Bật stack ở chế độ detached, liệt kê service, mở http://localhost:8080, và chứng minh db phân giải được trên mạng project bằng client psql một lần}.
Solution {Lời giải}
cd compose-lab
docker compose up -d
docker compose ps
curl -sI http://localhost:8080 | head -n 1
docker compose run --rm postgres:16-alpine \
psql -h db -U postgres -c "SELECT current_database();"
# password: secret3. Follow only the db service logs live, then stop following with Ctrl+C {Theo log live chỉ service db, rồi Ctrl+C dừng theo dõi}.
Solution {Lời giải}
docker compose logs -f db
# Ctrl+C stops following; containers keep running4. exec into web and verify the hostname redis resolves (e.g. with wget or getent) {exec vào web và kiểm tra hostname redis phân giải được (vd bằng wget hoặc getent)}.
Solution {Lời giải}
docker compose exec web getent hosts redis
# prints an IP — proof DNS resolves the service name5. Tear down without -v, bring the stack up again, and confirm Postgres data still exists; then down -v and confirm the named volume is gone {Tắt không -v, bật lại stack, xác nhận dữ liệu Postgres còn; rồi down -v và xác nhận named volume biến mất}.
Solution {Lời giải}
docker compose exec db psql -U postgres -c \
"CREATE TABLE markers (id int); INSERT INTO markers VALUES (1);"
docker compose down && docker compose up -d
docker compose exec db psql -U postgres -c "SELECT * FROM markers;" # still there
docker compose down -v
docker volume ls | grep compose-lab # pgdata goneStretch {Nâng cao}: add build: . to web using a minimal Dockerfile from Part 2, run docker compose up --build, and change the Dockerfile — confirm you need --build to see the change {Thêm build: . cho web với Dockerfile tối giản từ Phần 2, chạy docker compose up --build, sửa Dockerfile — xác nhận cần --build mới thấy thay đổi}.
Solution {Lời giải}
# Add build: . + a Dockerfile that writes /usr/share/nginx/html/build.txt
docker compose up -d --build && curl http://localhost:8080/build.txt
# Edit Dockerfile, then up -d alone → stale; up -d --build → freshKey takeaways {Điểm chính}
- Compose = one
compose.yaml+docker compose upinstead of manydocker runcommands and manual networks {Compose = mộtcompose.yaml+docker compose upthay cho nhiều lệnhdocker runvà mạng tạo tay}. servicesare containers; Compose creates a project network with DNS by service name {serviceslà container; Compose tạo mạng project với DNS theo tên service}.- Use
imagefor third-party images,buildfor your Dockerfile; rebuild withup --build{Dùngimagecho image bên thứ ba,buildcho Dockerfile của bạn; rebuild bằngup --build}. depends_onorders starts, not readiness — healthchecks come in Part 6 {depends_onsắp thứ tự khởi động, không đợi sẵn sàng — healthcheck ở Phần 6}.ports,volumes,environment/env_filemirror-p,-v,-efrom Parts 3–4 {ports,volumes,environment/env_filetương ứng-p,-v,-etừ Phần 3–4}.docker compose down -vdeletes named volumes; plaindownkeeps them {docker compose down -vxóa named volume;downthường giữ lại}.
Next up {Tiếp theo}
Part 6 — Compose in Depth: Env, Profiles, Healthchecks & Scaling: make depends_on wait for a healthy database, split dev/prod with profiles, override config with multiple Compose files, and scale stateless services with --scale {Phần 6 — Compose nâng cao: Env, Profiles, Healthchecks & Scaling: cho depends_on đợi database healthy, tách dev/prod bằng profiles, ghi đè cấu hình bằng nhiều file Compose, và scale service stateless với --scale}.