jvinhit//lab

Search posts

Type to search across journal entries.

navigate open esc close

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), volumesnetworks 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.yaml 3 services up network: appnet (auto-created) web build: . db postgres redis cache db redis named volume :8080 → host
One compose.yaml → many services on an auto-created network; they reach each other by service name, db persists to a named volume, web publishes a host port

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 legacy docker-compose (v1 hyphen) binary unless you’re maintaining an old CI image {Dùng docker 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-level version: key — Docker ignores it and may warn {File Compose hiện đại không cần key version: — 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:5432redis: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

down without -v keeps named volumes; down -v wipes them {down không -v giữ volume; down -v xó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}:

ApproachWhen {Khi nào}
image: postgres:16-alpinePull 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:devBuild 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 --buildup 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ó healthcheckdepends_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_on is not “wait until ready” — only start order. Use healthchecks in Part 6 or retry logic in the app {depends_on khô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 --build after Dockerfile changes — you’ll run an old image layer cache surprise {Quên up --build sau khi sửa Dockerfile — bạn chạy image cũ từ cache}.
  • Edited compose.yaml but didn’t recreatedocker compose up -d reconciles many changes; when in doubt: docker compose up -d --force-recreate {Sửa compose.yaml nhưng không tạo lạidocker compose up -d đồng bộ nhiều thay đổi; không chắc: docker compose up -d --force-recreate}.
  • docker-compose vs docker compose — install the Compose V2 plugin; hyphenated v1 is legacy {docker-compose vs docker compose — cài plugin Compose V2; bản v1 có gạch ngang là legacy}.
  • down leaves volumes; down -v wipes them — production data loss happens here {down giữ volume; down -v xóa — mất dữ liệu production hay gặp ở đây}.
  • localhost in app config — inside web, localhost is the web container, not db. Use service hostnames {localhost trong config app — trong web, localhost là chính container web, không phải db. 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.yamlweb (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: secret

3. 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 running

4. 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 name

5. 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 gone

Stretch {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 → fresh

Key takeaways {Điểm chính}

  • Compose = one compose.yaml + docker compose up instead of many docker run commands and manual networks {Compose = một compose.yaml + docker compose up thay cho nhiều lệnh docker run và mạng tạo tay}.
  • services are containers; Compose creates a project network with DNS by service name {services là container; Compose tạo mạng project với DNS theo tên service}.
  • Use image for third-party images, build for your Dockerfile; rebuild with up --build {Dùng image cho image bên thứ ba, build cho Dockerfile của bạn; rebuild bằng up --build}.
  • depends_on orders starts, not readiness — healthchecks come in Part 6 {depends_on sắp thứ tự khởi động, không đợi sẵn sàng — healthcheck ở Phần 6}.
  • ports, volumes, environment/env_file mirror -p, -v, -e from Parts 3–4 {ports, volumes, environment/env_file tương ứng -p, -v, -e từ Phần 3–4}.
  • docker compose down -v deletes named volumes; plain down keeps them {docker compose down -v xóa named volume; down thườ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}.