Bài 04
Docker Compose: 1 file YAML, dựng full stack
Bài 04 — Docker Compose: 1 file YAML, dựng full stack
Đây là bài cuối cùng của chặng Docker. Bài 01-03 bạn đã có mọi mảnh ghép:
- Container lifecycle (Bài 01)
- Image & Dockerfile (Bài 02)
- Network & Storage (Bài 03)
Giờ là lúc gắn tất cả lại. Vấn đề: app thực không chỉ 1 container. App thực = nhiều container chạy cùng nhau (web + database + cache + queue + ...). Quản lý 10 lệnh docker run mỗi lần deploy là kinh khủng.
Đây là lúc cần Docker Compose — orchestrate multi-container app qua 1 file YAML.
Mục tiêu bài này: dựng được full LEMP stack (nginx + PHP-FPM + MariaDB) qua 1 lệnh docker compose up. Đây là sản phẩm thật sự của chặng — gắn mọi kiến thức về 1 capstone.
Vấn đề: chạy nhiều container manual
Giả sử cần dựng stack có: 1 nginx + 1 PHP + 1 MariaDB + 1 Redis.
# 1. Tạo network
docker network create app-net
# 2. Tạo volumes
docker volume create db-data
docker volume create redis-data
# 3. Run MariaDB
docker run -d \
--name mariadb \
--network app-net \
-e MYSQL_ROOT_PASSWORD=secret \
-e MYSQL_DATABASE=app \
-v db-data:/var/lib/mysql \
--restart unless-stopped \
mariadb:11
# 4. Run Redis
docker run -d \
--name redis \
--network app-net \
-v redis-data:/data \
--restart unless-stopped \
redis:7-alpine
# 5. Build PHP image
docker build -t myapp-php ./php
# 6. Run PHP-FPM
docker run -d \
--name php \
--network app-net \
-v $(pwd)/src:/var/www/html \
--restart unless-stopped \
myapp-php
# 7. Run nginx
docker run -d \
--name nginx \
--network app-net \
-p 8080:80 \
-v $(pwd)/nginx.conf:/etc/nginx/conf.d/default.conf \
-v $(pwd)/src:/var/www/html \
--restart unless-stopped \
nginx:alpineVấn đề:
- 7 lệnh dài — dễ gõ sai.
- Thứ tự quan trọng — phải start MariaDB trước PHP. Sai thứ tự = lỗi.
- Khó tái sử dụng — onboarding dev mới: gửi 7 lệnh? Họ copy sai 1 dấu.
- Khó tear down — phải nhớ tên container, network, volume để xóa.
- Không version control — config nằm trong shell history.
Đây là 4 container đơn giản. Production stack 10-20 container = không khả thi.
Docker Compose là gì
Docker Compose là tool orchestrate multi-container app, mô tả toàn hệ thống trong 1 file YAML.
"Mô tả lại toàn hệ thống trong 1 file" — chính là tinh thần của Compose.
Manual (Bash): Docker Compose:
docker network create... ┌────────────────────┐
docker volume create... │ docker-compose.yml │
docker run -d --name... │ │
docker run -d --name... │ services: │
docker run -d --name... │ nginx: ... │
docker build -t... │ php: ... │
docker run -d --name... │ mariadb: ... │
docker run -d --name... │ volumes: │
│ db-data: │
7 lệnh, dễ sai │ networks: │
│ app-net: │
└─────────┬──────────┘
│
│ docker compose up
▼
Mọi container UP
Network + Volume tạo
Đúng thứ tự
1 LỆNH3 lý do cần Compose
- Môi trường nhiều container — app modern ít khi 1 container, thường 3-20.
- Chuẩn hóa môi trường —
compose.ymlcommit git, ai pull về cũng dựng giống nhau. - Mô tả toàn hệ thống trong 1 file — file vừa là code, vừa là documentation.
Compose vs Production Orchestration
⚠️ Quan trọng để biết:
Docker Compose Docker Swarm Kubernetes
─────────────── ───────────── ──────────
Scope: 1 host Cluster (multi) Cluster (multi)
Quy mô: Dev, small prod Medium prod Large prod
Học: Dễ (1 ngày) Trung bình Khó (tháng)
File: docker-compose.yml docker stack.yml YAML manifest
Use case: Dev, CI/CD, local test Small clusters Production scale
Tài liệu của bạn có ghi:
"Chủ yếu dùng trong test/dev. Môi trường prod thường dùng Stack, K8s"→ Compose tốt cho: local dev, CI/CD pipeline, smoke test, demo, app nhỏ 1 server. Production thật quy mô lớn: Kubernetes.
Nhưng học Compose là bước đệm tốt để hiểu Kubernetes — concept service, network, volume tương tự.
Cài đặt Docker Compose
Docker Compose v2 đã tích hợp sẵn trong Docker CLI từ 2022 — không cần cài thêm:
docker compose version
# Docker Compose version v2.20.3(Lưu ý syntax: docker compose — có space, không phải dash docker-compose. V1 dùng docker-compose, v2 dùng docker compose. V1 đã deprecated.)
Nếu chưa có:
# CentOS 9
sudo dnf install docker-compose-plugin
# Ubuntu
sudo apt install docker-compose-pluginCấu trúc file docker-compose.yml
File YAML mô tả toàn stack. 4 top-level keys chính:
version: "3.8" # Compose file format version
services: # Container services (web, db, ...)
nginx:
...
db:
...
volumes: # Named volumes
db-data:
networks: # Custom networks
app-net: docker-compose.yml
┌────────────────────────────────────────┐
│ version: "3.8" │
│ │
│ ┌────────────────────────────────────┐ │
│ │ services: (các container) │ │
│ │ ┌─────────┐ ┌─────────┐ ┌────────┐ │ │
│ │ │ nginx │ │ php │ │ db │ │ │
│ │ │ │ │ │ │ │ │ │
│ │ │ image │ │ build │ │ image │ │ │
│ │ │ ports │ │ volumes │ │ env │ │ │
│ │ │ volumes │ │ networks│ │ volumes│ │ │
│ │ │ depends │ │ │ │ network│ │ │
│ │ └────┬────┘ └────┬────┘ └────┬───┘ │ │
│ └──────┼───────────┼───────────┼─────┘ │
│ │ │ │ │
│ ┌──────┴───────────┴───────────┴─────┐ │
│ │ networks: │ │
│ │ app-net: │ │
│ └────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────┐│
│ │ volumes: ││
│ │ db-data: ││
│ └──────────────────────────────────────┘│
└────────────────────────────────────────┘YAML cú pháp nhanh
key: value # key-value
key:
- item1 # list
- item2
key: # nested object
subkey: value
another_key:
deep_key: value
# Comment
key: "string with spaces" # quote khi cần⚠️ YAML rất nhạy với indentation. Dùng space, KHÔNG tab. Mọi level con thụt vào 2 space.
Compose Commands cơ bản
# Khởi động stack (đọc docker-compose.yml ở thư mục hiện tại)
docker compose up # foreground, thấy log
docker compose up -d # detach (background)
# Dừng và xóa
docker compose down # giữ volume
docker compose down -v # xóa cả volume (CẢNH BÁO: mất data)
# Dùng file khác
docker compose -f production.yml up -d
# Xem container đang chạy
docker compose ps
# Xem log
docker compose logs -f # all services
docker compose logs -f nginx # 1 service
# Restart 1 service
docker compose restart nginx
# Exec vào container
docker compose exec nginx bash
# Build lại image trước khi up
docker compose up -d --build
# Scale 1 service (chạy nhiều instance)
docker compose up -d --scale php=3⚠️ Tài liệu của bạn ghi docker-compose downd — typo, đúng là docker compose down.
Service Definition chi tiết
Mỗi service là 1 container. Các thuộc tính phổ biến:
services:
myservice:
# ─── Source: từ image có sẵn HOẶC build từ Dockerfile ───
image: nginx:1.25-alpine # từ registry
# HOẶC
build:
context: ./php # thư mục chứa Dockerfile
dockerfile: Dockerfile # tên file Dockerfile
# ─── Container name (optional) ───
container_name: myapp-nginx
# ─── Port mapping ───
ports:
- "8080:80" # host:container
- "443:443"
# ─── Volumes ───
volumes:
- ./nginx.conf:/etc/nginx/conf.d/default.conf # bind mount
- db-data:/var/lib/mysql # named volume
# ─── Environment variables ───
environment:
- MYSQL_ROOT_PASSWORD=secret
- APP_ENV=production
# HOẶC
env_file:
- .env
# ─── Network ───
networks:
- app-net
- public-net
# ─── Dependencies ───
depends_on:
- db # start sau db
- redis
# ─── Restart policy ───
restart: unless-stopped # auto restart khi crash
# ─── Healthcheck ───
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost"]
interval: 30s
timeout: 3s
retries: 3
# ─── Resource limits ───
deploy:
resources:
limits:
cpus: '0.50'
memory: 512M
# ─── Override command ───
command: ["nginx", "-g", "daemon off;"]image vs build
# image: pull từ registry (Docker Hub, etc.)
image: mysql:8
# build: build từ Dockerfile local
build:
context: ./backend # folder có Dockerfile
dockerfile: Dockerfile # tên file (default "Dockerfile")→ Image cho service standard (DB, cache). Build cho app code custom.
depends_on — thứ tự khởi động
services:
nginx:
depends_on:
- php # nginx start SAU php
- db # AND sau db
php:
depends_on:
- db # php start sau dbStart order:
db ──► php ──► nginx
▲ ▲ ▲
│ │ │
"depends_on: db"
"depends_on: php"⚠️ depends_on chỉ check container start, KHÔNG check app ready. Vd: MySQL container start lên nhưng MySQL daemon chưa initialize → PHP try connect fail.
Solution: dùng depends_on với condition:
depends_on:
db:
condition: service_healthy # đợi healthcheck PASSHoặc app retry connection (production approach).
Restart policies
restart: "no" # không tự restart (default)
restart: always # luôn restart khi exit
restart: on-failure # restart khi exit != 0
restart: unless-stopped # restart trừ khi user stop (RECOMMEND)Production hầu hết dùng unless-stopped — auto recover khi app crash, nhưng respect user stop.
DNS giữa Services — magic
Trong Compose, mỗi service name = DNS hostname. Không cần config gì thêm.
services:
web:
image: nginx
db:
image: mysql:8
cache:
image: redisContainers tự thấy nhau qua tên service:
web → ping db ✅ (resolve thành IP container db)
web → ping cache ✅
cache → ping db ✅
Trong app code (PHP, Node):
$host = "db"; // dùng service name làm hostname
$port = 3306;→ Compose tự tạo network mặc định cho stack, mọi service connect vào, DNS work. Tương tự custom bridge từ Bài 03.
Variables với .env file
Tách secrets/config khỏi compose file — best practice.
Tạo file .env
# .env (cùng folder với docker-compose.yml)
MYSQL_ROOT_PASSWORD=supersecret
MYSQL_DATABASE=myapp
REDIS_PASSWORD=cache_pwd
APP_ENV=productionReference trong compose
services:
db:
image: mysql:8
environment:
- MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD}
- MYSQL_DATABASE=${MYSQL_DATABASE}Compose auto load .env khi run.
env_file — pass tất cả vars trong .env
services:
app:
image: myapp
env_file:
- .env # mọi var trong .env có sẵn trong containerBest practice security
- Commit
.env.example(template, không có giá trị thật):MYSQL_ROOT_PASSWORD= MYSQL_DATABASE= - KHÔNG commit
.env(có secret). Thêm.envvào.gitignore. - Production: dùng secret manager (Vault, AWS Secrets Manager), không phải
.env.
🚀 Lab Capstone — Dựng full LEMP stack
Đây là product cuối cùng của chặng. Sau lab này bạn dựng được toàn bộ stack chặng 4 chỉ với 1 lệnh.
Cấu trúc thư mục
my-lemp-stack/
├── docker-compose.yml
├── .env
├── nginx/
│ └── default.conf
├── php/
│ └── Dockerfile
└── src/
└── index.phpFile 1: docker-compose.yml
version: "3.8"
services:
nginx:
image: nginx:1.25-alpine
container_name: lemp-nginx
ports:
- "8080:80"
volumes:
- ./nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
- ./src:/var/www/html
depends_on:
- php
networks:
- app-net
restart: unless-stopped
php:
build:
context: ./php
dockerfile: Dockerfile
container_name: lemp-php
volumes:
- ./src:/var/www/html
depends_on:
- db
networks:
- app-net
restart: unless-stopped
db:
image: mariadb:11
container_name: lemp-db
environment:
- MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD}
- MYSQL_DATABASE=${MYSQL_DATABASE}
- MYSQL_USER=${MYSQL_USER}
- MYSQL_PASSWORD=${MYSQL_PASSWORD}
volumes:
- db-data:/var/lib/mysql
networks:
- app-net
restart: unless-stopped
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 10s
timeout: 5s
retries: 5
redis:
image: redis:7-alpine
container_name: lemp-redis
volumes:
- redis-data:/data
networks:
- app-net
restart: unless-stopped
volumes:
db-data:
redis-data:
networks:
app-net:
driver: bridgeFile 2: .env
# Database credentials
MYSQL_ROOT_PASSWORD=rootpass123
MYSQL_DATABASE=myapp
MYSQL_USER=appuser
MYSQL_PASSWORD=apppass123File 3: nginx/default.conf
server {
listen 80;
server_name localhost;
root /var/www/html;
index index.php index.html;
location / {
try_files $uri $uri/ /index.php?$args;
}
location ~ \.php$ {
fastcgi_pass php:9000; # ← "php" là service name, DNS auto
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
}File 4: php/Dockerfile
FROM php:8.2-fpm-alpine
# Cài extension cần thiết
RUN apk add --no-cache mariadb-client \
&& docker-php-ext-install pdo pdo_mysql mysqli
# Cài redis extension
RUN apk add --no-cache --virtual .build-deps autoconf gcc g++ make \
&& pecl install redis \
&& docker-php-ext-enable redis \
&& apk del .build-deps
WORKDIR /var/www/htmlFile 5: src/index.php
<?php
// Test connection tới các service trong stack
echo "<h1>LEMP Stack Test</h1>";
// 1. PHP version
echo "<p>PHP: " . phpversion() . "</p>";
// 2. MariaDB connection
try {
$db = new PDO(
"mysql:host=db;dbname=" . getenv("MYSQL_DATABASE"),
getenv("MYSQL_USER"),
getenv("MYSQL_PASSWORD")
);
echo "<p>✅ MariaDB: connected</p>";
} catch (PDOException $e) {
echo "<p>❌ MariaDB: " . $e->getMessage() . "</p>";
}
// 3. Redis connection
try {
$redis = new Redis();
$redis->connect("redis", 6379);
$redis->set("test", "OK");
echo "<p>✅ Redis: " . $redis->get("test") . "</p>";
} catch (Exception $e) {
echo "<p>❌ Redis: " . $e->getMessage() . "</p>";
}⚠️ Pass DB credentials vào PHP container — cần thêm environment trong service php:
php:
build: ...
environment:
- MYSQL_DATABASE=${MYSQL_DATABASE}
- MYSQL_USER=${MYSQL_USER}
- MYSQL_PASSWORD=${MYSQL_PASSWORD}Chạy stack
cd my-lemp-stack
# 1 lệnh dựng cả stack
docker compose up -d
# Output:
# [+] Running 4/4
# ✔ Network my-lemp-stack_app-net Created
# ✔ Container lemp-db Started
# ✔ Container lemp-redis Started
# ✔ Container lemp-php Started
# ✔ Container lemp-nginx Started
# Kiểm tra:
docker compose ps
# NAME IMAGE STATUS PORTS
# lemp-db mariadb:11 Up 30 seconds 3306/tcp
# lemp-nginx nginx:1.25-alpine Up 25 seconds 0.0.0.0:8080->80/tcp
# lemp-php my-lemp-stack-php Up 25 seconds 9000/tcp
# lemp-redis redis:7-alpine Up 30 seconds 6379/tcp
# Test:
curl http://localhost:8080
# <h1>LEMP Stack Test</h1>
# <p>PHP: 8.2.x</p>
# <p>✅ MariaDB: connected</p>
# <p>✅ Redis: OK</p>→ Toàn bộ LEMP stack lên trong 30 giây. So với chặng 4 cài bare-metal mất 12 tiếng. Magic của container.
Tear down
docker compose down # giữ data
docker compose down -v # xóa luôn volume (mất DB data)Multi-environment Pattern
Production thực tế: dev và prod khác nhau. Compose hỗ trợ multiple files.
Pattern 1: Compose override
my-lemp-stack/
├── docker-compose.yml # base
├── docker-compose.override.yml # dev (auto-loaded)
└── docker-compose.prod.yml # productiondocker-compose.yml (base):
services:
nginx:
image: nginx:1.25-alpine
ports:
- "8080:80"docker-compose.override.yml (dev — auto-loaded):
services:
nginx:
volumes:
- ./src:/var/www/html # bind mount dev
environment:
- DEBUG=truedocker-compose.prod.yml:
services:
nginx:
image: myrepo/nginx:v1.2.3 # image custom đã build
restart: always
deploy:
resources:
limits:
cpus: '1.0'
memory: 1GCách run
# Dev: auto merge base + override
docker compose up -d
# Production: explicit
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d→ 1 codebase, nhiều môi trường. Dev pull code, docker compose up ngay. Prod pull image build sẵn.
Câu chuyện thực tế
Năm 2023, mình join 1 startup. Team có 6 dev. Mỗi dev mới onboard mất 2 ngày chỉ để setup local env:
- Cài PHP 8.2 + extensions (3 tiếng, dễ sai version)
- Cài MySQL 8 + tạo DB + user (1 tiếng)
- Cài Redis (30 phút)
- Setup nginx config (1 tiếng)
- Fix permission các thư mục (1-2 tiếng)
- Debug "trên máy chạy được" issues (vài tiếng)
6 dev × 2 ngày = 12 ngày-người chỉ để setup env. Chưa kể inconsistency — dev A có PHP 8.2.3, dev B có 8.2.5, gây bug khó debug.
Mình spend 1 ngày viết docker-compose.yml. Sau đó workflow onboard dev mới:
git clone repo.git
cd repo
cp .env.example .env
docker compose up -d
# 30 giây sau: env sẵn sàng5 phút thay vì 2 ngày. 6 dev × 2 ngày → 6 dev × 5 phút = tiết kiệm 11.5 ngày-người onboard.
Bonus:
- Mọi dev có env giống y hệt — không còn "trên máy chạy được".
- Senior debug junior: pull code junior,
docker compose up→ reproduce ngay. - Switch project:
docker compose down→docker compose upproject khác. Không conflict port/version.
Bài học:
Compose ROI cực cao. 1 ngày viết = tiết kiệm hàng tuần onboarding.
Đầu tư setup chuẩn từ đầu project. Project thứ 2 trở đi: clone template, đổi tên service, xong.
.env.examplelà phần documentation. Junior xem.env.examplebiết app cần config gì.docker-compose.ymllà documentation thật. Quên app dùng port nào? Mở compose file ra xem.Tách dev vs prod compose. Production cần image build sẵn, không bind mount source code.
Mock data trong
init.sql. Mount SQL script vào MariaDB image — container tạo DB + data tự động lần đầu run.
Pitfalls
YAML indent sai → parse error, không hiểu lỗi. Dùng 2 space, không tab. Editor có YAML lint là nice.
depends_onkhông đợi app ready — chỉ đợi container start. App connect DB sớm → fail. Dùng healthcheck +service_healthy.Commit
.envlên git → leak secrets. Add.envvào.gitignore. Chỉ commit.env.example.Quên define volume trong top-level
volumes:→ compose báo lỗi "volume not declared".Service tên có dấu gạch dưới
_→ DNS resolve có thể fail trên 1 số config. Dùng-(hyphen).Hardcode port trong code thay vì DNS service name → khó migrate giữa env.
docker compose down -vngày production → mất toàn bộ volume. Cẩn thận-vflag.Compose file version cũ (2.x) → thiếu features mới. Dùng "3.8" hoặc bỏ version (v2+).
Mount bind code trong production → security risk + slow filesystem. Production dùng image với code build vào.
Quên
restart: unless-stopped→ container crash không tự lên. Production mọi service phải có restart policy.Logs không rotate → compose log mặc định ghi unlimited. Set log driver + rotation:
services: web: logging: driver: "json-file" options: max-size: "10m" max-file: "3"Dùng
docker-compose(V1) thaydocker compose(V2) — V1 deprecated. Update lên V2.Container name conflict giữa nhiều stack — đặt prefix project name hoặc bỏ
container_name.Healthcheck quá nghiêm —
interval: 1s→ spam check, ngốn CPU. 30s là OK.
Tóm tắt
- Docker Compose = orchestrate multi-container app qua 1 file YAML.
- 3 lý do dùng: môi trường nhiều container, chuẩn hóa, mô tả toàn hệ thống trong 1 file.
- Compose tốt cho dev/CI, production lớn dùng K8s.
- 4 top-level keys:
version,services,volumes,networks. docker compose up -d= 1 lệnh dựng full stack.docker compose down= teardown.- Service definition: image/build, ports, volumes, environment, depends_on, networks, restart, healthcheck.
- DNS auto giữa service qua service name (như custom bridge từ Bài 03).
.envfile cho secrets/config. Không commit.env, chỉ.env.example.depends_onchỉ check container start, không check app ready. Dùng healthcheck + condition.- Multi-environment: base file + override (dev/prod).
restart: unless-stoppedcho mọi production service.- YAML rất nhạy indent — dùng space, không tab.
🎓 Kết thúc Chặng 6 — Container & Docker
Đi qua 4 bài:
- Bài 01 — Docker cơ bản: container vs VM, kiến trúc Docker, lifecycle, vào shell, monitor.
- Bài 02 — Image & Dockerfile: layers, registry, Dockerfile, 10 best practices tối ưu image.
- Bài 03 — Network & Storage: 5 network drivers, custom bridge + DNS, Volume/Bind/tmpfs, immutable infrastructure.
- Bài 04 — Docker Compose: orchestrate multi-container, full LEMP stack qua 1 file YAML.
Từ 1 lệnh docker run hello-world → dựng được full production-ready stack trong 30 giây.
Bạn đã sẵn sàng cho
- Kubernetes (chặng tiếp): K8s là "Compose ở cluster scale". Service = service. Volume = PersistentVolume. Network = NetworkPolicy. Compose hiểu rồi K8s sẽ thấy quen.
- CI/CD: build image trong pipeline (GitHub Actions, GitLab CI), push registry, deploy. Chặng tiếp.
- Observability: Prometheus + Grafana scrape container metrics. Loki collect log. Cluster-ready monitoring.
- Service Mesh (Istio, Linkerd): tách service-to-service traffic ra mesh layer. Cần K8s trước.
- Microservices architecture: containerize từng service, mỗi service team riêng quản lý.
Lời khuyên đi tiếp
Không vội học K8s. Compose hiểu vững → K8s 50% đã quen. Đừng skip.
Containerize 1 project thật. Pick app cũ, dockerize end-to-end. Học từ pain thực.
Cố tình break stack — stop 1 container, network die, volume corrupt. Học debug.
Đọc Compose file open source. Awesome-compose repo có nhiều ví dụ production. Study how others do it.
docker compose down -vkhông phải lỗi gõ — là feature reset stack. Nhưng đừng trong production.Multi-stage Dockerfile + multi-environment Compose = combo production-grade.
Sau Docker là K8s, sau K8s là... infinity. Vẫn cần basics chắc. Đừng skip foundation.
Chặng tiếp theo là Kubernetes. Nếu hôm nay bạn dựng được LEMP stack qua 1 lệnh docker compose up — bạn đã có nền vững nhất.
Chúc bạn build được hệ thống mà junior mới vào, 5 phút là chạy được! 🚀
— Minh Hưng