Share

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

Sử dụng Docker Compose để kết nối và quản lý ứng dụng fullstack Node.js backend và React frontend - siucode.com

Ở 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 builddocker 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 updocker-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ộ.

docker compose giai quyet van de ket noi container siucode

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
Bash

Hã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à:

  1. Ứng dụng React sẽ gọi các API với đường dẫn tương đối, ví dụ /api/users.
  2. 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;
    }
}
Nginx

Bướ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 COPYstage 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;"]
Dockerfile

Bướ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
nginx reverse proxy concept docker compose siucode

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
YAML

Giải thích:

  • Chúng ta định nghĩa hai services: backendfrontend.
  • Tên service backend được sử dụng trong file nginx.conf (proxy_pass http://backend:3001;) để Nginx biết nơi chuyển tiếp request.
  • depends_on: đảm bảo container backend sẽ được khởi động trước container frontend.

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
Bash

Bạ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 🚀

You may also like

Mục lục