Docker Deev Dive #8: Docker Compose — Quản lý multi-container

15 phút đọc

Overview

Ở Bài 7, bạn đã chạy Laravel stack bằng tay: tạo network, tạo volume, gõ 3 lệnh docker run dài, nhớ thứ tự start, nhớ flag. Docker Compose giải quyết toàn bộ sự phức tạp đó — gói tất cả vào 1 file YAML, quản lý bằng 1 lệnh duy nhất.

💡 Mental model: Nếu docker run giống như nấu ăn bằng từng bước một (đun nước, thái hành, phi tỏi...), thì Docker Compose giống như một công thức (recipe) — ghi lại tất cả, chỉ cần "nấu" là xong.

Docker Compose = Declarative multi-container orchestrator

  • Declarative: Bạn mô tả trạng thái mong muốn ("tôi cần 3 services, 1 network, 2 volumes"), Compose lo phần thực thi.
  • Multi-container: Quản lý nhiều containers như một đơn vị thống nhất.
  • Orchestrator: Tự tạo network, volume, build image, start containers theo đúng thứ tự.

Key Concepts

Compose file = Bản thiết kế toàn bộ stack

Một file docker-compose.yml định nghĩa:

Khái niệm Ý nghĩa Tương đương lệnh thủ công (Bài 7)
services Các container cần chạy docker run --name ...
volumes Named volumes cho persistent data docker volume create ...
networks Custom networks (thường không cần khai báo — Compose tự tạo) docker network create ...

Compose tự động làm gì?

Khi chạy docker compose up -d, Compose thực hiện tuần tự:

1. Đọc docker-compose.yml
2. Tạo network: <project-name>_default (custom bridge → có DNS)
3. Tạo volumes (nếu chưa có)
4. Build images (nếu có "build:")
5. Pull images (nếu có "image:")
6. Tạo và start containers theo dependency order (depends_on)
7. Kết nối tất cả containers vào network

Cấu trúc docker-compose.yml

Anatomy — Mổ xẻ từng phần

# === 1. SERVICES: Các container cần chạy ===
services:

  # Service name = DNS hostname trong network
  # (tương đương --name trong docker run)
  nginx:
    image: nginx:alpine            # Image từ Docker Hub
    ports:
      - "8000:80"                  # Port mapping (= -p 8000:80)
    volumes:
      - ./src:/var/www/html        # Bind mount (= -v $(pwd)/src:...)
      - ./docker/nginx/default.conf:/etc/nginx/conf.d/default.conf
    depends_on:
      - app                        # Start sau service "app"

  app:
    build: .                       # Build từ Dockerfile trong thư mục hiện tại
    volumes:
      - ./src:/var/www/html
    depends_on:
      - mysql
      - redis

  mysql:
    image: mysql:8.0
    environment:                   # Env vars (= -e KEY=VALUE)
      MYSQL_ROOT_PASSWORD: secret
      MYSQL_DATABASE: laravel
    volumes:
      - mysql_data:/var/lib/mysql  # Named volume

  redis:
    image: redis:7-alpine
    volumes:
      - redis_data:/data

# === 2. VOLUMES: Named volumes ===
volumes:
  mysql_data:                      # Compose tự tạo <project>_mysql_data
  redis_data:

# === 3. NETWORKS (thường KHÔNG cần khai báo) ===
# Compose tự tạo network "<project>_default" (custom bridge)
# Tất cả services tự động kết nối vào network này

Mapping 1:1 với lệnh thủ công Bài 7

docker-compose.yml Lệnh docker tương đương Giải thích
image: nginx:alpine Phần image trong docker run ... nginx:alpine Dùng image có sẵn từ registry
build: . docker build -t <project>-app . Build image từ Dockerfile
ports: ["8000:80"] -p 8000:80 Port mapping host → container
volumes: ["./src:/var/www/html"] -v $(pwd)/src:/var/www/html Bind mount
volumes: ["mysql_data:/var/lib/mysql"] -v mysql_data:/var/lib/mysql Named volume
environment: MYSQL_ROOT_PASSWORD: secret -e MYSQL_ROOT_PASSWORD=secret Environment variable
depends_on: [mysql] Bạn tự nhớ start MySQL trước Dependency order
(tự động) docker network create ... Compose tự tạo custom bridge network

How It Works — Các thuộc tính service chi tiết

image vs build

# Dùng image có sẵn
mysql:
  image: mysql:8.0

