🐳 DOCKER: ORCHESTRATION & BEST PRACTICES
🎯 Mục Tiêu
- Hiểu rõ vai trò, lợi ích của Docker Compose trong việc quản lý ứng dụng đa-container.
- Nắm vững cú pháp và các chỉ thị quan trọng của file
docker-compose.yml. - Biết cách định nghĩa và quản lý services, networks, và volumes một cách hiệu quả với Docker Compose.
- Thực hành xây dựng một ứng dụng multi-container phức tạp hơn, bao gồm web, database và caching.
- Tìm hiểu và áp dụng các best practices khi làm việc với Docker và Docker Compose để tối ưu hóa quy trình phát triển và triển khai.
- Hiểu cách Docker Compose đơn giản hóa việc thiết lập môi trường phát triển và đảm bảo tính nhất quán.
1. ⏪ Ôn Lại phần trước
Key Concepts: Image, Container, Dockerfile, Registry
- Image: Template read-only, chứa mọi thứ cần để chạy ứng dụng (code, runtime, libraries, environment variables, config files). Được build từ Dockerfile.
- Container: Instance chạy của một image. Là một môi trường isolated, có filesystem, process, network riêng, nhưng chia sẻ kernel của Host OS.
- Dockerfile: File text chứa các instructions (lệnh) để Docker Engine tự động build một image.
- Registry: Kho lưu trữ và phân phối Docker images (VD: Docker Hub, AWS ECR, Google GCR).
Basic Docker CLI Commands
docker build -t <name:tag> .: Build image từ Dockerfile.docker run [OPTIONS] <image>: Chạy container từ image.- Options quan trọng:
-d(detached),-p HOST_PORT:CONTAINER_PORT,--name,-it(interactive),--rm(auto-remove),-v HOST_PATH:CONTAINER_PATH(volume),-e VAR=value.
- Options quan trọng:
docker ps [-a]: Liệt kê containers (đang chạy / tất cả).docker images: Liệt kê images.docker stop/start/restart <container>: Quản lý lifecycle container.docker rm <container>: Xóa container (phải dừng trước, hoặc dùng-f).docker rmi <image>: Xóa image (phải không có container nào dùng, hoặc dùng-f).docker logs [-f] <container>: Xem logs.docker exec -it <container> <command>: Chạy lệnh trong container đang chạy.docker pull <image>/docker push <repo/image>: Tương tác với registry.
2. 🚀 Giới Thiệu Docker Compose
Tại sao cần Docker Compose? Vấn đề với nhiều docker run
Hãy tưởng tượng một ứng dụng web hiện đại thường bao gồm nhiều thành phần (services) phối hợp với nhau:
- Một
web server(Nginx, Apache) để phục vụ static files hoặc làm reverse proxy. - Một
application server(Node.js, Python/Django/Flask, Java/Spring Boot, Ruby/Rails, PHP/Laravel) chứa business logic. - Một
database(PostgreSQL, MySQL, MongoDB, etc.) để lưu trữ dữ liệu. - Có thể thêm một
caching service(Redis, Memcached) để tăng tốc độ. - Có thể thêm
message queue(RabbitMQ, Kafka) cho xử lý bất đồng bộ.
Nếu dùng docker run cho từng service, bạn sẽ phải đối mặt với:
# Chạy database (VD: Postgres)
docker run -d --name my_db \
-e POSTGRES_USER=user \
-e POSTGRES_PASSWORD=secret \
-e POSTGRES_DB=appdb \
-v db_data:/var/lib/postgresql/data \
--network app_net \
postgres:14-alpine
# Chạy backend app, link tới DB, expose port
# Cần đợi DB sẵn sàng, biết IP của DB (hoặc dùng Docker network và tên service)
docker run -d --name my_app \
--network app_net \
-p 3000:3000 \
-e DATABASE_URL="postgresql://user:secret@my_db:5432/appdb" \
-e NODE_ENV=development \
-v ./app_src:/usr/src/app \
my_backend_image:latest
# Chạy frontend (VD: Nginx phục vụ static files và proxy API)
docker run -d --name my_frontend \
--network app_net \
-p 80:80 \
-v ./frontend_static:/usr/share/nginx/html \
-v ./nginx.conf:/etc/nginx/nginx.conf:ro \
nginx:alpine
# Và có thể còn nhiều services khác...Những khó khăn:
- Quá nhiều lệnh
docker rundài và phức tạp: Khó nhớ, dễ gõ sai. - Quản lý dependencies: Service A cần Service B chạy trước và sẵn sàng (VD: app cần DB).
docker runkhông có cơ chếdepends_onrõ ràng. - Quản lý network: Phải tự tạo Docker network (
docker network create app_net) và kết nối các container vào đó để chúng thấy nhau. - Quản lý volumes: Khai báo volumes cho từng service.
- Cấu hình: Truyền biến môi trường, mount config files cho từng service.
- Khó chia sẻ và tái tạo: Đưa cho đồng nghiệp một loạt lệnh
docker runđể setup môi trường là không hiệu quả và dễ lỗi. - Khó scale (ở mức cơ bản): Muốn chạy nhiều instance của một service sẽ phức tạp.
- Dừng và dọn dẹp: Phải
docker stopvàdocker rmtừng container một.
Docker Compose được sinh ra để giải quyết tất cả những vấn đề này.
Docker Compose là gì?
- Là một công cụ dòng lệnh (CLI tool) để định nghĩa (define) và chạy (run) các ứng dụng Docker đa-container (multi-container) một cách dễ dàng.
- Sử dụng một file cấu hình duy nhất viết bằng YAML (thường là
docker-compose.yml) để mô tả toàn bộ "stack" ứng dụng của bạn, bao gồm:- Các
services(tương ứng với các containers). - Cấu hình cho từng service: image nào để build/pull, ports, volumes, environment variables, dependencies, networks, etc.
Networksmà các services sẽ kết nối vào.Volumesđể lưu trữ dữ liệu bền bỉ.
- Các
- Với một lệnh duy nhất (ví dụ:
docker-compose up), bạn có thể khởi tạo, cấu hình và chạy toàn bộ ứng dụng với tất cả các services liên quan.
Lợi ích chính của Docker Compose:
- Đơn giản hóa quản lý: Tất cả cấu hình nằm trong một file, dễ đọc, dễ hiểu, dễ quản lý.
- Tái tạo môi trường nhất quán: Đảm bảo môi trường phát triển, testing, staging giống nhau cho mọi thành viên trong team và trên các máy khác nhau.
- Phát triển nhanh hơn: Setup môi trường nhanh chóng, tập trung vào code thay vì loay hoay cấu hình.
- Tích hợp tốt với Docker Engine: Sử dụng các khái niệm Docker quen thuộc.
- Quản lý vòng đời ứng dụng dễ dàng:
up,down,start,stop,restarttoàn bộ stack hoặc từng service. - Cô lập môi trường: Mỗi project Compose có thể chạy độc lập mà không ảnh hưởng đến nhau.
+----------------------------+
| docker-compose.yml |
| (Định nghĩa App Stack) |
+-------------+--------------+
| (Input cho)
v
+---------------------------------------+----------------------------------------+
| DOCKER COMPOSE CLI |
| (`docker-compose up`, `down`, `ps`, `logs`, `exec`, etc.) |
+---------------------------------------+----------------------------------------+
| (Ra lệnh cho Docker Daemon)
v
+--------------------------------------------------------------------------------+
| DOCKER HOST |
| +----------------------------------------------------------------------------+ |
| | Docker Daemon | |
| | (Tạo và quản lý containers, networks, volumes dựa trên docker-compose.yml) | |
| | | |
| | +------------------ Network: myproject_default ------------------------+ | |
| | | | | |
| | | +-----------------+ <-- communicates via service name --> +---------+ | | |
| | | | Service: web | | Service:db| | | |
| | | | (Container 1) | | (Cont. 3) | | | |
| | | | - Nginx/Node.js | <-- communicates via service name --> +---------+ | | |
| | | | - Port 80 mapped| | - Postgres| | | |
| | | +-----------------+ <-- communicates via service name --> +---------+ | | |
| | | ^ | Service:api| | | |
| | | | (communicates via service name) | (Cont. 2) | | | |
| | | +--------------------------------------------------+ - Python | | | |
| | | +---------+ | | |
| | +--------------------------------------------------------------------------+ | |
| +----------------------------------------------------------------------------+ |
+--------------------------------------------------------------------------------+
(Volume: myproject_db_data) <--- (Persists data for db service)Cài đặt Docker Compose
Docker Desktop (Windows, macOS): Docker Compose thường được cài đặt sẵn cùng với Docker Desktop. Bạn không cần cài riêng.
Linux:
Thường phải cài đặt riêng. Cách phổ biến là tải binary từ GitHub releases của Docker Compose.
Tham khảo hướng dẫn cài đặt chính thức: https://docs.docker.com/compose/install/
Lệnh ví dụ (phiên bản có thể thay đổi, luôn kiểm tra trang chủ):
bash# Tải phiên bản ổn định mới nhất LATEST_COMPOSE_VERSION=$(curl -s https://api.github.com/repos/docker/compose/releases/latest | grep 'tag_name' | cut -d\" -f4) sudo curl -L "https://github.com/docker/compose/releases/download/${LATEST_COMPOSE_VERSION}/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose sudo chmod +x /usr/local/bin/docker-compose # Kiểm tra cài đặt docker-compose --versionMột số bản phân phối Linux có thể cung cấp Docker Compose qua package manager (
apt,yum), nhưng phiên bản có thể cũ hơn.
Lưu ý: docker-compose (có dấu gạch nối) là phiên bản V1. docker compose (không có dấu gạch nối) là phiên bản V2, được tích hợp trực tiếp vào Docker CLI. Hiện tại, V2 được khuyến khích sử dụng. Nếu Docker Desktop của bạn mới, docker compose sẽ hoạt động. Trên Linux, cài đặt trên có thể là V1 hoặc V2 tùy phiên bản. Về cơ bản, cú pháp file YAML và các lệnh chính là tương tự.
3. 🎼 Cú Pháp docker-compose.yml
File docker-compose.yml là một file text định dạng YAML, là trái tim của Docker Compose. YAML (YAML Ain't Markup Language) là một định dạng tuần tự hóa dữ liệu dễ đọc cho con người, sử dụng thụt đầu dòng (indentation) để biểu thị cấu trúc. QUAN TRỌNG: YAML rất nhạy cảm với thụt đầu dòng. Luôn sử dụng spaces, không dùng tabs, và thụt đầu dòng nhất quán (thường là 2 spaces).
File này thường nằm ở thư mục gốc của project.
version
Chỉ định phiên bản của cú pháp file Compose mà bạn đang sử dụng. Điều này quan trọng vì các phiên bản khác nhau có thể hỗ trợ các tính năng và cú pháp khác nhau.
version:
"3.8" # Hoặc "3.9", "3.x". Nên dùng phiên bản mới nhất được Docker Engine của bạn hỗ trợ.
# Version "2.x" cũ hơn, "3.x" phổ biến hiện nay.
# Docker Compose V2 (docker compose) không yêu cầu version string, nhưng vẫn nên có.services
Đây là nơi bạn định nghĩa các thành phần (containers) của ứng dụng. Mỗi key dưới services là tên của một service. Tên này quan trọng vì nó sẽ được Docker Compose sử dụng để tham chiếu giữa các services (ví dụ: service web có thể kết nối đến service db bằng hostname db).
services:
web:# Tên service 1 (ví dụ:
frontend hoặc web server)
# ... cấu hình cho service 'web' ...
api:# Tên service 2 (ví dụ:
backend application)
# ... cấu hình cho service 'api' ...
db:# Tên service 3 (ví dụ:
database)
# ... cấu hình cho service 'db' ...Dưới mỗi tên service, bạn sẽ khai báo các chi tiết cấu hình cho nó:
build vs image
Chỉ định image Docker sẽ được sử dụng cho service. Bạn có thể dùng một trong hai (hoặc đôi khi cả hai).
image: <image_name>:<tag>: Sử dụng một image đã có sẵn từ Docker Hub hoặc một private registry. Docker Compose sẽpullimage này nếu nó chưa có ở local.yamlservices: database: image: postgres:14-alpine # Lấy image postgres phiên bản 14-alpine redis: image: redis:7-alpinebuild: <path_to_context>hoặcbuild: { context: <path>, dockerfile: <name>, args: ... }: Docker Compose sẽ build một image từ Dockerfile.Dạng string đơn giản:
yamlservices: backend: build: ./app_folder # Đường dẫn đến thư mục chứa Dockerfile (build context) # Docker Compose sẽ tìm file tên 'Dockerfile' trong đó.Dạng object để cung cấp thêm chi tiết:
yamlservices: backend: build: context: ./app_folder # Thư mục build context. dockerfile: Dockerfile.dev # Tên Dockerfile (nếu khác 'Dockerfile'). args: # Các biến --build-arg truyền vào Dockerfile. NODE_VERSION: 18 APP_ENV: development # target: builder # (Tùy chọn) Chỉ build một stage cụ thể trong multi-stage Dockerfile. # cache_from: # (Tùy chọn) Sử dụng cache từ các image khác. # - myapp_cache:latestBạn có thể dùng cả
imagevàbuild. Docker Compose sẽ build (nếubuildđược định nghĩa) và tag image đó với tên bạn cung cấp trongimage. Nếu image đó đã tồn tại và bạn không yêu cầu build lại, nó sẽ dùng image đó.
yamlservices: custom_app: build: ./my_app_src image: myusername/my_custom_app:latest # Sau khi build, image sẽ được tag thế này
ports
Ánh xạ ports giữa máy host và container. Định dạng: "HOST_PORT:CONTAINER_PORT". Nếu chỉ ghi "CONTAINER_PORT", Docker sẽ tự động chọn một port ngẫu nhiên trên host.
services:
frontend:
image: nginx:latest
ports:
- "8080:80" # Map port 8080 của host tới port 80 của container frontend.
- "127.0.0.1:8081:81" # Map port 8081 của host (chỉ trên localhost) tới port 81 của container.
# - "443:443" # Map port 443 của host tới port 443 của container (cho HTTPS).
# - "9000" # Expose port 9000 của container ra một port ngẫu nhiên trên host.volumes
Mount (gắn) thư mục từ host vào container (bind mount) hoặc quản lý data volumes (named volumes) để lưu trữ dữ liệu bền bỉ. Định dạng:
- Bind mount:
"HOST_PATH:CONTAINER_PATH"hoặc"HOST_PATH:CONTAINER_PATH:ro"(read-only). - Named volume:
"VOLUME_NAME:CONTAINER_PATH". - Anonymous volume:
"CONTAINER_PATH"(ít dùng trực tiếp trong compose, Docker tự quản lý).
services:
backend:
build: ./backend_app
volumes:
# Bind mount: thay đổi code ở host -> thay đổi trong container (rất tiện cho dev)
- ./backend_app/src:/app/src
# Named volume: lưu trữ logs bền bỉ, tách biệt với lifecycle của container
- app_logs:/app/logs
# Anonymous volume (trong trường hợp này, để node_modules trong container không bị ghi đè bởi host)
- /app/node_modules
database:
image: postgres:14
volumes:
# Named volume: lưu trữ data của DB, đảm bảo dữ liệu không mất khi container bị xóa/tạo lại
- db_data:/var/lib/postgresql/data
# Bind mount: mount file cấu hình tùy chỉnh
- ./custom-postgres.conf:/etc/postgresql/postgresql.conf:ro
# Khai báo top-level named volumes (nếu không khai báo, Docker tự tạo với tên project_prefix)
volumes:
db_data:# Docker sẽ tạo và quản lý volume tên là 'myproject_db_data' (nếu project tên myproject)
# driver: local # Mặc định
# external: true # Nếu volume đã được tạo bên ngoài Docker Compose
# name: my_existing_db_data # Nếu dùng external volume với tên khác
app_logs:(Sẽ nói chi tiết hơn ở phần Docker Volumes)
environment
Thiết lập biến môi trường cho container. Có nhiều cách khai báo:
Dạng list (mảng các string
KEY=VALUE):yamlservices: api: image: my-api-image environment: - NODE_ENV=development - API_KEY=your_api_key_here - DATABASE_URL=postgresql://user:pass@db_service_name:5432/mydbDạng map (dictionary
KEY: VALUE):yamlservices: api: image: my-api-image environment: NODE_ENV: development API_KEY: your_api_key_here # Giá trị có thể là số, boolean, hoặc string (nên để trong "" nếu có ký tự đặc biệt) DEBUG_MODE: "true"Tham chiếu từ file
.env(file tên.envnằm cùng cấp vớidocker-compose.yml): Docker Compose tự động đọc file.envvà các biến trong đó có thể được sử dụng trongdocker-compose.ymlbằng cú pháp${VARIABLE_NAME}.bash# .env file POSTGRES_USER=mysecretuser POSTGRES_PASSWORD=supersecretpassword123 WEB_PORT=8000yaml# docker-compose.yml services: db: image: postgres environment: POSTGRES_USER: ${POSTGRES_USER} # Tham chiếu từ .env POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} # POSTGRES_DB: ${POSTGRES_DB:-default_db_name} # Cú pháp với giá trị mặc định web: image: my_web_app ports: - "${WEB_PORT}:3000"
env_file
Chỉ định một hoặc nhiều file chứa biến môi trường để load vào container. Mỗi dòng trong file phải theo định dạng KEY=VALUE.
services:
api:
image: my-api
env_file:
- ./common.env # Đường dẫn tới file env chung
- ./api.prod.env # File env cụ thể cho production (ghi đè giá trị từ common.env nếu trùng key)
# - .env # Có thể load cả file .env mặc địnhThứ tự ưu tiên của biến môi trường:
- Giá trị được set trong
docker-compose.yml(phầnenvironment). - Giá trị được truyền qua CLI (
docker-compose run -e KEY=VAL ...). - Giá trị từ
env_file. - Giá trị từ file
.env(nếu dùng${VAR}substitution). - Giá trị mặc định trong image (từ
ENVtrong Dockerfile).
depends_on
Chỉ định thứ tự khởi động và phụ thuộc giữa các services. Service A depends_on Service B nghĩa là Docker Compose sẽ đảm bảo Service B được khởi động trước Service A. Lưu ý quan trọng: depends_on chỉ đảm bảo container của service phụ thuộc đã khởi động, không đảm bảo ứng dụng bên trong container đó đã sẵn sàng để nhận kết nối (ví dụ: database cần thời gian để initialize, web server cần thời gian để load).
services:
api:
build: ./api_app
depends_on:
- db # api sẽ khởi động sau khi container 'db' đã khởi động
- redis # và sau khi container 'redis' đã khởi động
db:
image: postgres
redis:
image: redisĐể xử lý việc "chờ service sẵn sàng", bạn thường cần:
Sử dụng
healthcheck(xem bên dưới).depends_oncó thể được cấu hình để chờ service phụ thuộc thànhhealthy.yamlservices: api: build: ./api depends_on: db: condition: service_healthy # Chờ db báo healthy db: image: postgres healthcheck: # Cấu hình healthcheck cho db test: ["CMD-SHELL", "pg_isready -U postgres"] interval: 10s timeout: 5s retries: 5Hoặc dùng các script như
wait-for-it.shhoặcdockerizebên trong entrypoint/command của service phụ thuộc.
networks
Cho phép services kết nối với nhau.
- Mặc định: Docker Compose tự động tạo một default bridge network cho tất cả services trong file. Tên network thường là
<project_name>_default(project_name là tên thư mục chứadocker-compose.yml). Các services trong cùng network này có thể giao tiếp với nhau bằng tên service. - Custom networks: Bạn có thể định nghĩa network riêng để kiểm soát tốt hơn.
services:
frontend:
image: nginx
networks: # Kết nối service 'frontend' vào network 'front-tier'
- front-tier
api:
image: my-api
networks: # Kết nối service 'api' vào cả 'front-tier' và 'back-tier'
- front-tier
- back-tier
db:
image: postgres
networks: # Kết nối service 'db' chỉ vào 'back-tier' (tăng bảo mật)
- back-tier
# Khai báo top-level networks
networks:
front-tier:
driver: bridge # Mặc định
back-tier:
driver: bridge
# internal: true # (Tùy chọn) Nếu true, network này không có kết nối ra ngoài(Sẽ nói chi tiết hơn ở phần Docker Networking)
command
Ghi đè lệnh CMD mặc định được định nghĩa trong Dockerfile của image.
services:
worker:
image: my-worker-image
command: ["python", "process_queue.py", "--verbose"] # Ghi đè CMD của image
# command: /app/start-worker.sh # Dạng shellentrypoint
Ghi đè ENTRYPOINT mặc định được định nghĩa trong Dockerfile của image.
services:
app:
image: my-app-image
entrypoint: /usr/local/bin/custom-entrypoint.sh
# entrypoint: ["python", "manage.py"]
# command: ["runserver", "0.0.0.0:8000"] # command trở thành đối số cho entrypoint mớiLưu ý: Nếu bạn ghi đè entrypoint, command sẽ trở thành đối số cho entrypoint mới đó. Nếu bạn chỉ ghi đè command, nó sẽ là đối số cho entrypoint gốc của image (hoặc là lệnh chính nếu image không có entrypoint).
restart
Chính sách khởi động lại container nếu nó bị dừng hoặc lỗi.
no: (Mặc định) Không tự khởi động lại.always: Luôn khởi động lại container nếu nó dừng, trừ khi bị dừng một cách tường minh (bằngdocker stophoặcdocker-compose stop).on-failure: Chỉ khởi động lại nếu container thoát với exit code khác 0 (lỗi).on-failure:5: Khởi động lại tối đa 5 lần.
unless-stopped: Luôn khởi động lại, trừ khi container bị dừng tường minh bởi người dùng hoặc Docker daemon bị dừng/khởi động lại.
services:
backend:
image: my-backend
restart: always # Luôn cố gắng chạy service này
worker:
image: my-worker
restart: on-failure # Khởi động lại nếu worker bị lỗihealthcheck
Cấu hình kiểm tra "sức khỏe" cho service, tương tự HEALTHCHECK trong Dockerfile. Docker Compose sẽ sử dụng thông tin này để biết service có đang hoạt động bình thường không. Hữu ích khi kết hợp với depends_on và trong các môi trường orchestration.
services:
db:
image: postgres:14
healthcheck:
test:
[
"CMD-SHELL",
"pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB || exit 1",
]
interval: 15s # Kiểm tra mỗi 15 giây
timeout: 5s # Chờ tối đa 5 giây cho lệnh test hoàn thành
retries: 3 # Thử lại 3 lần nếu thất bại
start_period: 30s # Thời gian chờ ban đầu trước khi bắt đầu healthcheck (cho DB có thời gian khởi tạo)
environment:
POSTGRES_USER: user
POSTGRES_DB: appdbTrạng thái healthcheck có thể xem bằng docker ps hoặc docker inspect.
expose
Expose ports của container chỉ cho các services khác trong cùng network, không publish ra host. Hữu ích khi bạn có một service nội bộ (ví dụ: database) không cần truy cập từ bên ngoài host, nhưng các service khác trong Compose stack cần kết nối đến nó.
services:
db:
image: mysql:8.0
expose:
- "3306" # Các service khác trong cùng network có thể kết nối đến 'db:3306'
# ports: # Không dùng 'ports' nếu không muốn map ra host
# - "3306:3306" # Điều này sẽ map ra hostThực tế, với default network của Docker Compose, các service đã có thể giao tiếp với nhau qua tên service và port mà ứng dụng trong container lắng nghe, ngay cả khi không có expose. expose chủ yếu mang tính tài liệu hóa hoặc khi dùng với các network driver khác.
extends
Cho phép chia sẻ cấu hình chung giữa các services hoặc giữa các file Compose khác nhau.
# common-services.yml
version: "3.8"
services:
base_app:
image: alpine
environment:
COMMON_VAR: "shared_value"
logging:
driver: "json-file"
options:
max-size: "200k"
max-file: "10"# docker-compose.yml
version: "3.8"
services:
web:
extends:
file: common-services.yml # File chứa cấu hình chung
service: base_app # Tên service trong file đó để kế thừa
build: ./web_app
ports:
- "80:8000"
environment: # Ghi đè hoặc thêm biến môi trường
APP_SPECIFIC_VAR: "web_value"
worker:
extends:
file: common-services.yml
service: base_app
build: ./worker_app
environment:
APP_SPECIFIC_VAR: "worker_value"secrets và configs
(Nâng cao, thường dùng với Docker Swarm, nhưng Compose cũng hỗ trợ ở mức độ nào đó cho local dev)
secrets: Quản lý dữ liệu nhạy cảm (passwords, API keys). Secrets được mount vào container dưới dạng file trong/run/secrets/(read-only).configs: Quản lý file cấu hình không nhạy cảm. Tương tự secrets, được mount vào container.
version: "3.8"
services:
myapp:
image: myapp:latest
secrets:
- db_password
configs:
- app_config
secrets:
db_password:
file: ./db_password.txt # File trên host chứa password
# external: true # Nếu secret đã được tạo trong Docker
configs:
app_config:
file: ./app_config.json
# external: trueTrong container, db_password sẽ có tại /run/secrets/db_password và app_config tại /run/configs/app_config (hoặc tên file gốc nếu dùng target).
Ví dụ docker-compose.yml đơn giản
Ứng dụng gồm một web app (Node.js) và một Redis instance.
version: "3.8" # Luôn khai báo version
services:
# Service 1: Web application (ví dụ: Node.js app)
web:
build: ./app # Thư mục chứa Dockerfile của app (ví dụ: ./app/Dockerfile)
image: myusername/my-web-app:1.0 # (Tùy chọn) Tên image sau khi build
container_name: my_web_container # (Tùy chọn) Tên cụ thể cho container
ports:
- "3000:3000" # Map port 3000 của host tới port 3000 của container
volumes:
# Mount source code từ thư mục 'app' trên host vào '/usr/src/app' trong container
# Giúp live reload khi code thay đổi (cho môi trường dev)
- ./app:/usr/src/app
# Mount anonymous volume cho node_modules để không bị ghi đè bởi node_modules trên host (nếu có)
# Điều này đảm bảo node_modules được cài đặt bởi 'RUN npm install' trong Dockerfile được sử dụng.
- /usr/src/app/node_modules
environment:
- NODE_ENV=development
- REDIS_HOST=cache # Tên service của Redis
- REDIS_PORT=6379
depends_on: # web app phụ thuộc vào Redis
- cache # Đảm bảo 'cache' service khởi động trước 'web'
restart: unless-stopped
# Service 2: Redis (caching)
cache: # Tên service là 'cache', web app sẽ kết nối tới Redis qua hostname 'cache'
image: "redis:7-alpine" # Sử dụng image Redis chính thức từ Docker Hub
container_name: my_redis_cache
# ports: # Không nhất thiết phải expose port Redis ra host nếu chỉ app nội bộ dùng
# - "6379:6379" # Ví dụ nếu muốn kết nối từ host vào Redis này để debug
volumes:
- redis_data:/data # Sử dụng named volume 'redis_data' để lưu trữ dữ liệu Redis bền bỉ
restart: always
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 3
# Khai báo named volumes ở top-level
volumes:
redis_data:# Docker Compose sẽ tự tạo volume này (nếu chưa có) với tên project_redis_data
# driver: local # (Mặc định)Cấu trúc thư mục ví dụ cho ví dụ trên:
my_project/
├── docker-compose.yml
├── .env # (Tùy chọn) Chứa các biến môi trường chung
└── app/
├── Dockerfile
├── package.json
└── server.js
└── ... (các file khác của app)Các lệnh Docker Compose cơ bản
Chạy các lệnh này từ thư mục chứa file docker-compose.yml.
docker-compose up: Build (nếubuildđược định nghĩa và image chưa có hoặc cần build lại), tạo và khởi động tất cả các services. Logs của tất cả services sẽ được stream ra terminal. NhấnCtrl+Cđể dừng.docker-compose up -d: Chạy ở background (detached mode).docker-compose up --build: Luôn build lại images trước khi khởi động (kể cả khi image đã tồn tại).docker-compose up <service_name> [<service_name2>...]: Chỉ khởi động các service được chỉ định (và các dependencies của chúng).
docker-compose down: Dừng và xóa containers, networks, (tùy chọn) volumes.docker-compose down -v: Xóa cả named volumes được định nghĩa trongvolumessection (và anonymous volumes). CẨN THẬN: Mất dữ liệu trong volumes!docker-compose down --rmi all: Xóa cả images được build bởi Compose.docker-compose down --remove-orphans: Xóa các container không còn được định nghĩa trong compose file.
docker-compose ps: Liệt kê trạng thái của các containers do Compose quản lý cho project hiện tại.docker-compose logs <service_name>: Xem logs của một service cụ thể.docker-compose logs -f <service_name>: Theo dõi logs (live stream).docker-compose logs --tail=50 <service_name>: Xem 50 dòng log cuối.docker-compose logs: Xem logs của tất cả services.
docker-compose exec <service_name> <command>: Chạy một lệnh bên trong một service đang chạy.- Ví dụ:
docker-compose exec web bash(mở bash shell trong serviceweb). - Ví dụ:
docker-compose exec db psql -U myuser -d mydb
- Ví dụ:
docker-compose build <service_name>: Build (hoặc rebuild) image cho một hoặc nhiều service. Nếu không cóservice_name, build tất cả.docker-compose build --no-cache <service_name>: Build không dùng cache.
docker-compose pull <service_name>: Pull image mới nhất cho một hoặc nhiều service (nếu service đó dùngimage:).docker-compose start <service_name>: Khởi động các service đã được tạo nhưng đang dừng.docker-compose stop <service_name>: Dừng các service đang chạy mà không xóa chúng.docker-compose restart <service_name>: Khởi động lại các service.docker-compose rm <service_name>: Xóa các container đã dừng của service.docker-compose rm -f <service_name>: Xóa kể cả đang chạy.
docker-compose run --rm <service_name> <command>: Chạy một container "one-off" cho một service (thường để chạy task, test).--rmtự xóa sau khi chạy xong. Lệnh này sẽ không map ports đã định nghĩa trongports(trừ khi dùng--service-ports).- Ví dụ:
docker-compose run --rm web npm test
- Ví dụ:
docker-compose config: Kiểm tra cú pháp filedocker-compose.ymlvà hiển thị cấu hình đã được tính toán (sau khi đã xử lýextends,env_file, biến môi trường, etc.). Rất hữu ích để debug file compose.docker-compose -f docker-compose.yml -f docker-compose.override.yml config: Xem cấu hình kết hợp từ nhiều file.
docker-compose top <service_name>: Hiển thị các process đang chạy trong service.
(Lưu ý: Nếu bạn dùng Docker Compose V2, các lệnh sẽ là docker compose ... thay vì docker-compose ...)
4. 🔗 Docker Networking (với Compose)
Docker Compose giúp việc quản lý network cho ứng dụng đa-container trở nên rất đơn giản.
Mạng mặc định (Default Bridge Network)
- Khi bạn chạy
docker-compose uplần đầu cho một project (một thư mục chứadocker-compose.yml), Compose sẽ tự động tạo một user-defined bridge network mặc định cho ứng dụng đó. - Tên của network này thường được đặt theo quy tắc:
<project_name>_default.<project_name>là tên của thư mục chứadocker-compose.yml(ví dụ: nếu thư mục làmy_app, network sẽ làmy_app_default). Bạn có thể ghi đè tên project bằng option-p <custom_project_name>hoặc biến môi trườngCOMPOSE_PROJECT_NAME.
- Tất cả các
servicesđược định nghĩa trong filedocker-compose.ymlsẽ tự động được kết nối vào network mặc định này.
Kết nối giữa các services (Service Discovery)
- Đây là một trong những tính năng mạnh mẽ nhất của Docker Compose networking.
- Bên trong network này, các containers (services) có thể tham chiếu và kết nối lẫn nhau bằng tên service đã định nghĩa trong
docker-compose.yml. - Docker Engine cung cấp một DNS server nội bộ cho network này. Khi service
webmuốn kết nối đến servicedb, nó chỉ cần dùng hostname làdb. Docker DNS sẽ tự động phân giải (resolve) têndbthành địa chỉ IP nội bộ của container đang chạy servicedb.- Ví dụ: Trong code của service
web(Node.js), chuỗi kết nối database có thể là:const dbUrl = "postgres://user:password@db:5432/mydb";(Vớidblà tên service của PostgreSQL trongdocker-compose.yml).
- Ví dụ: Trong code của service
- Bạn không cần phải
expose(hayports) port của service nội bộ (như database, redis) ra host machine nếu chỉ có các service khác trong cùng Compose network cần truy cập. Điều này giúp tăng cường bảo mật. Chỉportsnhững service cần truy cập từ bên ngoài (thường là web server/frontend).
Sơ đồ minh họa:
Host Machine (Ví dụ: IP 192.168.1.100)
---------------------------------------------------------------------------------
| |
| Docker Network: myproject_default (VD: 172.18.0.0/16) |
| ------------------------------------------------------------------------- |
| | Service: web (Container A) | |
| | - IP nội bộ: 172.18.0.2 | |
| | - Code kết nối: connect_to('api:5000'), connect_to('db:5432') | |
| | - Mapped port: Host:8080 -> Container:80 (Truy cập từ ngoài vào đây) | |
| ------------------------------------------------------------------------- |
| | Service: api (Container B) | |
| | - IP nội bộ: 172.18.0.3 | |
| | - Lắng nghe trên port 5000 (nội bộ) | |
| | - Code kết nối: connect_to('db:5432') | |
| ------------------------------------------------------------------------- |
| | Service: db (Container C) | |
| | - IP nội bộ: 172.18.0.4 | |
| | - Lắng nghe trên port 5432 (nội bộ, không map ra host) | |
| ------------------------------------------------------------------------- |
| |
---------------------------------------------------------------------------------
Người dùng truy cập http://localhost:8080 hoặc http://192.168.1.100:8080
-> Host OS chuyển đến Container A (web) port 80
-> Container A (web) có thể gọi Container B (api) qua 'api:5000'
-> Container B (api) có thể gọi Container C (db) qua 'db:5432'Custom Networks
Bạn cũng có thể định nghĩa các network tùy chỉnh trong docker-compose.yml để:
- Tạo nhiều network và kết nối các service vào các network khác nhau để cô lập (ví dụ:
frontend_net,backend_net). - Kết nối với các network đã tồn tại bên ngoài Docker Compose.
- Tinh chỉnh driver hoặc options của network.
version: "3.8"
services:
proxy:
image: nginx
networks:
- frontnet # Chỉ kết nối vào frontnet
ports:
- "80:80"
app:
build: ./app
networks:
- frontnet
- backnet # Kết nối vào cả frontnet và backnet
db:
image: postgres
networks:
- backnet # Chỉ kết nối vào backnet
networks:
frontnet:
driver: bridge
# driver_opts:
# com.docker.network.enable_ipv6: "true"
backnet:
driver: bridge
internal: true # Network này không có kết nối ra ngoài Internet, tăng bảo mật cho DB.
# existing_net: # Kết nối tới network đã tồn tại
# external: true
# name: my_preexisting_bridge_networkTrong ví dụ này:
proxyvàappcó thể giao tiếp với nhau quafrontnet.appvàdbcó thể giao tiếp với nhau quabacknet.proxykhông thể trực tiếp giao tiếp vớidb.
5. 💾 Docker Volumes (với Compose)
Tại sao cần Volumes? (Data Persistence)
- Containers là ephemeral (tạm thời, có tính chất "thoáng qua"). Khi một container bị xóa (
docker rmhoặcdocker-compose down), mọi dữ liệu được ghi vào filesystem của nó (lớp writable trên cùng của container) sẽ bị mất vĩnh viễn. - Điều này không thành vấn đề với các stateless application server, nhưng lại là thảm họa đối với:
- Databases: Dữ liệu là tài sản quý giá.
- User uploads: Hình ảnh, tài liệu người dùng tải lên.
- Logs quan trọng: Cần lưu trữ để phân tích, gỡ lỗi.
- File cấu hình: Cần được duy trì qua các lần khởi động lại container.
- Volumes là cơ chế của Docker để lưu trữ dữ liệu một cách bền bỉ (persistently), tách biệt khỏi vòng đời của container. Dữ liệu trong volume vẫn tồn tại ngay cả khi container sử dụng nó bị xóa và tạo lại.
Các loại Volumes trong Docker
Docker hỗ trợ một số loại volumes/mounts:
Bind Mounts:
- Khái niệm: Mount (gắn) một file hoặc thư mục từ filesystem của máy host vào một đường dẫn bên trong container.
- Đặc điểm:
- Rất tiện cho development: Bạn sửa code trên host, thay đổi đó được phản ánh ngay lập tức vào container, hỗ trợ live-reloading.
- Dữ liệu được quản lý trực tiếp trên host.
- Đường dẫn trên host phải tồn tại (hoặc Docker sẽ tạo thư mục rỗng).
- Có thể gây vấn đề về performance trên một số OS (macOS, Windows do cơ chế chia sẻ file).
- Vấn đề về quyền (permissions): UID/GID của file trên host có thể không khớp với user bên trong container, gây lỗi permission denied.
- Cú pháp trong Compose:
- ./path/on/host:/path/in/containerhoặc- ./path/on/host:/path/in/container:ro(read-only).
Named Volumes (hoặc chỉ là "Volumes"):
- Khái niệm: Volumes được Docker quản lý hoàn toàn. Dữ liệu được lưu trữ trong một phần đặc biệt của host filesystem do Docker quản lý (thường là
/var/lib/docker/volumes/trên Linux). Bạn không cần quan tâm đến vị trí chính xác này. - Đặc điểm:
- Cách được khuyến khích để lưu trữ dữ liệu ứng dụng bền bỉ (VD: database data, logs).
- Độc lập với cấu trúc thư mục của host.
- Dễ dàng backup, migrate, chia sẻ giữa các containers.
- Performance tốt hơn bind mounts trên macOS và Windows.
- Docker tự tạo volume nếu chưa tồn tại.
- Có thể được quản lý bằng các lệnh
docker volume ...(create, ls, inspect, rm, prune).
- Cú pháp trong Compose:
- volume_name:/path/in/container.volume_nameđược khai báo ở top-levelvolumes:section.
- Khái niệm: Volumes được Docker quản lý hoàn toàn. Dữ liệu được lưu trữ trong một phần đặc biệt của host filesystem do Docker quản lý (thường là
Anonymous Volumes:
- Khái niệm: Tương tự named volumes, nhưng Docker tự gán một tên ngẫu nhiên (một chuỗi hash dài) cho volume đó.
- Đặc điểm:
- Khó tham chiếu lại nếu bạn không biết tên ngẫu nhiên của nó.
- Thường được tạo khi bạn chỉ định đường dẫn container trong
Dockerfile(VOLUME /app/data) hoặc trongdocker-compose.yml(- /app/data) mà không đặt tên cho volume. - Dữ liệu vẫn bền bỉ, nhưng khó quản lý hơn named volumes.
- Sẽ bị xóa bởi
docker-compose down -vhoặcdocker volume prune.
- Cú pháp trong Compose (ít dùng trực tiếp):
- /path/in/container
tmpfs Mounts (Linux only):
Khái niệm: Mount một thư mục vào bộ nhớ RAM của container, không ghi xuống đĩa.
Đặc điểm:
- Dữ liệu rất nhanh nhưng hoàn toàn không bền bỉ. Mất khi container dừng.
- Hữu ích cho việc lưu trữ file tạm thời, nhạy cảm mà bạn không muốn ghi ra disk.
Cú pháp trong Compose:
yamlservices: myservice: image: ... tmpfs: /app/tmp # Mount /app/tmp vào RAM # tmpfs: # - /run # - /tmp:size=100m,mode=755 # Có thể set size và mode
Khi nào dùng cái nào?
- Named Volumes: Ưu tiên hàng đầu cho dữ liệu ứng dụng cần tính bền bỉ và độc lập với host (database data, user uploads, logs quan trọng, state của ứng dụng).
- Bind Mounts:
- Source code trong môi trường development: Để live-reloading, thay đổi code trên host và thấy ngay kết quả trong container.
- File cấu hình (config files): Mount file config từ host vào container để dễ dàng thay đổi mà không cần build lại image.
- Chia sẻ file giữa host và container: Ví dụ, build artifacts từ container ra host.
- Anonymous Volumes: Tránh sử dụng chủ động. Nếu bạn cần dữ liệu bền bỉ, hãy dùng named volume. Một trường hợp sử dụng phổ biến là
- /app/node_modulesđể ngăn bind mount của source code ghi đè lênnode_modulesđã đượcRUN npm installtrong image. - tmpfs Mounts: Cho dữ liệu tạm thời, không cần lưu trữ, hoặc dữ liệu nhạy cảm.
Khai báo và sử dụng Volumes trong Compose
Đã được minh họa một phần ở trên.
Khai báo Named Volumes ở top-level
volumes:section: Đây là nơi bạn định nghĩa các named volumes mà các services của bạn sẽ sử dụng.yamlversion: "3.8" # ... services ... volumes: postgres_data: # Tên volume bạn sẽ tham chiếu trong service driver: local # (Mặc định) Driver cho volume, thường là 'local' # external: true # Đặt là true nếu volume này đã được tạo bên ngoài compose (bằng docker volume create) # name: my_actual_external_volume_name # Tên thực tế của external volume # driver_opts: # type: "nfs" # o: "addr=192.168.1.1,rw" # device: ":/path/to/nfs/share" mysql_data: app_uploads:Sử dụng Volumes trong
services.<service_name>.volumes: Trong mỗi service, bạn khai báo các volumes mà nó sẽ sử dụng.yamlservices: db_postgres: image: postgres:14 volumes: # Sử dụng named volume 'postgres_data' đã khai báo ở top-level # Mount vào /var/lib/postgresql/data bên trong container - postgres_data:/var/lib/postgresql/data # Bind mount file cấu hình từ host - ./config/postgres/custom.conf:/etc/postgresql/postgresql.conf:ro # Bind mount script khởi tạo DB (chạy một lần) - ./init-db.sh:/docker-entrypoint-initdb.d/init-db.sh db_mysql: image: mysql:8 volumes: - mysql_data:/var/lib/mysql # Named volume web_app: build: ./my_web_app_src volumes: # Bind mount source code cho development - ./my_web_app_src:/usr/src/app # Named volume cho user uploads - app_uploads:/usr/src/app/public/uploads # Anonymous volume để bảo vệ node_modules trong image - /usr/src/app/node_modules
6. 🛠️ Thực Hành: Xây Dựng Ứng Dụng Web + Database + Cache với Docker Compose
Mục tiêu: Tạo một ứng dụng web Node.js/Express đơn giản có các tính năng:
- Hiển thị một bộ đếm số lượt truy cập trang.
- Lưu trữ và truy xuất bộ đếm này từ Redis (cache).
- Nếu Redis không có, hoặc khi khởi động lại, sẽ lấy giá trị từ PostgreSQL (database) và cập nhật vào Redis.
- Khi trang được truy cập, tăng bộ đếm, lưu vào Redis và (bất đồng bộ) vào PostgreSQL.
Cấu trúc thư mục dự kiến:
compose_advanced_practice/
├── docker-compose.yml
├── .env # Chứa credentials cho DB
└── web_app/
├── Dockerfile
├── package.json
├── server.js
└── init_db.sql # (Tùy chọn) Script SQL để khởi tạo bảng1. web_app/package.json: (Chạy cd web_app && npm init -y && npm install express redis pg)
{
"name": "web-app-db-redis",
"version": "1.0.0",
"description": "Node.js app with Postgres and Redis",
"main": "server.js",
"scripts": {
"start": "node server.js",
"dev": "nodemon server.js"
},
"dependencies": {
"express": "^4.18.1",
"redis": "^4.6.7", // Redis client v4 sử dụng Promises
"pg": "^8.11.0" // PostgreSQL client
},
"devDependencies": {
"nodemon": "^2.0.15" // (Tùy chọn) Cho live reload khi dev
}
}2. web_app/server.js:
// web_app/server.js
const express = require("express");
const redis = require("redis");
const { Pool } = require("pg");
const app = express();
const PORT = process.env.PORT || 3000;
// --- Cấu hình Redis Client (v4) ---
const redisClient = redis.createClient({
socket: {
host: process.env.REDIS_HOST || "localhost", // Sẽ là 'cache_service' từ docker-compose
port: process.env.REDIS_PORT || 6379,
},
});
redisClient.on("error", (err) => console.error("Redis Client Error", err));
(async () => {
await redisClient.connect();
console.log("Connected to Redis!");
})();
// --- Cấu hình PostgreSQL Client ---
const pgPool = new Pool({
user: process.env.POSTGRES_USER,
host: process.env.POSTGRES_HOST, // Sẽ là 'db_service' từ docker-compose
database: process.env.POSTGRES_DB,
password: process.env.POSTGRES_PASSWORD,
port: process.env.POSTGRES_PORT || 5432,
});
pgPool.on("connect", () => console.log("Connected to PostgreSQL!"));
pgPool.on("error", (err) => console.error("PostgreSQL Client Error", err));
// --- Hàm khởi tạo bảng nếu chưa có ---
async function initializeDatabase() {
try {
await pgPool.query(`
CREATE TABLE IF NOT EXISTS visit_counts (
id VARCHAR(255) PRIMARY KEY DEFAULT 'page_visits',
count INTEGER NOT NULL DEFAULT 0
);
`);
// Chèn dòng mặc định nếu chưa có
await pgPool.query(`
INSERT INTO visit_counts (id, count)
VALUES ('page_visits', 0)
ON CONFLICT (id) DO NOTHING;
`);
console.log("Database table 'visit_counts' initialized or already exists.");
} catch (err) {
console.error("Error initializing database table:", err);
}
}
// --- Logic chính ---
const COUNTER_KEY = "page_visits_counter";
app.get("/", async (req, res) => {
let visits = 0;
try {
// 1. Thử lấy từ Redis
const redisVisits = await redisClient.get(COUNTER_KEY);
if (redisVisits !== null) {
visits = parseInt(redisVisits, 10);
} else {
// 2. Nếu không có trong Redis, lấy từ DB
const dbResult = await pgPool.query(
"SELECT count FROM visit_counts WHERE id = 'page_visits'"
);
if (dbResult.rows.length > 0) {
visits = dbResult.rows[0].count;
} else {
// Fallback nếu DB cũng chưa có (dù init_db nên xử lý việc này)
visits = 0;
await pgPool.query(
"INSERT INTO visit_counts (id, count) VALUES ('page_visits', 0) ON CONFLICT (id) DO UPDATE SET count = 0"
);
}
// Cập nhật vào Redis
await redisClient.set(COUNTER_KEY, visits);
console.log("Loaded visits from DB to Redis:", visits);
}
// 3. Tăng bộ đếm
visits++;
// 4. Lưu lại vào Redis
await redisClient.set(COUNTER_KEY, visits);
// 5. (Bất đồng bộ) Lưu vào DB
pgPool
.query("UPDATE visit_counts SET count = $1 WHERE id = 'page_visits'", [
visits,
])
.catch((err) => console.error("Error updating DB:", err));
res.send(
`<h1>Hello Docker Universe!</h1><p>This page has been visited ${visits} times.</p><p>Counter updated in Redis and (eventually) PostgreSQL.</p>`
);
} catch (error) {
console.error("Error processing request:", error);
res.status(500).send("Oops, something went wrong! Check server logs.");
}
});
// --- Khởi động server ---
app.listen(PORT, async () => {
await initializeDatabase(); // Đảm bảo bảng tồn tại khi server khởi động
console.log(`Web app listening on port ${PORT}`);
});3. web_app/Dockerfile:
FROM node:18-alpine
WORKDIR /usr/src/app
# Copy package.json và package-lock.json (nếu có)
COPY package*.json ./
# Cài đặt dependencies (bao gồm devDependencies nếu không có --production)
# Trong môi trường dev, có thể muốn cả devDependencies (như nodemon)
RUN npm install
# Copy toàn bộ source code
COPY . .
# Expose port mà ứng dụng chạy
EXPOSE 3000
# Lệnh để chạy ứng dụng (sử dụng nodemon nếu có trong devDependencies và là môi trường dev)
# Bạn có thể truyền biến môi trường NODE_ENV từ docker-compose.yml
# CMD if [ "$NODE_ENV" = "development" ]; then npm run dev; else npm start; fi
# Hoặc đơn giản hơn:
CMD [ "npm", "start" ]4. web_app/init_db.sql (Tùy chọn, nếu bạn muốn Postgres tự chạy script này):
-- web_app/init_db.sql
-- Script này không cần thiết nếu server.js tự tạo bảng, nhưng là một cách khác.
CREATE TABLE IF NOT EXISTS visit_counts (
id VARCHAR(255) PRIMARY KEY DEFAULT 'page_visits',
count INTEGER NOT NULL DEFAULT 0
);
INSERT INTO visit_counts (id, count)
VALUES ('page_visits', 0)
ON CONFLICT (id) DO NOTHING;
-- Bạn có thể thêm các bảng khác ở đây
-- CREATE TABLE IF NOT EXISTS users (...);5. .env file (trong thư mục compose_advanced_practice):
# .env
# Credentials cho PostgreSQL
POSTGRES_USER=dockeruser
POSTGRES_PASSWORD=dockerpass
POSTGRES_DB=appdb
POSTGRES_PORT=5432 # Port bên trong container DB
# Config cho App
NODE_ENV=development
PORT=3000 # Port app lắng nghe bên trong container
# Config cho Redis (không cần credentials cho ví dụ này)
REDIS_HOST=cache_service # Tên service của Redis trong docker-compose
REDIS_PORT=63796. docker-compose.yml (trong thư mục compose_advanced_practice):
version: "3.8"
services:
# Web Application Service
app:
build: ./web_app
container_name: node_app_service
ports:
- "${PORT}:${PORT}" # Sử dụng biến PORT từ .env cho host, map tới PORT trong container
volumes:
- ./web_app:/usr/src/app # Mount source code cho live reload
- /usr/src/app/node_modules # Anonymous volume để giữ node_modules của image
environment: # Truyền các biến môi trường cần thiết cho app
NODE_ENV: ${NODE_ENV}
PORT: ${PORT}
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB}
POSTGRES_HOST: db_service # Tên service của PostgreSQL
POSTGRES_PORT: ${POSTGRES_PORT} # Port PostgreSQL lắng nghe BÊN TRONG network
REDIS_HOST: ${REDIS_HOST} # Tên service của Redis
REDIS_PORT: ${REDIS_PORT}
depends_on: # App phụ thuộc vào db và cache
db_service:
condition: service_healthy # Chờ db_service báo healthy
cache_service:
condition: service_healthy # Chờ cache_service báo healthy
restart: unless-stopped
# PostgreSQL Database Service
db_service:
image: postgres:14-alpine
container_name: postgres_db_service
environment: # Các biến này được image postgres sử dụng để khởi tạo DB
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB}
volumes:
- postgres_app_data:/var/lib/postgresql/data # Named volume cho dữ liệu Postgres
# - ./web_app/init_db.sql:/docker-entrypoint-initdb.d/init.sql # (Tùy chọn) Mount script SQL khởi tạo
ports: # Chỉ map ra host nếu bạn cần truy cập DB từ bên ngoài (VD: bằng pgAdmin)
- "5433:5432" # Host port 5433 -> Container port 5432 (tránh xung đột nếu host đã có Postgres chạy ở 5432)
healthcheck:
test:
[
"CMD-SHELL",
"pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB -q || exit 1",
]
interval: 10s
timeout: 5s
retries: 5
start_period: 20s # Cho Postgres thời gian khởi tạo
restart: always
# Redis Cache Service
cache_service:
image: redis:7-alpine
container_name: redis_cache_service
volumes:
- redis_app_data:/data # Named volume cho dữ liệu Redis (nếu Redis được cấu hình để persist)
# ports: # Chỉ map ra host nếu bạn cần truy cập Redis từ bên ngoài (VD: bằng redis-cli từ host)
# - "6380:6379" # Host port 6380 -> Container port 6379
healthcheck:
test: ["CMD", "redis-cli", "-h", "localhost", "-p", "6379", "ping"]
interval: 10s
timeout: 3s
retries: 5
restart: always
# Top-level named volumes
volumes:
postgres_app_data:
redis_app_data:Chạy ứng dụng:
- Chuẩn bị:
- Đảm bảo bạn đã tạo các file và thư mục như cấu trúc trên.
- Trong thư mục
web_app, chạynpm init -yvànpm install express redis pg nodemon(nếu bạn muốnnodemon).
- Khởi động: Mở terminal trong thư mục
compose_advanced_practice(chứadocker-compose.yml). Chạy:docker-compose up --build(hoặcdocker compose up --buildcho V2).-dđể chạy ở background:docker-compose up -d --build. - Kiểm tra:
- Mở trình duyệt và truy cập
http://localhost:3000(hoặc port bạn đặt trong.env). - Mỗi lần refresh, bộ đếm sẽ tăng.
- Kiểm tra logs:
docker-compose logs app,docker-compose logs db_service,docker-compose logs cache_service. - (Nếu map port DB/Redis ra host) Bạn có thể kết nối tới PostgreSQL (qua port 5433) hoặc Redis (qua port 6380) từ máy host để kiểm tra dữ liệu.
- Mở trình duyệt và truy cập
- Thử nghiệm tính bền bỉ và cache:
- Refresh trang vài lần.
- Dừng và khởi động lại toàn bộ stack:
docker-compose down(KHÔNG dùng-vnếu muốn giữ data DB) rồidocker-compose up. Bộ đếm nên được lấy từ DB và tiếp tục. - Nếu bạn dừng Redis (
docker-compose stop cache_service), ứng dụng vẫn nên hoạt động (lấy từ DB). Khởi động lại Redis (docker-compose start cache_service), nó sẽ được cập nhật lại.
- Dọn dẹp:
docker-compose downĐể xóa cả volumes (mất dữ liệu DB và Redis):docker-compose down -v
7. ✨ Best Practices & Mẹo
Dockerfile Best Practices (Nhắc lại và bổ sung)
Sử dụng base image chính thức và nhỏ gọn (official & slim images): Ưu tiên các image
alpine(VD:node:18-alpine,python:3.10-alpine) vì chúng nhỏ, giảm attack surface.Sử dụng tag cụ thể cho base image: Tránh
latest(VD:node:18.17.0-alpinethay vìnode:alpinehoặcnode:latest) để đảm bảo build nhất quán và tránh lỗi bất ngờ khilatestthay đổi.Tối ưu hóa thứ tự lệnh để tận dụng caching: Đặt các lệnh ít thay đổi lên trên (VD:
FROM,ENV,RUN apt-get install ...).COPY package.jsonvàRUN npm installnên đặt trướcCOPY . .(vì dependencies ít thay đổi hơn source code).Giảm số lượng layers: Mỗi
RUN,COPY,ADDtạo một layer. Kết hợp các lệnhRUNliên quan bằng&&và\(nối dòng) để giảm số layer, giúp image nhỏ hơn và build nhanh hơn.dockerfileRUN apt-get update && apt-get install -y \ package1 \ package2 \ package3 \ && rm -rf /var/lib/apt/lists/* # Xóa cache của package managerSử dụng
.dockerignorehiệu quả: Loại bỏ các file/folder không cần thiết (VD:.git,node_modulescủa host,*.log,*.tmp,Dockerfileitself) khỏi build context.Chạy ứng dụng với user không phải root (non-root user): Tăng cường bảo mật. Tạo user/group bằng
RUNvà chuyển sang user đó bằngUSER.dockerfileRUN addgroup -S appgroup && adduser -S appuser -G appgroup USER appuserSử dụng multi-stage builds: Để tạo image production nhỏ gọn, chỉ chứa runtime và artifact đã biên dịch, không chứa build tools, source code gốc, hay dev dependencies.
dockerfile# Stage 1: Build stage (Node.js ví dụ) FROM node:18-alpine AS builder WORKDIR /app COPY package*.json ./ RUN npm ci --only=production COPY . . RUN npm run build # Giả sử có script build (VD: transpile TS, bundle frontend) # Stage 2: Production stage FROM node:18-alpine WORKDIR /app # (Tùy chọn) Tạo non-root user RUN addgroup -S appgroup && adduser -S appuser -G appgroup # Copy chỉ những thứ cần thiết từ builder stage COPY --from=builder /app/node_modules ./node_modules COPY --from=builder /app/dist ./dist # Hoặc ./build tùy vào output của bạn COPY --from=builder /app/package.json ./package.json # Có thể cần cho runtime # (Tùy chọn) Chuyển user # USER appuser EXPOSE 3000 CMD [ "node", "dist/server.js" ] # Chạy từ artifact đã buildCOPYthay vìADDcho file/folder cục bộ:COPYrõ ràng hơn. Chỉ dùngADDkhi bạn cần tính năng tự động giải nén tarball hoặc tải file từ URL (dù tải URL trong Dockerfile cũng không phải là best practice lắm, có thể làm ở entrypoint script).Hiểu rõ
CMDvàENTRYPOINT:ENTRYPOINTđịnh nghĩa executable chính.CMDcung cấp tham số mặc định choENTRYPOINThoặc là lệnh mặc định nếu không cóENTRYPOINT.- Ưu tiên exec form (
["executable", "param1"]).
Không lưu trữ secrets trong image: Dùng biến môi trường truyền vào lúc runtime, hoặc Docker secrets/configs, hoặc các giải pháp quản lý secret chuyên dụng (HashiCorp Vault).
Dọn dẹp sau khi cài đặt: Xóa cache của package manager (
rm -rf /var/lib/apt/lists/*,apk add --no-cache ...), file tạm, source code đã biên dịch không cần thiết trong cùng mộtRUNlayer.
Docker Compose Best Practices
Sử dụng
version: "3.x"(phiên bản mới nhất mà Docker Engine của bạn hỗ trợ, thường là 3.8 hoặc 3.9).Đặt tên services rõ ràng, dễ hiểu. Tên service cũng là hostname cho service discovery.
Sử dụng biến môi trường và file
.env:- Để cấu hình các thông số thay đổi giữa các môi trường (dev, test, prod) như database credentials, API keys, ports.
- Không hardcode credentials trực tiếp trong
docker-compose.yml. - File
.envở thư mục gốc của project được Docker Compose tự động load.
Sử dụng
depends_onvàhealthcheck:depends_onđể kiểm soát thứ tự khởi động.- Kết hợp
depends_onvớicondition: service_healthyvàhealthchecktrong service phụ thuộc để đảm bảo service đó thực sự sẵn sàng trước khi service khác bắt đầu.
Chỉ
ports(map ra host) những service thực sự cần truy cập từ bên ngoài. Các service giao tiếp nội bộ qua network của Compose không cần map port ra host.Sử dụng
volumescho persistent data (named volumes) và source code khi dev (bind mounts).- Đặt tên rõ ràng cho named volumes.
- Cẩn thận với
docker-compose down -v(sẽ xóa named volumes).
Tách biệt cấu hình cho các môi trường (dev, staging, prod):
- Sử dụng nhiều file Compose:
docker-compose.yml(chung),docker-compose.override.yml(cho dev, được load tự động),docker-compose.prod.yml,docker-compose.test.yml. - Sử dụng option
-fđể chỉ định các file compose cần load:docker-compose -f docker-compose.yml -f docker-compose.prod.yml up -d docker-compose.override.ymllà file mặc định được load saudocker-compose.ymlnếu nó tồn tại, rất tiện để ghi đè cấu hình cho môi trường dev (VD: thêm bind mount cho source code, map port debug).
- Sử dụng nhiều file Compose:
Sử dụng
restartpolicies phù hợp:alwayshoặcunless-stoppedcho các service quan trọng (DB, web server),on-failurecho workers.Đặt
container_name(tùy chọn): Giúp container có tên cố định, dễ tham chiếu bằngdockerCLI, nhưng có thể gây xung đột nếu bạn chạy nhiều project Compose giống hệt nhau. Mặc định Compose sẽ tạo tên dạng<project>_<service>_<instance_number>.Sử dụng
profiles(Compose v2.1+): Cho phép định nghĩa các nhóm services tùy chọn, chỉ được khởi động khi profile đó được kích hoạt. Hữu ích để bật/tắt các service không cốt lõi (VD: tool debug, service thử nghiệm).yamlservices: web:# Luôn chạy # ... db:# Luôn chạy # ... mailhog: # Service để test email, chỉ chạy khi profile 'dev-tools' được active image: mailhog/mailhog profiles: ["dev-tools"] ports: - "1025:1025" # SMTP - "8025:8025" # Web UI # Chạy: docker-compose --profile dev-tools up
Sử dụng .dockerignore
Nhắc lại từ phần Dockerfile: file .dockerignore (đặt cùng cấp với Dockerfile) chỉ định các file/folder sẽ KHÔNG được gửi tới Docker daemon khi build image (trong build context). Ví dụ .dockerignore cho một project Node.js:
# Logs and temporary files
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pids
*.pid
*.seed
*.pid.lock
# Build artifacts
dist
build
coverage
.nyc_output
# Dependency directories
node_modules/
jspm_packages/
# Git
.git
.gitignore
.gitattributes
# Editor / OS specific
.vscode/
.idea/
*.swp
*~
.DS_Store
Thumbs.db
# Docker specific
Dockerfile # Không cần copy chính Dockerfile vào image
.dockerignore
# Environment files (nên được quản lý bên ngoài image)
.env
*.env.local
*.env.development.local
*.env.test.local
*.env.production.local
# Optional: Local config files not for image
config/local.jsonĐiều này giúp:
- Giảm kích thước build context -> build nhanh hơn.
- Tránh vô tình copy file nhạy cảm (như
.envchứa credentials) vào image. - Tránh ghi đè file/folder đã được tạo bởi các
RUNcommand trong Dockerfile (VD:node_modulestrong image không bị ghi đè bởinode_modulestrên host nếuCOPY . .được dùng). - Tối ưu hóa Docker build cache: Nếu file không cần thiết thay đổi, nó sẽ không làm mất cache của lệnh
COPY.
Quản lý môi trường (Dev, Staging, Prod)
Sử dụng kết hợp các file docker-compose.yml, docker-compose.override.yml, và các file dành riêng cho môi trường (ví dụ: docker-compose.prod.yml), cùng với biến môi trường.
docker-compose.yml(Base): Chứa cấu hình chung, ổn định cho tất cả các môi trường. Thường là cấu hình gần giống production nhất nhưng không chứa secrets.docker-compose.override.yml(Dev default):- Tự động được load bởi
docker-compose upnếu tồn tại. - Ghi đè/bổ sung cấu hình cho môi trường development.
- Ví dụ:
- Mount source code bằng bind volumes để live reload.
- Map thêm ports (VD: port debug cho Node.js).
- Sử dụng image có tool dev (VD:
node:18-alpinethay vìnode:18-slim). - Thay đổi
commandđể chạy vớinodemonhoặc tool tương tự. - Thêm các service chỉ dùng cho dev (VD:
mailhog,adminer).
- Tự động được load bởi
docker-compose.prod.yml(Production):- Chứa cấu hình riêng cho production.
- Ví dụ:
- Không mount source code.
- Sử dụng image đã được tối ưu cho production (VD: từ multi-stage build).
- Cấu hình
restart: always. - Cấu hình logging phù hợp cho production.
- Sử dụng biến môi trường hoặc secrets cho credentials.
- Chạy:
docker-compose -f docker-compose.yml -f docker-compose.prod.yml up -d
- Biến môi trường:
- Sử dụng file
.envcho các giá trị mặc định hoặc không nhạy cảm. - Trên server production, các biến môi trường nhạy cảm nên được inject bởi hệ thống CI/CD hoặc orchestration platform, không nên commit vào Git.
- Sử dụng file
8. 🏋️ Bài Tập
Yêu cầu: Mở rộng bài tập của phần Docker cơ bản (ứng dụng web tĩnh với Nginx) bằng cách thêm một service backend API đơn giản và quản lý toàn bộ bằng Docker Compose. Frontend sẽ gọi API này để lấy dữ liệu.
Mô tả chi tiết:
- Service 1:
frontend- Sử dụng image
nginx:alpine(hoặchttpd:alpine). - Phục vụ một file
index.htmlvà (tùy chọn)style.css. index.htmlsẽ có một nút. Khi nhấn nút, JavaScript sẽ gọi đến một endpoint API của servicebackend(ví dụ:/api/data) và hiển thị kết quả trả về lên trang.- Nginx cần được cấu hình để proxy các request có tiền tố
/api/tới servicebackend.
- Sử dụng image
- Service 2:
backend(Service mới)- Tạo một ứng dụng API cực kỳ đơn giản bằng Node.js/Express (hoặc Python/Flask, Go,... tùy bạn chọn, nhưng ví dụ sẽ ưu tiên Node.js).
- API này có một endpoint, ví dụ
/data(khi Nginx proxy, nó sẽ là/api/datatừ phía client). Endpoint này trả về một đối tượng JSON, ví dụ:{"message": "Hello from Backend API managed by Docker Compose!", "timestamp": "2023-10-27T10:30:00Z"}. - Viết
Dockerfilecho servicebackendnày.
Nhiệm vụ:
Cấu trúc thư mục dự kiến:
textmy_fullstack_app/ ├── docker-compose.yml ├── .env # (Tùy chọn) Cho port của frontend ├── frontend/ # Service frontend │ ├── Dockerfile # Dockerfile cho Nginx (chủ yếu là COPY file) │ ├── nginx.conf # Cấu hình Nginx để proxy_pass │ └── public/ # Thư mục chứa static files │ ├── index.html │ └── style.css (optional) └── backend/ # Service backend ├── Dockerfile # Dockerfile cho Node.js API ├── package.json ├── server.js # Code API └── ...Tạo service
backend(Node.js/Express):cd backendnpm init -ynpm install expressserver.js:javascriptconst express = require("express"); const app = express(); const PORT = process.env.BACKEND_PORT || 3001; // Port backend lắng nghe BÊN TRONG container app.get("/data", (req, res) => { console.log("Backend /data endpoint hit!"); res.json({ message: "Hello from Backend API managed by Docker Compose!", timestamp: new Date().toISOString(), servedByHost: req.hostname, // Sẽ là container ID hoặc tên nếu không có alias }); }); app.listen(PORT, "0.0.0.0", () => { // Lắng nghe trên tất cả network interfaces trong container console.log(`Backend API listening on port ${PORT}`); });Dockerfile(trongbackend/):dockerfileFROM node:18-alpine WORKDIR /usr/src/app COPY package*.json ./ RUN npm install --production # Chỉ cài production dependencies COPY . . # Biến môi trường BACKEND_PORT có thể được set trong docker-compose.yml # EXPOSE 3001 # Port mà backend lắng nghe (đã được set trong server.js) CMD [ "node", "server.js" ]
Cấu hình Nginx Proxy cho
frontend:frontend/public/index.html:html<!DOCTYPE html> <html> <head> <title>Frontend App</title> <style> body { font-family: sans-serif; } button { padding: 10px; } #result { margin-top: 20px; padding: 10px; border: 1px solid #ccc; background-color: #f9f9f9; } </style> </head> <body> <h1>Welcome to the Frontend!</h1> <button id="fetchDataBtn">Fetch Data from Backend</button> <div id="result">Click the button to see data.</div> <script> document .getElementById("fetchDataBtn") .addEventListener("click", async () => { const resultDiv = document.getElementById("result"); resultDiv.textContent = "Loading..."; try { // Nginx sẽ proxy request /api/data này đến service backend const response = await fetch("/api/data"); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const data = await response.json(); resultDiv.innerHTML = ` <p><strong>Message:</strong> ${data.message}</p> <p><strong>Timestamp:</strong> ${data.timestamp}</p> <p><strong>Served by:</strong> ${data.servedByHost}</p> `; } catch (error) { resultDiv.textContent = "Error fetching data: " + error.message; console.error("Error:", error); } }); </script> </body> </html>frontend/nginx.conf:nginx# frontend/nginx.conf server { listen 80; # Nginx lắng nghe trên port 80 BÊN TRONG container frontend server_name localhost; # Phục vụ static files từ /usr/share/nginx/html (nơi ta sẽ COPY public/ vào) location / { root /usr/share/nginx/html; index index.html index.htm; try_files $uri $uri/ /index.html; # Quan trọng cho SPA nếu có } # Proxy các request /api/ tới service backend location /api/ { # 'backend_service' phải khớp với tên service của backend trong docker-compose.yml # '3001' là port mà backend API lắng nghe BÊN TRONG container của nó proxy_pass http://backend_service:3001/; # Dấu / cuối cùng quan trọng # Các headers này giúp backend biết thông tin gốc của request proxy_set_header Host $host; # Giữ nguyên Host header gốc proxy_set_header X-Real-IP $remote_addr; # IP của client proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; # Danh sách IP nếu qua nhiều proxy proxy_set_header X-Forwarded-Proto $scheme; # http hoặc https # (Tùy chọn) Cấu hình timeout # proxy_connect_timeout 60s; # proxy_send_timeout 60s; # proxy_read_timeout 60s; } # (Tùy chọn) Tắt access log hoặc error log cho đỡ nhiễu khi dev # access_log off; # error_log /dev/null; }frontend/Dockerfile:dockerfileFROM nginx:1.25-alpine # Xóa config Nginx mặc định RUN rm /etc/nginx/conf.d/default.conf # Copy file nginx.conf tùy chỉnh vào vị trí config của Nginx COPY nginx.conf /etc/nginx/conf.d/default.conf # Copy toàn bộ nội dung thư mục public/ (chứa index.html, style.css) # vào thư mục phục vụ web mặc định của Nginx COPY ./public/ /usr/share/nginx/html/ # EXPOSE 80 # Image nginx đã làm việc này # CMD ["nginx", "-g", "daemon off;"] # Image nginx đã làm việc này
Viết
docker-compose.yml(trongmy_fullstack_app/):yamlversion: "3.8" services: frontend_service: # Tên service cho frontend build: ./frontend # Đường dẫn đến thư mục chứa Dockerfile của frontend container_name: my_frontend_nginx ports: - "${FRONTEND_PORT:-8080}:80" # Map port từ .env (mặc định 8080) ra port 80 của container frontend volumes: # Mount thư mục public và nginx.conf để live reload khi dev (tùy chọn) - ./frontend/public:/usr/share/nginx/html:ro # :ro cho read-only an toàn hơn - ./frontend/nginx.conf:/etc/nginx/conf.d/default.conf:ro depends_on: - backend_service # Frontend phụ thuộc vào backend restart: unless-stopped backend_service: # Tên service cho backend (khớp với proxy_pass trong nginx.conf) build: ./backend container_name: my_backend_api environment: # BACKEND_PORT: 3001 # Port này đã được set trong server.js, có thể set ở đây để ghi đè NODE_ENV: development # ports: # Không cần map port backend ra host nếu chỉ frontend gọi nội bộ # - "3001:3001" volumes: - ./backend:/usr/src/app # Mount source code backend cho live reload - /usr/src/app/node_modules # Để không bị ghi đè node_modules từ host restart: unless-stopped # (Tùy chọn) Thêm healthcheck cho backend # healthcheck: # test: ["CMD", "curl", "-f", "http://localhost:3001/data"] # Hoặc một endpoint /health đơn giản # interval: 30s # timeout: 10s # retries: 3 # (Tùy chọn) Định nghĩa network nếu muốn custom # networks: # app_network: # driver: bridgeTạo file
.env(trongmy_fullstack_app/) (tùy chọn):envFRONTEND_PORT=8080
Chạy và Kiểm tra:
- Từ thư mục
my_fullstack_app, chạydocker-compose up --build. - Mở trình duyệt, truy cập
http://localhost:8080(hoặc port bạn set trong.env). - Nhấn nút "Fetch Data from Backend". Dữ liệu từ backend API (qua Nginx proxy) sẽ được hiển thị.
- Kiểm tra logs của các service:
docker-compose logs frontend_service,docker-compose logs backend_service. - Thử thay đổi code trong
backend/server.jshoặcfrontend/public/index.html. Nếu bạn đã mount volumes và dùngnodemoncho backend (hoặc trình duyệt tự refresh cho frontend), bạn sẽ thấy thay đổi mà không cần build lại (có thể cần restart service backend nếu không có nodemon).
- Từ thư mục
Dọn dẹp:
docker-compose down
Chúc bạn thành công với bài tập này! Nó sẽ giúp bạn hiểu rõ hơn về cách các service tương tác với nhau trong Docker Compose.
9. 📚 Tài Liệu Tham Khảo & Next Steps
- Docker Official Documentation: https://docs.docker.com/ (Nguồn tài liệu toàn diện nhất)
- Docker Compose CLI Reference: https://docs.docker.com/compose/reference/
- Compose File Reference: https://docs.docker.com/compose/compose-file/
- Best practices for writing Dockerfiles: https://docs.docker.com/develop/develop-images/dockerfile_best-practices/
- Docker Samples (GitHub): https://github.com/dockersamples (Nhiều ví dụ thực tế)
- Awesome Docker (Curated list on GitHub): https://github.com/veggiemonk/awesome-docker
- Play with Docker (Online Docker playground): https://labs.play-with-docker.com/ (Thực hành Docker mà không cần cài đặt)
- Bret Fisher's Docker and Kubernetes Resources: https://www.bretfisher.com/ (Nhiều khóa học và tips hay)
Next Steps (Sau khi nắm vững những kiến thức này):
- Multi-stage builds trong Dockerfile: Tìm hiểu sâu hơn để tối ưu image cho production.
- Quản lý secrets và configurations nâng cao: Nghiên cứu Docker secrets, Docker configs, hoặc các tool như HashiCorp Vault.
- Docker Swarm: Tìm hiểu về công cụ orchestration container tích hợp sẵn của Docker, đơn giản hơn Kubernetes cho các ứng dụng vừa và nhỏ.
- Kubernetes (K8s): "Ông vua" của orchestration container, mạnh mẽ nhưng phức tạp hơn. Cần thiết cho các hệ thống lớn, đòi hỏi tính sẵn sàng cao và khả năng scale mạnh.
- Tích hợp Docker vào CI/CD pipelines: Tự động hóa quá trình build, test, và deploy ứng dụng Dockerized (VD: với Jenkins, GitLab CI, GitHub Actions).
- Khám phá các private registries chuyên sâu: AWS ECR, Google Artifact Registry (GCR cũ), Azure CR, Harbor (tự host).
- Docker Security: Tìm hiểu về các best practice để bảo mật Docker images và containers (quét lỗ hổng, image signing, runtime security).
- Service Mesh (Istio, Linkerd): Cho các ứng dụng microservices phức tạp, quản lý traffic, observability, security giữa các services.
- Infrastructure as Code (Terraform, Pulumi): Kết hợp Docker với các công cụ IaC để quản lý toàn bộ hạ tầng.
