Học Docker từ A-Z (Phần 5): Kết nối backend – frontend với Docker Compose

Ở các phần trước, chúng ta đã có những thành tựu đáng kể. Phần 3 giúp chúng ta Docker hóa thành công backend Node.js. Phần 4 đã hướng dẫn chúng ta cách đóng gói frontend một cách hiệu quả. Hiện tại, chúng ta đang có hai container riêng biệt, mỗi container chạy một phần của ứng dụng, nhưng chúng lại đang “cô đơn”, không thể “nói chuyện” được với nhau.
Vậy làm thế nào để frontend có thể gọi API từ backend khi chúng bị cô lập trong các container khác nhau? Làm sao để quản lý cả hai như một thể thống nhất?
Câu trả lời nằm ở: Docker Compose!
Docker Compose là gì?
Docker Compose là một công cụ dòng lệnh mạnh mẽ được cung cấp bởi Docker, giúp bạn định nghĩa (define) và chạy (run) các ứng dụng Docker bao gồm nhiều container một cách dễ dàng.
Thay vì phải docker build
và docker run
từng container một cách thủ công với hàng loạt tham số dòng lệnh phức tạp, bạn chỉ cần mô tả toàn bộ “hệ sinh thái” ứng dụng của mình (bao gồm các services, networks, volumes…) trong một file cấu hình duy nhất viết bằng ngôn ngữ YAML, thường được đặt tên là docker-compose.yml
.
Sau đó, chỉ với một vài lệnh đơn giản như docker-compose up
và docker-compose down
, bạn có thể khởi tạo, chạy, dừng và quản lý toàn bộ cụm container một cách đồng bộ.

