</>Học Dev
Bài học

Tuần 1 - Ngày 4: Docker Compose

Tuần 1 – Ngày 4

Tuần 1 - Ngày 4: Docker Compose

Mục tiêu học tập

  • Hiểu vấn đề Docker Compose giải quyết khi chạy multi-container app
  • Nắm cú pháp docker-compose.yml (compose spec)
  • Hiểu networking mặc định giữa các services
  • Dùng volumes để persist data
  • Quản lý environment variables và secrets trong Compose
  • Xây dựng full example app: web + Postgres + Redis

1. Tại sao cần Docker Compose?

Không có Compose, chạy app 3 containers cần 3 lệnh dài:

# Không có Compose — lệnh phức tạp, dễ sai
docker network create myapp-net

docker run -d \
  --name postgres \
  --network myapp-net \
  -e POSTGRES_PASSWORD=secret \
  -v pgdata:/var/lib/postgresql/data \
  postgres:16

docker run -d \
  --name redis \
  --network myapp-net \
  redis:7-alpine

docker run -d \
  --name web \
  --network myapp-net \
  -p 3000:3000 \
  -e DATABASE_URL=postgres://postgres:secret@postgres:5432/mydb \
  -e REDIS_URL=redis://redis:6379 \
  myapp:latest

Với Compose, tất cả trong một file:

docker compose up -d

2. Cú pháp docker-compose.yml

Cấu trúc tổng quan

# docker-compose.yml

services:          # Các containers
  web:             # Tên service
    ...
  db:
    ...

volumes:           # Named volumes
  pgdata:

networks:          # Custom networks (tuỳ chọn)
  backend:

Lưu ý phiên bản: Compose V2 (tích hợp sẵn Docker Desktop 2022+) dùng docker compose (không có dấu gạch nối). version: field ở đầu file đã deprecated — bỏ qua.

Service definition chi tiết

services:
  web:
    # 1. Image hoặc build
    image: nginx:1.27              # dùng image có sẵn
    # hoặc
    build:
      context: .                   # build context
      dockerfile: Dockerfile       # Dockerfile path
      target: runtime              # multi-stage target

    # 2. Đặt tên container
    container_name: my-web

    # 3. Port mapping
    ports:
      - "8080:80"                  # host:container
      - "443:443"

    # 4. Environment variables
    environment:
      NODE_ENV: production
      PORT: "3000"
    # hoặc từ file
    env_file:
      - .env
      - .env.local

    # 5. Volumes
    volumes:
      - ./src:/app/src             # bind mount (dev — sync code real-time)
      - pgdata:/var/lib/postgresql # named volume (persist data)
      - /tmp:/tmp:ro               # read-only mount

    # 6. Networks
    networks:
      - frontend
      - backend

    # 7. Phụ thuộc vào service khác
    depends_on:
      db:
        condition: service_healthy  # đợi healthcheck pass

    # 8. Healthcheck
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 40s

    # 9. Restart policy
    restart: unless-stopped       # always | on-failure | unless-stopped | no

    # 10. Resource limits
    deploy:
      resources:
        limits:
          cpus: '0.5'
          memory: 512M

3. Networking trong Compose

Default network

Compose tự tạo một bridge network và attach tất cả services vào đó. Các services giao tiếp với nhau qua tên service (DNS resolution).

myapp_defaultnetworkwebdbredis:3000:5432:6379service"web"cóthgi:http://db:5432http://redis:6379
# web service có thể connect đến db bằng hostname "db"
environment:
  DATABASE_URL: "postgres://user:pass@db:5432/mydb"
  #                                   ^^
  #                             Tên service trong Compose

Custom networks (phân tách frontend/backend)

services:
  nginx:
    networks: [frontend]

  web:
    networks: [frontend, backend]

  db:
    networks: [backend]           # Không truy cập được từ nginx

networks:
  frontend:
  backend:
    internal: true                # Không có internet access

4. Volumes — Persist data