# Build từ Dockerfile
app:
  build: .                    # Dockerfile ở thư mục hiện tại

# Build với options nâng cao
app:
  build:
    context: .                # Build context
    dockerfile: docker/Dockerfile.dev  # Dockerfile tùy chỉnh
    args:                     # Build arguments
      PHP_VERSION: "8.2"

ports — Port mapping

nginx:
  ports:
    - "8000:80"              # host:8000 → container:80
    - "443:443"              # HTTPS
    - "127.0.0.1:8000:80"   # Chỉ bind localhost

Nhắc lại Bài 6: chỉ service cần truy cập từ host mới cần ports. Các service giao tiếp nội bộ (app → mysql) không cần.

volumes — Bind mount & Named volume

services:
  app:
    volumes:
      # Bind mount: host path : container path
      - ./src:/var/www/html

      # Named volume: volume name : container path
      - vendor_data:/var/www/html/vendor

      # Read-only bind mount
      - ./config/php.ini:/usr/local/etc/php/php.ini:ro

# Khai báo named volumes ở top-level
volumes:
  vendor_data:

Quy tắc phân biệt:

  • Bắt đầu bằng ./ hoặc /Bind mount
  • Bắt đầu bằng tên (không có /) → Named volume

environment — Biến môi trường

3 cách khai báo:

# Cách 1: Map trực tiếp (phổ biến nhất)
mysql:
  environment:
    MYSQL_ROOT_PASSWORD: secret
    MYSQL_DATABASE: laravel

# Cách 2: Array format
mysql:
  environment:
    - MYSQL_ROOT_PASSWORD=secret
    - MYSQL_DATABASE=laravel

# Cách 3: Dùng .env file (RECOMMENDED cho secrets)
mysql:
  env_file:
    - .env.mysql

depends_on — Thứ tự khởi động

services:
  nginx:
    depends_on:
      - app          # Nginx start SAU app
  app:
    depends_on:
      - mysql        # App start SAU mysql
      - redis
  mysql: ...
  redis: ...

Thứ tự start: mysql → redis → app → nginx

networks — Custom network (nâng cao)

Mặc định Compose tự tạo <project>_default. Nhưng bạn có thể tạo nhiều networks:

services:
  nginx:
    networks:
      - frontend
  app:
    networks:
      - frontend
      - backend
  mysql:
    networks:
      - backend

networks:
  frontend:
  backend:

Pattern network segmentation từ Bài 6 — nginx không thể truy cập mysql trực tiếp.


Thực hành: Viết docker-compose.yml cho Laravel

Cấu trúc project

Dùng lại project từ Bài 7, hoặc tạo mới:

~/docker-lab/laravel-compose/
├── docker/
│   └── nginx/
│       └── default.conf
├── docker-compose.yml          ← File chính
├── Dockerfile
├── .env                        ← Biến môi trường cho Compose
└── src/                        ← Laravel source code

Bước 1: Tạo Dockerfile (giống Bài 7)

FROM php:8.2-fpm-alpine

RUN apk add --no-cache \
    libpng-dev \
    libjpeg-turbo-dev \
    libzip-dev \
    oniguruma-dev

RUN docker-php-ext-configure gd --with-jpeg \
    && docker-php-ext-install \
        pdo_mysql \
        mbstring \
        zip \
        gd \
        bcmath

COPY --from=composer:latest /usr/bin/composer /usr/bin/composer

WORKDIR /var/www/html
RUN chown -R www-data:www-data /var/www/html

EXPOSE 9000
CMD ["php-fpm"]

Bước 2: Tạo Nginx config (giống Bài 7)

server {
    listen 80;
    server_name localhost;
    root /var/www/html/public;
    index index.php index.html;

    access_log /dev/stdout;
    error_log /dev/stderr;

    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    location ~ \.php$ {
        fastcgi_pass app:9000;
        fastcgi_index index.php;
        fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
        include fastcgi_params;
    }

    location ~ /\.(?!well-known).* {
        deny all;
    }
}

Bước 3: Tạo .env cho Compose

# .env (root project — Compose tự đọc file này)
DB_PASSWORD=secret
DB_DATABASE=laravel
DB_USERNAME=root
REDIS_PASSWORD=

Bước 4: Viết docker-compose.yml từ đầu

