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 rungiố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 container —
docker 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 runthủ công. - Viết được
docker-compose.ymltừ đầu cho Laravel stack (nginx + php-fpm + mysql + redis). - Phân biệt được
imagevsbuild, 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-stoppedvà khi nào dùng. - Phân biệt
execvsruntrong Compose. - Biết cách dùng
.envfile 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.