services:
  db:
    image: postgres:16
    volumes:
      - pgdata:/var/lib/postgresql/data    # Named volume — persist qua restart/recreate

  web:
    volumes:
      - ./src:/app/src                     # Bind mount — sync code từ host (dev)
      - uploads:/app/uploads               # Named volume cho user uploads

volumes:
  pgdata:        # Docker quản lý, lưu trong /var/lib/docker/volumes/
  uploads:

Phân biệt bind mount vs named volume:

Tiêu chíBind mountNamed volume
PathĐường dẫn cụ thể trên hostDocker quản lý
Dùng choDev (sync source code)Production data (DB, uploads)
Dễ backupDễ (biết path)Cần docker volume commands
Hiệu năngHost filesystemTốt hơn trên Docker Desktop/Mac

5. Full example: Web App + Postgres + Redis

Cấu trúc dự án

myproject/docker-compose.ymldocker-compose.override.yml#devoverrides.env#localsecrets(khôngcommit).env.example#template(commit)Dockerfilesrc/app.jsnginx/nginx.conf

.env.example (commit vào git)

# .env.example — copy thành .env và điền giá trị thật
POSTGRES_USER=myapp
POSTGRES_PASSWORD=changeme
POSTGRES_DB=myapp_db
REDIS_PASSWORD=changeme
APP_SECRET_KEY=changeme

docker-compose.yml (production-like)

services:#PostgreSQLdb:image:postgres:16-alpinecontainer_name:myapp_dbrestart:unless-stoppedenvironment:POSTGRES_USER:${POSTGRES_USER}POSTGRES_PASSWORD:${POSTGRES_PASSWORD}POSTGRES_DB:${POSTGRES_DB}volumes:-pgdata:/var/lib/postgresql/datahealthcheck:test:["CMD-SHELL","pg_isready-U${POSTGRES_USER}-d${POSTGRES_DB}"]interval:10stimeout:5sretries:5networks:-backend#Redisredis:image:redis:7-alpinecontainer_name:myapp_redisrestart:unless-stoppedcommand:redis-server--requirepass${REDIS_PASSWORD}volumes:-redisdata:/datahealthcheck:test:["CMD","redis-cli","-a","${REDIS_PASSWORD}","ping"]interval:10stimeout:5sretries:5networks:-backend#WebAppweb:build:context:.dockerfile:Dockerfiletarget:runtimecontainer_name:myapp_webrestart:unless-stoppedenvironment:NODE_ENV:productionDATABASE_URL:"postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}"REDIS_URL:"redis://:${REDIS_PASSWORD}@redis:6379"SECRET_KEY:${APP_SECRET_KEY}depends_on:db:condition:service_healthyredis:condition:service_healthynetworks:-frontend-backend#Nginxreverseproxynginx:image:nginx:1.27-alpinecontainer_name:myapp_nginxrestart:unless-stoppedports:-"80:80"volumes:-./nginx/nginx.conf:/etc/nginx/conf.d/default.conf:rodepends_on:-webnetworks:-frontendvolumes:pgdata:redisdata:networks:frontend:backend:internal:true

docker-compose.override.yml (dev — không commit)

# Override cho môi trường development
# Compose tự động merge file này với docker-compose.yml
services:
  web:
    build:
      target: builder          # dùng stage builder (có devDependencies)
    volumes:
      - ./src:/app/src            # sync code real-time (bind mount)
    environment:
      NODE_ENV: development
      DEBUG: "true"
    command: ["npm", "run", "dev"]  # hot reload

  db:
    ports:
      - "5432:5432"            # expose DB ra host để dùng TablePlus/DBeaver

  redis:
    ports:
      - "6379:6379"

6. Các lệnh Compose thường dùng

# Khởi động tất cả services (detached)
docker compose up -d

# Chỉ khởi động một số services
docker compose up -d db redis

# Build lại images trước khi up
docker compose up -d --build

# Xem logs
docker compose logs -f
docker compose logs -f web       # chỉ service web

# Xem status
docker compose ps

# Exec vào service
docker compose exec web bash
docker compose exec db psql -U myapp -d myapp_db