services:

  # ─── NGINX: Web server + reverse proxy ───
  nginx:
    image: nginx:alpine
    ports:
      - "8000:80"
    volumes:
      - ./src:/var/www/html
      - ./docker/nginx/default.conf:/etc/nginx/conf.d/default.conf
    depends_on:
      - app
    restart: unless-stopped

  # ─── APP: PHP-FPM + Laravel ───
  app:
    build:
      context: .
      dockerfile: Dockerfile
    volumes:
      - ./src:/var/www/html
    depends_on:
      - mysql
      - redis
    restart: unless-stopped

  # ─── MYSQL: Database ───
  mysql:
    image: mysql:8.0
    environment:
      MYSQL_ROOT_PASSWORD: ${DB_PASSWORD}
      MYSQL_DATABASE: ${DB_DATABASE}
    volumes:
      - mysql_data:/var/lib/mysql
    ports:
      - "3306:3306"                 # Optional: kết nối từ host (TablePlus)
    restart: unless-stopped

  # ─── REDIS: Cache + Session + Queue ───
  redis:
    image: redis:7-alpine
    volumes:
      - redis_data:/data
    restart: unless-stopped

# ─── NAMED VOLUMES ───
volumes:
  mysql_data:
  redis_data:

Bước 5: Khởi chạy

cd ~/docker-lab/laravel-compose

# Khởi chạy toàn bộ stack
docker compose up -d

# Output:
# ✔ Network laravel-compose_default  Created
# ✔ Volume "laravel-compose_mysql_data"  Created
# ✔ Volume "laravel-compose_redis_data"  Created
# ✔ Container laravel-compose-mysql-1  Started
# ✔ Container laravel-compose-redis-1  Started
# ✔ Container laravel-compose-app-1    Started
# ✔ Container laravel-compose-nginx-1  Started

Bước 6: Setup Laravel

# Install dependencies
docker compose exec app composer install

# Generate app key
docker compose exec app php artisan key:generate

# Chạy migration
docker compose exec app php artisan migrate

# Test
curl http://localhost:8000
# → Laravel Welcome Page

Các lệnh Docker Compose quan trọng

Lifecycle commands

# Khởi chạy (build + create + start)
docker compose up -d

# Dừng + xóa containers + network (giữ volumes)
docker compose down

# Dừng + xóa containers + network + VOLUMES (mất data!)
docker compose down -v

# Dừng (không xóa)
docker compose stop

# Khởi động lại (không recreate)
docker compose start

# Restart services
docker compose restart

Build & Pull

# Build lại images (khi thay đổi Dockerfile)
docker compose build

# Build lại + up
docker compose up -d --build

# Pull images mới nhất
docker compose pull

Monitoring & Debug

# Xem trạng thái tất cả services
docker compose ps

# Xem logs tất cả services
docker compose logs

# Xem logs 1 service cụ thể (follow)
docker compose logs -f app

# Xem logs 50 dòng cuối
docker compose logs --tail 50 app

Exec & Run

# Chạy lệnh trong container đang chạy (= docker exec)
docker compose exec app php artisan migrate
docker compose exec app composer install
docker compose exec mysql mysql -uroot -psecret laravel

# Mở shell
docker compose exec app sh

# Chạy one-off command (tạo container tạm, xóa sau khi xong)
docker compose run --rm app php artisan tinker

docker compose vs docker-compose

Phiên bản Lệnh Ghi chú
Compose V1 (legacy) docker-compose (có dấu gạch ngang) Standalone binary, Python. Deprecated.
Compose V2 (hiện tại) docker compose (space, không gạch ngang) Plugin của Docker CLI, Go. Dùng cái này.

Docker Desktop tự cài Compose V2. Nếu bạn thấy tutorial cũ dùng docker-compose, hãy thay bằng docker compose.


restart Policy

Trong docker-compose.yml, restart tương đương --restart flag từ Bài 4:

services:
  app:
    restart: unless-stopped    # Restart trừ khi stop thủ công
Policy Khi nào restart Use case
no Không bao giờ (default) Development, one-off tasks
always Luôn luôn, kể cả sau Docker restart Production critical services
unless-stopped Trừ khi stop thủ công Recommended cho dev/staging
on-failure Chỉ khi exit code ≠ 0 Queue workers, background tasks

So sánh Bài 7 vs Bài 8

Sau khi hoàn thành cả 2 bài, đây là sự khác biệt:

Tiêu chí Bài 7 (Thủ công) Bài 8 (Docker Compose)
Số lệnh để start ~10 lệnh (network, volume, 3x run, setup) 1 lệnh: docker compose up -d
Nhớ thứ tự start Tự nhớ: MySQL → App → Nginx depends_on tự xử lý
Tạo network docker network create thủ công Compose tự tạo
Tạo volume docker volume create thủ công Compose tự tạo
Build image docker build riêng build: trong file, auto build
Reproducible Phải ghi lại commands File YAML = documentation sống
Chia sẻ team Gửi README dài Commit docker-compose.yml vào Git
Cleanup 3-4 lệnh rm, network rm, volume rm docker compose down -v

Workflow hàng ngày với Compose

Sáng — Bắt đầu làm việc

cd ~/projects/my-laravel-app
docker compose up -d
# → Tất cả services start trong vài giây
# → http://localhost:8000 sẵn sàng

Trong ngày — Development

# Sửa code trên host → browser refresh (bind mount tự sync)

# Chạy artisan commands
docker compose exec app php artisan make:model Post -m
docker compose exec app php artisan migrate

# Chạy tests
docker compose exec app php artisan test

# Xem logs khi debug
docker compose logs -f app

# Vào MySQL shell
docker compose exec mysql mysql -uroot -psecret laravel

Tối — Kết thúc (optional)

# Dừng services (giữ data)
docker compose stop

# Hoặc để chạy nếu có restart: unless-stopped
# → Services tự start khi bật Docker Desktop lần sau

Khi thay đổi Dockerfile

# Rebuild image + recreate container
docker compose up -d --build

Khi muốn reset sạch

# Xóa tất cả (containers + network + volumes)
docker compose down -v

# Chạy lại từ đầu
docker compose up -d
docker compose exec app composer install
docker compose exec app php artisan migrate --seed

Trade-offs & Lưu ý

✅ Khi nào dùng Docker Compose

  • Local development — multi-container app (Laravel + MySQL + Redis + Nginx)
  • CI/CD — spin up test environment nhanh
  • Small deployments — single server, staging environment
  • Prototyping — thử nghiệm kiến trúc mới nhanh chóng

❌ Khi nào KHÔNG dùng Docker Compose

  • Production at scale — dùng Kubernetes, Docker Swarm, ECS
  • Single containerdocker run đủ rồi
  • Serverless — Lambda, Cloud Run không cần Compose

⚠️ Gotchas phổ biến

1. depends_on không đợi service ready

# depends_on chỉ đảm bảo container START, không đợi MySQL accept connections
app:
  depends_on:
    - mysql  # MySQL container started, nhưng có thể chưa ready!

Giải pháp → Bài 9 (healthcheck).

2. Volume data persist qua down

docker compose down      # Giữ volumes → data còn
docker compose down -v   # XÓA volumes → data mất

3. Rebuild khi thay Dockerfile

# SAI: up -d không rebuild nếu image đã có
docker compose up -d

# ĐÚNG: force rebuild
docker compose up -d --build

4. File .env có 2 chức năng khác nhau

.env ở thư mục docker-compose.yml:
→ Compose đọc để substitute ${VAR} trong docker-compose.yml
→ KHÔNG tự inject vào container

src/.env (Laravel .env):
→ Laravel đọc khi app chạy
→ Được bind mount vào container qua volumes

Checklist kiểm tra hiểu bài

  • Giải thích được Docker Compose giải quyết vấn đề gì so với docker run thủ công.
  • Viết được docker-compose.yml từ đầu cho Laravel stack (nginx + php-fpm + mysql + redis).
  • Phân biệt được image vs build, bind mount vs named volume trong Compose.
  • Hiểu depends_on — biết hạn chế của nó (không đợi service ready).
  • Dùng thành thạo: up -d, down, exec, logs, build, ps.
  • Hiểu restart: unless-stopped và khi nào dùng.
  • Phân biệt exec vs run trong Compose.
  • Biết cách dùng .env file với ${VAR} substitution.

Kết nối bài tiếp theo

Bạn đã biết cách dùng Docker Compose cho development. Nhưng còn nhiều thứ cần tối ưu: depends_on không đợi service ready, chưa phân biệt dev/prod config, chưa có healthcheck. Ở Bài 9: Docker Compose nâng cao, bạn sẽ học env_file, profiles, healthcheck, và restart policy để làm Compose setup production-ready hơn.


References

Bài viết liên quan

Đang cập nhật...