Học Docker từ A-Z (Phần 4): Docker hóa React App & Tối ưu với Multi-stage Build

Ở phần trước, anh em đã thành công khi Docker hóa được ứng dụng backend Node.js đầu tiên. Chúng ta đã thấy được sức mạnh của Dockerfile, docker build
và docker run
rồi đúng không?
(Bạn nào muốn ôn lại các bước thực hành với Node.js thì có thể xem lại Phần 3 tại đây nhé.)
Backend đã xong, giờ là lúc chúng ta xử lý “mặt tiền” – ứng dụng frontend. Trong phần này, chúng ta sẽ cùng nhau Docker hóa một ứng dụng React. Tuy nhiên, với frontend, mọi thứ có hơi khác một chút. Chúng ta không chỉ sao chép code rồi chạy, mà còn cần một bước build để tối ưu code cho production. Nếu làm không khéo, Docker image cho frontend có thể trở nên rất lớn và không hiệu quả.
Đừng lo, Docker có một kỹ thuật rất hữu ích để giải quyết vấn đề này, đó chính là multi-stage build. Hãy cùng SiuCode khám phá nhé!
Vấn đề thường gặp với image frontend
Với ứng dụng frontend hiện đại (React, Vue, Angular…), chúng ta thường có hai chế độ:
- Development: Chạy server dev (
npm start
) để code và thấy thay đổi ngay lập tức (hot-reloading). Môi trường này cần toàn bộ mã nguồn,node_modules
(bao gồm cả devDependencies) và chạy bằng Node.js. - Production: Chạy lệnh build (
npm run build
) để tạo ra các file tĩnh (HTML, CSS, JS đã được tối ưu, minify). Các file tĩnh này sau đó sẽ được phục vụ bởi một web server như Nginx hoặc Apache.
Nếu chúng ta viết Dockerfile một cách thông thường bằng cách cài Node.js, sao chép hết mã nguồn + node_modules
vào image rồi dùng CMD ["npm", "start"]
, image tạo ra sẽ:
- Rất lớn: Vì chứa cả mã nguồn gốc,
node_modules
(có thể lên đến hàng trăm MB hoặc GB), và cả Node.js runtime. - Không tối ưu cho production: Chạy bằng dev server không hiệu quả và không an toàn.
- Kém bảo mật: Chứa cả mã nguồn và các công cụ build không cần thiết trong image production.
Rõ ràng cách này không phù hợp để triển khai lên server thật. Chúng ta cần build ra file tĩnh và chỉ đưa những file đó vào image cuối cùng.

2. Giải pháp: Multi-stage build – Xây dựng đa giai đoạn
Đây chính là lúc multi-stage build phát huy tác dụng. Kỹ thuật này cho phép bạn định nghĩa nhiều giai đoạn (stage) trong cùng một file Dockerfile
. Mỗi giai đoạn có thể bắt đầu từ một base image khác nhau và thực hiện các công việc riêng. Quan trọng nhất là các giai đoạn sau có thể sao chép các thành phẩm (artifacts) từ các giai đoạn trước đó.
Để dễ hình dung hơn, tưởng tượng bạn có 2 phân xưởng:
- Phân xưởng 1 (Build stage): Dùng “máy móc chuyên dụng” (Node.js + các công cụ build) để chế tạo ra các “linh kiện thành phẩm” (là các file static HTML, CSS, JS sau khi chạy
npm run build
). - Phân xưởng 2 (Production stage): Dùng một “khung sườn” thật gọn nhẹ, hiệu quả (Nginx server) và chỉ lấy các “linh kiện thành phẩm” từ Phân xưởng 1 gắn vào để tạo ra “sản phẩm cuối cùng” (Docker image rất nhẹ chỉ chứa file tĩnh và Nginx).
Lợi ích:
- Image cuối cùng rất nhẹ: Chỉ chứa những gì thực sự cần thiết để chạy (file tĩnh + Nginx).
- Bảo mật tốt hơn: Không lộ mã nguồn hay các công cụ build trong image production.
- Dockerfile gọn gàng: Tất cả các bước build và đóng gói nằm trong cùng một file.