# Dừng (giữ containers)
docker compose stop

# Dừng và xoá containers (giữ volumes)
docker compose down

# Xoá cả volumes (XOÁ DATA)
docker compose down -v

# Restart một service sau khi sửa code
docker compose restart web

# Scale một service
docker compose up -d --scale web=3

# Chạy một-lần (không restart)
docker compose run --rm web npm run migrate

7. Environment variables — thứ tự ưu tiên

Cao nhất → Thấp nhất

1. docker compose run -e VAR=val     (CLI flag)
2. Shell environment của user        (export VAR=val trước khi chạy)
3. docker-compose.yml environment:   (inline trong compose file)
4. .env file                         (trong cùng thư mục)
5. Dockerfile ENV instruction        (thấp nhất)
# Debug: xem tất cả env vars của service
docker compose config           # in ra compose config đã interpolate
docker compose exec web env     # in env vars trong container

Câu hỏi ôn tập

  1. Hai services trong cùng Compose file giao tiếp với nhau bằng gì (không phải IP)?

    Xem đáp án

    Bằng tên service làm hostname. Compose tự tạo một bridge network và register DNS cho mỗi service. Service web có thể connect đến service db bằng hostname db (port 5432), hoặc redis bằng hostname redis — không cần biết IP động.

  2. Sự khác biệt giữa bind mount và named volume là gì?

    Xem đáp án

    Bind mount map một đường dẫn cụ thể trên host vào container — phù hợp cho development (sync source code real-time). Named volume do Docker quản lý ở /var/lib/docker/volumes/ — phù hợp cho production data (DB, uploads) vì persist qua docker compose down và có hiệu năng tốt hơn trên Docker Desktop/Mac.

    Bind mount phụ thuộc vào cấu trúc thư mục host; named volume portable hơn.

  3. Lệnh nào để dừng và xoá containers nhưng giữ lại data volumes?

    Xem đáp án

    docker compose down (không có flag -v). Lệnh này dừng và xoá containers, networks được tạo bởi Compose, nhưng giữ lại named volumes. Thêm -v (docker compose down -v) mới xoá volumes — cẩn thận vì xoá cả data database.

  4. docker compose up -d --build khác gì docker compose up -d?

    Xem đáp án

    --build buộc Compose rebuild image từ Dockerfile trước khi up, dù image đã tồn tại. Nếu không có --build, Compose dùng image đã cached (có thể là phiên bản cũ nếu code đã thay đổi). Dùng --build sau khi sửa Dockerfile hoặc source code để đảm bảo container chạy code mới nhất.

  5. Tại sao .env không nên commit vào git nhưng .env.example thì nên?

    Xem đáp án

    .env chứa giá trị thật của secrets (passwords, API keys) — commit vào git nghĩa là lịch sử git lưu secrets vĩnh viễn, ngay cả khi xoá file sau này. .env.example chứa key template với giá trị placeholder (không có secret thật), giúp developer mới biết cần set những biến gì mà không lộ thông tin nhạy cảm.

Bài tập thực hành

# Tạo thư mục dự án
mkdir compose-demo && cd compose-demo

# Tạo file .env
cat > .env << 'EOF'
POSTGRES_USER=demo
POSTGRES_PASSWORD=demopass
POSTGRES_DB=demodb
EOF

# Tạo docker-compose.yml
cat > docker-compose.yml << 'EOF'
services:
  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_DB: ${POSTGRES_DB}
    volumes:
      - pgdata:/var/lib/postgresql/data
    ports:
      - "5432:5432"
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER}"]
      interval: 5s
      retries: 5

  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"

volumes:
  pgdata:
EOF

# Khởi động
docker compose up -d

# Kiểm tra status
docker compose ps

# Kết nối vào Postgres
docker compose exec db psql -U demo -d demodb -c "\l"

# Xem logs
docker compose logs db

# Dọn dẹp
docker compose down -v   # -v xoá cả volume

Tài liệu tham khảo chính thức


Tiếp theo: Ngày 5 — ECR và Deploy lên ECS Fargate