Lợi ích của Docker Compose
- Quản lý đa container đơn giản: Dễ dàng định nghĩa và điều khiển nhiều services (mỗi service chạy trong một container) như web server, database, cache…
- Môi trường phát triển nhất quán: Giúp tạo ra một môi trường phát triển trên máy local gần giống nhất với môi trường production.
- Tự động tạo network: Docker Compose tự động tạo một network ảo cho tất cả các services trong file, cho phép chúng dễ dàng giao tiếp với nhau bằng tên service.
- Dễ dàng chia sẻ cấu hình: Chỉ cần chia sẻ file
docker-compose.yml
và các Dockerfile, bất kỳ ai trong team cũng có thể dựng lại toàn bộ môi trường ứng dụng nhanh chóng.
Chuẩn bị cấu trúc dự án
Để sử dụng Docker Compose, chúng ta sẽ tổ chức lại dự án một chút. Giả sử bạn có một thư mục gốc cho toàn bộ dự án, ví dụ my-fullstack-app
, bên trong sẽ chứa:
my-fullstack-app/
├── backend/ # Thư mục chứa code Node.js và Dockerfile của backend
│ ├── ...
│ └── Dockerfile
├── frontend/ # Thư mục chứa code React và Dockerfile của frontend
│ ├── nginx/
│ │ └── nginx.conf # File cấu hình Nginx chúng ta sẽ tạo
│ ├── ...
│ └── Dockerfile
└── docker-compose.yml # File cấu hình Docker Compose chúng ta sẽ tạo
BashHãy đảm bảo rằng Dockerfile cho backend Node.js (từ Phần 3) nằm trong thư mục backend/
và Dockerfile cho frontend React (từ Phần 4) nằm trong thư mục frontend/
.
Cập nhật Frontend để giao tiếp qua Nginx Reverse Proxy
Để giải quyết vấn đề giao tiếp giữa frontend và backend một cách thanh lịch (và tránh các vấn đề về CORS trong môi trường dev), chúng ta sẽ sử dụng một kỹ thuật rất phổ biến: dùng chính server Nginx trong container frontend làm reverse proxy.
Tức là:
- Ứng dụng React sẽ gọi các API với đường dẫn tương đối, ví dụ
/api/users
. - Chúng ta sẽ cấu hình Nginx để khi nó nhận được một request có đường dẫn bắt đầu bằng
/api
, nó sẽ không tìm file trong thư mục static, mà sẽ chuyển tiếp (forward) request đó đến container backend.
Bước 1: Tạo file cấu hình Nginx
Trong thư mục frontend/
, tạo một thư mục con nginx
và trong đó tạo file nginx.conf
với nội dung sau:
# frontend/nginx/nginx.conf
server {
listen 80;
# Phục vụ các file tĩnh của React App
location / {
root /usr/share/nginx/html;
index index.html index.htm;
# Cần thiết cho các ứng dụng single-page (SPA) như React
try_files $uri $uri/ /index.html;
}
# Reverse proxy cho các request API
# Bất kỳ request nào đến /api/... sẽ được chuyển tiếp đến backend
location /api {
# 'backend' là tên service của Node.js app trong docker-compose.yml
# 3001 là port mà backend container đang lắng nghe
proxy_pass http://backend:3001;
# Các header cần thiết để chuyển tiếp thông tin request
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
NginxBước 2: Cập nhật Dockerfile của Frontend
Mở file frontend/Dockerfile
(file multi-stage chúng ta đã tạo ở Phần 4) và thêm một dòng COPY
ở stage cuối cùng (production stage) để sử dụng file nginx.conf
mới này:
# ----- STAGE 2: Production Stage -----
# ... các dòng trên giữ nguyên
FROM nginx:stable-alpine
# DÒNG MỚI: Copy file cấu hình Nginx tùy chỉnh của chúng ta
COPY nginx/nginx.conf /etc/nginx/conf.d/default.conf
# Copy kết quả build từ stage 'builder'
COPY --from=builder /app/build /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
DockerfileBước 3: Cập nhật code React
Bây giờ trong code React (frontend/src/App.js
), bạn chỉ cần gọi API với đường dẫn tương đối:
// frontend/src/App.js (đoạn fetch data)
useEffect(() => {
fetch('/api/users') // Gọi đường dẫn tương đối, Nginx sẽ lo phần còn lại
.then(res => res.json())
.then(data => {
setUsers(data);
setMessage('Dữ liệu từ Backend Node.js:');
})
.catch(error => {
console.error("Lỗi khi gọi backend:", error);
setMessage(`Lỗi khi gọi backend: ${error.message}`);
});
}, []);
TypeScript
Viết file docker-compose.yml
Trong thư mục gốc my-fullstack-app/
, tạo file docker-compose.yml
:
version: '3.8'
services:
backend:
build: ./backend
container_name: my_backend_service
ports:
- "3001:3001" # Expose port này chủ yếu để dev có thể kiểm tra API trực tiếp
environment:
- NODE_ENV=development
frontend:
build: ./frontend
container_name: my_frontend_service
ports:
- "3000:80" # Ánh xạ port 3000 của host vào port 80 của container Nginx
depends_on:
- backend
YAMLGiải thích:
- Chúng ta định nghĩa hai services:
backend
vàfrontend
. - Tên service
backend
được sử dụng trong filenginx.conf
(proxy_pass http://backend:3001;
) để Nginx biết nơi chuyển tiếp request. depends_on:
đảm bảo containerbackend
sẽ được khởi động trước containerfrontend
.
Chạy ứng dụng với Docker Compose
Giờ là lúc khởi chạy toàn bộ hệ thống! Mở terminal ở thư mục gốc my-fullstack-app/
.
- Chạy và build (nếu cần):
docker-compose up --build
BashBạn sẽ thấy log từ cả hai container backend và frontend hiển thị xen kẽ. [Ảnh minh họa: Terminal-Docker-Compose-Up-Output]
Kiểm tra thành quả
- Mở trình duyệt và truy cập vào địa chỉ frontend:
http://localhost:3000
. - Giao diện React sẽ hiện ra, và sau đó dữ liệu user từ backend sẽ được tải và hiển thị. Điều này chứng tỏ Nginx reverse proxy đã hoạt động, kết nối thành công từ frontend đến backend!
Các lệnh Docker Compose hữu ích khác
- Dừng và xóa toàn bộ hệ thống:
docker-compose down
- Liệt kê các container:
docker-compose ps
- Xem log của một service:
docker-compose logs backend
- Mở shell vào container:
docker-compose exec frontend sh
Lời kết
Qua Phần 5, bạn đã nắm được sức mạnh của Docker Compose trong việc định nghĩa, quản lý và kết nối các ứng dụng multi-container. Việc sử dụng network nội bộ và reverse proxy là những kỹ thuật cực kỳ phổ biến và hữu ích trong thực tế. Với Docker Compose, việc xây dựng và chạy các kiến trúc phức tạp trở nên đơn giản và nhất quán hơn bao giờ hết.
Ứng dụng fullstack của chúng ta đã chạy. Nhưng nếu backend cần lưu trữ dữ liệu vào database thì sao? Dữ liệu trong container sẽ mất khi nó bị xóa. Trong Phần 6, chúng ta sẽ giải quyết vấn đề này bằng cách tìm hiểu về Volumes trong Docker, và tích hợp một service database (ví dụ PostgreSQL) vào docker-compose.yml
.
Hẹn gặp lại anh em ở phần sau.
SiuCode – Vừa code vừa siuuuu 🚀