Chuẩn bị ứng dụng React mẫu
Chúng ta sẽ dùng một ứng dụng React đơn giản được tạo bằng create-react-app
. Nếu bạn chưa có, hãy tạo nhanh bằng lệnh:
npx create-react-app my-react-app
cd my-react-app
BashCấu trúc thư mục cơ bản sẽ là:
my-react-app/
├── public/
├── src/
│ └── App.js
├── package.json
├── Dockerfile # Sẽ tạo sau
└── ...
BashSau khi chạy npm run build
, bạn sẽ có thêm thư mục build
chứa các file tĩnh.

Bạn có thể chỉnh sửa src/App.js
một chút nếu muốn. Quan trọng là file package.json
phải có script "build": "react-scripts build"
.
Viết Dockerfile với multi-stage build
Trong thư mục gốc my-react-app/
, tạo file Dockerfile
với nội dung sau:
# ----- GIAI ĐOẠN 1: Build Stage -----
FROM node:18-alpine as builder
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
# ----- GIAI ĐOẠN 2: Production Stage -----
FROM nginx:stable-alpine
COPY --from=builder /app/build /usr/share/nginx/html
# COPY nginx.conf /etc/nginx/conf.d/default.conf # Tùy chọn
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
DockerfileGiải thích chi tiết các chỉ dẫn mới/quan trọng:
FROM node:18-alpine as builder
:as builder
đặt tên cho stage này.RUN npm run build
: Tạo thư mục/app/build
chứa file tĩnh.FROM nginx:stable-alpine
: Bắt đầu stage mới, nhẹ hơn.COPY --from=builder /app/build /usr/share/nginx/html
: Điểm mấu chốt, chỉ sao chép kết quả build từ stagebuilder
.CMD ["nginx", "-g", "daemon off;"]
: Khởi động Nginx.
Build Docker image – Tạo khuôn React đã tối ưu
Mở terminal trong thư mục my-react-app/
. Chạy lệnh build:
Chạy lệnh build:
docker build -t my-react-app:1.0 .
BashQuan sát output: Bạn sẽ thấy Docker chạy qua từng bước của cả hai stage.

Kiểm tra image: docker images
. Chú ý kích thước nhỏ của image my-react-app:1.0
.

Chạy Docker container – Phục vụ app React
Chạy lệnh sau:
docker run -p 3000:80 --name my-running-react-app -d my-react-app:1.0
BashGiải thích -p 3000:80
: Ánh xạ port 3000
trên máy host vào port 80
bên trong container (Nginx).
Kiểm tra container: docker ps
.
Kiểm tra thành quả!
Mở trình duyệt và truy cập http://localhost:3000
.
Bạn sẽ thấy ứng dụng React của mình đang chạy!

Frontend giao tiếp với backend như thế nào?
Giờ chúng ta đã có container backend Node.js và container frontend React chạy độc lập. Nhưng làm thế nào để ứng dụng React trong container frontend có thể gọi API từ ứng dụng Node.js trong container backend? Chúng đang ở trong các môi trường biệt lập.
Đây chính là lúc chúng ta cần một công cụ mạnh mẽ hơn để quản lý và kết nối các container này lại với nhau.
Dọn dẹp
Đừng quên dọn dẹp khi thử nghiệm xong:
docker stop my-running-react-app
docker rm my-running-react-app
docker rmi my-react-app:1.0
BashLời kết
Vậy là xong! Chúc mừng bạn đã hoàn thành Docker hóa ứng dụng frontend React và nắm được kỹ thuật multi-stage build để tối ưu hóa image. Kỹ thuật này rất hữu ích để giữ image production luôn gọn nhẹ và an toàn.
Ở phần tiếp theo sẽ là lời giải cho bài toán kết nối frontend và backend. Chúng ta sẽ tìm hiểu về Docker Compose – công cụ giúp định nghĩa và điều phối các ứng dụng multi-container một cách dễ dàng nhé.
Hẹn gặp lại anh em ở phần sau.
SiuCode – Vừa code vừa siuuuu 🚀