Cách dùng Docker Compose cho Local Dev: Quy trình chuẩn từ A-Z
Docker Compose là gì?
Docker Compose là một công cụ cho phép bạn định nghĩa và chạy nhiều container Docker cùng lúc chỉ với một lệnh duy nhất. Thay vì phải gõ hàng loạt docker run phức tạp, bạn chỉ cần viết một file cấu hình docker-compose.yml mô tả toàn bộ các dịch vụ (web server, database, cache,…) và chạy docker compose up. Đây chính là “chìa khóa” để làm việc chuyên nghiệp với multi‑container trên môi trường local.
Tại sao Docker đơn lẻ là chưa đủ?
Bạn là một developer mới bắt đầu. Bạn đã biết Docker giúp chạy ứng dụng trong container một cách nhẹ nhàng, đồng nhất. Tuyệt vời! Nhưng hãy tưởng tượng một dự án thực tế. Nó không chỉ là một ứng dụng web đơn thuần. Nó là sự kết hợp: một web server (ví dụ Node.js), một cơ sở dữ liệu (ví dụ MySQL), một bộ nhớ đệm (Redis), và có thể là một vài dịch vụ phụ trợ khác.
Với Docker đơn lẻ, để khởi động toàn bộ hệ sinh thái này, bạn sẽ phải gõ một loạt các lệnh docker run dài ngoằng, nhớ chính xác các tham số -p, -v, --network cho từng container. Và rồi khi tắt máy, bạn lại quên mất lệnh nào để chạy lại? Thật là một cơn ác mộng cho local development đúng không?
Đây chính là lúc Docker Compose lên tiếng. Nó cho phép bạn định nghĩa và chạy multi-container Docker applications chỉ với một câu lệnh duy nhất (docker compose up). Thay vì phải nhớ 5-6 dòng lệnh thủ công, bạn chỉ cần viết một file cấu hình duy nhất. Đơn giản, hiệu quả và chuyên nghiệp hơn rất nhiều.
Giải phẫu file docker-compose.yml
Trái tim của Docker Compose nằm trong file docker-compose.yml (có thể đặt tên là compose.yml). Đây là một file YAML, nơi bạn mô tả toàn bộ ứng dụng của mình. Hãy cùng “mổ xẻ” các thành phần quan trọng nhất:
Services
Đây là thành phần bắt buộc và quan trọng nhất. Mỗi service tương ứng với một container. Ví dụ: một service web cho Node.js, một service db cho MySQL. Trong mỗi service, bạn sẽ khai báo image (có thể từ Docker Hub hoặc build từ Dockerfile), port mapping, volume mounts, và các cấu hình khác.
Networks
Mặc định, Docker Compose sẽ tự động tạo một network cho toàn bộ các service trong cùng file. Nhờ đó, các container có thể giao tiếp với nhau bằng tên service như tên miền (ví dụ: từ service web có thể gọi đến db bằng hostname db). Bạn cũng có thể khai báo custom networks để kiểm soát chi tiết hơn.
Volumes
Volumes là cơ chế lưu trữ dữ liệu bền vững. Khi container bị xóa, toàn bộ dữ liệu bên trong cũng mất theo. Đối với database, điều này là không thể chấp nhận. Sử dụng volume, dữ liệu sẽ được lưu trên máy host, an toàn ngay cả khi container restart hay bị xóa.
Environment variables
Bạn có thể truyền biến môi trường vào container bằng hai cách: dùng trực tiếp key environment trong service hoặc dùng env_file để load từ một file .env bên ngoài. Cách thứ hai giúp bạn quản lý cấu hình (như mật khẩu database) sạch sẽ và an toàn hơn, đặc biệt khi dùng chung code qua Git.
Thực hành: Setup môi trường Fullstack trong 5 phút
Đủ lý thuyết rồi, hãy bắt tay vào thực hành. Chúng ta sẽ xây dựng một file docker-compose.yml chuẩn chỉnh cho một ứng dụng Node.js kết nối với MySQL.
Bước 1: Cài đặt Docker Compose
Nếu bạn đã cài Docker Desktop (trên Windows/macOS) hoặc Docker Engine + Docker Compose plugin (trên Linux), bạn đã sẵn sàng. Từ phiên bản Docker mới nhất, lệnh sử dụng là docker compose (không có dấu gạch ngang), là phiên bản V2 được viết bằng Go, có hiệu suất cao hơn và tích hợp sẵn.
⚠️ Lưu ý về xác minh: Trên một số bản phân phối Linux cũ, Docker Compose vẫn tồn tại dưới dạng gói nhị phân riêng với lệnh docker-compose (có dấu gạch ngang). Nếu bạn gặp lỗi “command not found” với docker compose, hãy thử dùng docker-compose (v1). Tuy nhiên, cho các dự án mới, nên cập nhật lên V2 để được hỗ trợ tốt nhất.
Bước 2: Tạo file docker-compose.yml
Tạo một thư mục dự án mới, ví dụ my-fullstack-app. Bên trong, tạo file docker-compose.yml với nội dung sau. Đây là file cấu hình hoàn chỉnh có giải thích chi tiết kèm theo:
services:
# Dịch vụ MySQL Database
mysql_db:
image: mysql:8.0
container_name: mysql_container
# Khởi tạo database mặc định khi container lần đầu chạy
environment:
MYSQL_ROOT_PASSWORD: root_password
MYSQL_DATABASE: app_db
MYSQL_USER: app_user
MYSQL_PASSWORD: app_password
ports:
- "3307:3306"
volumes:
# Mount dữ liệu MySQL ra bên ngoài để không bị mất
- mysql_data:/var/lib/mysql
# Kiểm tra sức khỏe của database trước khi cho app kết nối
healthcheck:
test: ["CMD", "sh", "-c", "mysqladmin ping -h localhost -u root --password=${MYSQL_ROOT_PASSWORD}"]
interval: 10s
timeout: 5s
retries: 5
networks:
- app_network
# Dịch vụ Node.js Backend
node_app:
build:
context: .
dockerfile: Dockerfile
container_name: node_container
ports:
- "3000:3000"
# Mount source code để hỗ trợ live reload
volumes:
- .:/app
# Tránh ghi đè node_modules bên trong container
- /app/node_modules
environment:
NODE_ENV: development
DB_HOST: mysql_db
DB_USER: app_user
DB_PASSWORD: app_password
DB_NAME: app_db
DB_PORT: 3306
# Sử dụng condition: service_healthy để đảm bảo database sẵn sàng
depends_on:
mysql_db:
condition: service_healthy
networks:
- app_network
networks:
app_network:
driver: bridge
volumes:
mysql_data:
Giải thích file cấu hình:
services: Định nghĩa hai container dịch vụ chính làmysql_dbvànode_app.mysql_db:image: mysql:8.0: Sử dụng image MySQL chính thức từ Docker Hub.container_name: Đặt tên container cố định, dễ nhận biết.environment: Các biến môi trường được MySQL image sử dụng để khởi tạo database mặc định; việc hardcode trong file này chỉ phù hợp với môi trường phát triển.ports: Map port3307trên máy host ra port3306của container (phòng trường hợp máy bạn đã có MySQL chạy ở cổng 3306).healthcheck: (Rất quan trọng!) Đây là một check định kỳ để biết khi nào MySQL thực sự sẵn sàng nhận kết nối. Câu lệnhmysqladmin pingsẽ kiểm tra điều này. Lưu ý: lệnh đã được viết dưới dạng shell (sh -c ...) để có thể lấy biến môi trườngMYSQL_ROOT_PASSWORDmột cách chính xác.
node_app:build:context: .vàdockerfile: Dockerfile: Hướng dẫn Compose build image từDockerfilenằm cùng thư mục.volumes: Mount thư mục hiện tại (toàn bộ source code) vào/appbên trong container. Đây là chìa khóa cho Live Reload: khi bạn sửa code, thay đổi sẽ được cập nhật ngay lập tức.depends_on: Kết hợp vớicondition: service_healthy, đảm bảo containernode_appchỉ khởi động sau khi MySQL đã hoàn thành healthcheck. Điều này giải quyết triệt để lỗi “connection refused” kinh điển mà các dev hay gặp.
Bước 3: Tạo Dockerfile cho Node.js app
Trong cùng thư mục dự án, tạo một file tên là Dockerfile (không có phần mở rộng) với nội dung:
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
EXPOSE 3000
CMD ["npm", "run", "dev"]
Giải thích: Dockerfile này sử dụng image Node.js nhẹ dựa trên Alpine Linux. Nó copy file package.json để cài đặt dependencies, sau đó copy toàn bộ source code. Lệnh CMD ["npm", "run", "dev"] giả định rằng bạn có một script "dev" trong package.json để chạy ứng dụng với cơ chế hot-reload (ví dụ: sử dụng nodemon). Điều này kết hợp với volume mount ở trên sẽ mang lại trải nghiệm phát triển mượt mà.
📦 Tạo package.json và cài nodemon (bắt buộc để có live reload)
Trước khi chạy Docker Compose, bạn cần khởi tạo một ứng dụng Node.js đơn giản. Thực hiện:
npm init -y
npm install express
npm install -D nodemon
Sau đó, sửa file package.json, thêm script "dev":
"scripts": {
"dev": "nodemon app.js"
}
Tạo file app.js với nội dung tối thiểu:
const express = require('express');
const app = express();
app.get('/', (req, res) => res.send('Hello Docker!'));
app.listen(3000, () => console.log('App running on port 3000'));
Live Reload bên trong container:
Bí quyết nằm ở hai yếu tố: Volume mount source code (- .:/app) và process watcher nodemon.
Khi bạn thay đổi file trên máy tính, thay đổi đó gần như ngay lập tức xuất hiện trong container thông qua volume. nodemon sẽ phát hiện sự thay đổi và tự động restart lại ứng dụng Node.js của bạn.
🐧 Lưu ý với người dùng Linux:
Nếu bạn gặp lỗi permission khi mount volume (ví dụ: không thể ghi file trong container), hãy thêm dòng user: "node" vào service node_app trong docker-compose.yml hoặc đảm bảo thư mục dự án có quyền đọc/ghi cho user node (UID 1000).
Bước 4: Khởi chạy và kiểm tra
Mở terminal trong thư mục chứa file docker-compose.yml và chạy:
docker compose up -d
-d(detached mode) chạy các container ở chế độ nền.- Lần đầu chạy, Docker sẽ pull image MySQL và build image Node.js của bạn.
- Sau khi hoàn tất, kiểm tra trạng thái:
docker compose ps - Xem log của một service cụ thể, ví dụ:
docker compose logs node_app
Để dừng và xóa tất cả container được quản lý bởi Compose:
docker compose down
Thêm flag -v để xóa luôn các volumes (cẩn thận vì sẽ mất dữ liệu database):
docker compose down -v
Các lệnh Docker Compose cơ bản
Bạn sẽ cần những lệnh dưới đây trong quá trình làm việc hàng ngày:
docker compose up -d: Khởi động tất cả các servicedocker compose down: Dừng và xóa tất cả containerdocker compose ps: Liệt kê trạng thái các containerdocker compose logs -f: Xem log realtime của tất cả service- **`docker compose exec sh`** : Mở shell bên trong container của service
docker compose build: Build lại image mà không cần chạy containerdocker compose up --build -d: Build lại image rồi mới khởi động (thường dùng sau khi sửa Dockerfile)
Lỗi thường gặp và cách khắc phục
1. Lỗi “ECONNREFUSED” hoặc “Connection refused” (DB connection refused)
Biểu hiện:
App của bạn báo lỗi không thể kết nối tới database khi khởi động.
Nguyên nhân:
App cố gắng kết nối đến database trước khi database khởi động xong hoàn toàn và sẵn sàng nhận kết nối.
Cách khắc phục triệt để:
Sử dụng healthcheck kết hợp depends_on với condition: service_healthy. Đây là giải pháp chuẩn cho cả môi trường Production và Local.
Cách khắc phục nhanh (Alternative):
Sử dụng script wait-for-it. Bạn thêm script này vào Dockerfile và sửa lệnh command trong service:
command: ["./wait-for-it.sh", "db:3306", "--", "npm", "start"]
👉 Trong hướng dẫn này, tôi khuyên dùng healthcheck vì nó tận dụng được cơ chế native của Docker Compose, không cần script phụ trợ.
2. WARNING: version is obsolete
Biểu hiện:
Khi chạy docker compose up, bạn thấy dòng cảnh báo: WARN[0000] docker-compose.yml: version is obsolete.
Nguyên nhân:
File cấu hình có dòng version: "3.8" ở đầu. Kể từ Docker Compose V2, trường này chính thức bị bãi bỏ.
Cách khắc phục:
Đơn giản là xóa dòng version: "3.8" khỏi file docker-compose.yml. Docker hiện đại sẽ tự động hiểu và xử lý cấu hình dựa trên Compose Specification.
Best Practices cho một Docker Compose chuyên nghiệp
1. Luôn sử dụng biến môi trường (.env)
Đừng bao giờ hardcode mật khẩu, tên người dùng vào file docker-compose.yml như ví dụ trên nếu bạn định push lên GitHub. Thay vào đó:
- Tạo một file
.envcùng thư mục:
MYSQL_ROOT_PASSWORD=strong_root_password
MYSQL_DATABASE=app_db
MYSQL_USER=app_user
MYSQL_PASSWORD=strong_app_password
- Sửa lại file
docker-compose.yml:
services:
mysql_db:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
MYSQL_DATABASE: ${MYSQL_DATABASE}
MYSQL_USER: ${MYSQL_USER}
MYSQL_PASSWORD: ${MYSQL_PASSWORD}
# ...
Nhớ thêm file .env vào .gitignore để không vô tình commit mật khẩu lên Git. Cách làm này giúp dự án linh hoạt hơn và bảo mật hơn.
📄 File .gitignore mẫu cho dự án Node.js + Docker:
# Dependencies
node_modules/
# Environment variables
.env
# Docker volumes (nếu lưu trong thư mục dự án)
data/
# Logs
logs/
*.log
# OS metadata
.DS_Store
Thumbs.db
2. Sử dụng Named Volumes để lưu trữ dữ liệu quan trọng
Như đã làm với mysql_data, hãy luôn sử dụng named volumes cho dữ liệu cần lưu trữ (database, upload folder, cache…). Điều này đảm bảo dữ liệu của bạn còn nguyên vẹn ngay cả khi bạn chạy docker compose down -v.
3. Tách biệt file compose cho môi trường dev và prod
Với dự án thực tế, bạn nên có hai file riêng: docker-compose.dev.yml (với volume mount, hot-reload, mở các debug port) và docker-compose.yml (tối ưu cho production). Bạn có thể chỉ định file bằng flag -f:
docker compose -f docker-compose.dev.yml up -d
FAQ
1. Lệnh docker-compose (có gạch ngang) và docker compose (không gạch ngang) khác gì nhau?
Đây là sự khác biệt giữa phiên bản V1 và V2.
docker-compose (có gạch ngang) là phiên bản V1 được viết bằng Python, hiện đã ngừng phát triển và không được khuyến khích dùng nữa. docker compose (không gạch ngang) là phiên bản V2 hiện tại, được viết bằng Go, tích hợp sẵn trong Docker CLI, nhanh hơn, và tuân thủ Compose Specification hiện đại. Hãy luôn dùng phiên bản V2 cho mọi dự án mới.
2. Làm sao để debug code bên trong container, ví dụ với Node.js?
Có hai cách phổ biến để thực hiện việc này:
- Dùng VS Code
– Mở project của bạn trên host, cài extension “Docker”. Trong VS Code, vào panel Run and Debug (Ctrl+Shift+D), tạo cấu hình “Docker: Attach to Node.js”. Sau đó chạy container lên, và attach vào nó. Bạn có thể đặt breakpoint như bình thường. - Mở cổng debug trong Docker Compose
– Thêm dòngports: - "9229:9229"vào servicenode_appvà đảm bảo app của bạn được khởi động với flag--inspect=0.0.0.0:9229. Sau đó bạn có thể dùng Chrome DevTools (truy cậpchrome://inspect) hoặc các IDE khác để kết nối vào cổng này.
3. Làm thế nào để reset hoàn toàn database (xóa sạch dữ liệu)?
Chạy lệnh
docker compose down -v sẽ dừng container và xóa luôn các volume được khai báo trong file compose (trong ví dụ là volume mysql_data). Lần sau khi chạy docker compose up -d, database sẽ được khởi tạo lại từ đầu (với dữ liệu mặc định nếu có).
Kết luận
Bạn vừa hoàn thành một bước tiến lớn trên con đường trở thành một developer chuyên nghiệp. Docker Compose không chỉ giúp bạn quản lý multi-container một cách dễ dàng mà còn đảm bảo rằng môi trường development của bạn nhất quán với mọi thành viên trong nhóm hoặc với production.
Hãy nhớ, chìa khóa cho một workflow hiệu quả là Live Reload và xử lý dependency giữa các services. Với những kiến thức nền tảng này, bạn có thể tự tin mở rộng cho bất kỳ stack nào (PHP, Python, Go, …) chỉ với một vài điều chỉnh nhỏ.
Nếu bạn muốn tiết kiệm thời gian hơn nữa, đừng ngần ngại tải ngay mẫu Docker Compose cho Node.js và PHP, đã được cấu hình sẵn các best practices. Chạy ngay lập tức và bắt đầu code, thay vì mất hàng giờ cấu hình.