Bài 36 — Build và deploy lên môi trường thật
Phần 6 · Nâng cao & hoàn thiện — Thời lượng ước tính: ~120 phút ⬅️ Bài trước: 35 — 🎓 Đồ án cuối khoá: làm chức năng Voucher end-to-end · Bài sau: 🎉 Bạn đã hoàn thành giáo trình! 🏠 Mục lục
🎯 Sau bài này bạn sẽ
- Phân biệt rạch ròi development và production, và hiểu vì sao
localhostkhông ai ngoài máy bạn truy cập được. - Chạy được
npm run buildcho frontend và hiểu bundle production khác gì server dev. - Chuẩn bị backend cho production: biến môi trường, MongoDB Atlas (database đám mây miễn phí), chạy bằng
node/pm2thay vìnodemon. - Giới hạn CORS về đúng domain frontend thay vì mở cho mọi domain.
- Chọn một combo deploy miễn phí (Vercel + Render + Atlas) và bấm từng bước để đưa web lên internet.
- Chạy qua một checklist trước khi deploy gom lại mọi thứ đã học từ Bài 33 và 34.
📋 Cần chuẩn bị
- Đã hoàn thành Bài 34 — dự án đã tách
.env, đã đổi secret, đã cóconfigureStorevà error handler. - Đã đọc Bài 33 — biết 8 lỗ hổng cần vá trước khi đưa lên mạng.
- Một tài khoản GitHub (miễn phí), đã đẩy dự án lên một repo.
- Trình duyệt + Git đã cài trên máy.
- Khoảng 2 tiếng và một tách trà sữa. Đây là bài cuối cùng — không vội.
💡 Mẹo: Bài này dài vì deploy là chuỗi nhiều bước nhỏ, mỗi bước dễ sai một chỗ. Đừng đọc lướt. Làm tới đâu chắc tới đó, sai ở đâu quay lại mục 🐞 Lỗi thường gặp ở cuối.
1. Development vs Production: hai thế giới khác nhau
Suốt 35 bài qua, bạn chạy dự án theo kiểu development (viết tắt: dev):
- Backend:
npm start→nodemon+babel-node, cổng 8080. - Frontend:
npm start→react-scripts start, cổng 3000. - Cả hai đều nằm trên máy bạn, gọi nhau qua
localhost.
Production (viết tắt: prod) là khi web chạy thật trên một máy chủ ngoài internet để người lạ ở bất kỳ đâu đều mở được. Bảng so sánh:
| Development (đang làm) | Production (bài này) | |
|---|---|---|
| Ai truy cập được | Chỉ máy bạn | Cả thế giới, qua một domain |
| Frontend chạy bằng | react-scripts start (server dev, tự reload) |
File tĩnh đã build sẵn, đặt trên CDN |
| Backend chạy bằng | nodemon (tự restart khi sửa code) |
node/pm2 (chạy ổn định, tự hồi phục khi crash) |
| Database | MongoDB localhost:27017 |
MongoDB Atlas (đám mây) |
| Cấu hình | Hardcode / .env trên máy |
Biến môi trường đặt trên dashboard hosting |
| Tốc độ frontend | Chậm, file to (chưa nén) | Nhanh, file nhỏ (đã minify + tách chunk) |
| Báo lỗi | Hiện stack trace chi tiết | Ẩn stack trace, chỉ báo lỗi chung |
1.1. Vì sao localhost không ai truy cập được từ ngoài?
localhost (và địa chỉ 127.0.0.1) nghĩa là "chính máy này". Khi trình duyệt của bạn
gọi http://localhost:3000, nó tìm web ngay trên máy bạn — không đi ra internet chút nào.
Nếu bạn nhắn cho bạn bè link http://localhost:3000, trên máy họ chữ localhost lại
trỏ về máy họ — nơi chẳng có Yotea nào cả. Họ sẽ thấy lỗi "không kết nối được".
📖 Thuật ngữ: localhost — tên gọi vòng-lặp-nội-bộ (loopback) chỉ chính thiết bị đang chạy. Muốn người khác vào được, web phải nằm trên một máy chủ có địa chỉ công khai (public IP) và thường gắn với một domain (ví dụ
yotea.vercel.app).
Deploy, nói ngắn gọn, là mang code từ máy bạn đặt lên một máy chủ công khai rồi cho nó một cái tên (domain) để mọi người gõ vào.
1.2. Sơ đồ luồng: dev vs prod
DEVELOPMENT (trên máy bạn)
Trình duyệt ──> localhost:3000 (React dev server)
│ gọi API
▼
localhost:8080 (Express + nodemon)
│
▼
MongoDB localhost:27017
PRODUCTION (trên internet)
Trình duyệt của khách ──> yotea.vercel.app (frontend tĩnh, CDN)
(bất kỳ đâu) │ gọi API
▼
yotea-api.onrender.com (Express + pm2/node)
│
▼
MongoDB Atlas (cluster đám mây)
Ba mảnh — frontend, backend, database — lúc dev nằm chung một máy, lúc prod tách ra ba nơi khác nhau. Phần lớn lỗi deploy là do ba mảnh này gọi nhầm địa chỉ của nhau.
2. Build frontend: npm run build
2.1. npm start khác npm run build thế nào?
Nhìn lại script của frontend, yotea-fe/package.json:36-42:
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
| Lệnh | Làm gì | Dùng khi nào |
|---|---|---|
npm start |
Bật một server dev ở cổng 3000, tự reload khi bạn sửa code, giữ file ở dạng dễ đọc | Lúc đang code |
npm run build |
Biên dịch toàn bộ React thành file tĩnh (HTML + CSS + JS đã nén) trong thư mục build/, không chạy server nào |
Trước khi deploy |
Chạy lệnh build (đứng tại thư mục yotea-fe):
# đứng tại thư mục yotea-fe
npm run build
Sau khoảng 30–60 giây, xuất hiện thư mục mới:
yotea-fe/build/
├── index.html
├── asset-manifest.json
└── static/
├── css/main.<hash>.css ← toàn bộ CSS đã gộp + nén
└── js/main.<hash>.js ← toàn bộ React đã gộp + minify
Terminal in ra kích thước từng file, đại loại:
File sizes after gzip:
148.2 kB build/static/js/main.a1b2c3d4.js
6.4 kB build/static/css/main.e5f6a7b8.css
2.2. "Bundle", "minify" là gì?
- Bundle (gộp gói): hàng trăm file
.jsbạn viết được gộp lại thành một (vài) file lớn, để trình duyệt tải ít lượt hơn → nhanh hơn. - Minify (rút gọn): xoá mọi khoảng trắng, đổi tên biến
formatCurrencythànht, bỏ comment... File nhỏ đi nhiều lần. Con người đọc không nổi, nhưng máy chạy y hệt. - Hash trong tên file (
main.a1b2c3d4.js): mỗi lần build lại mà nội dung đổi thì hash đổi theo, buộc trình duyệt tải bản mới thay vì dùng bản cũ trong cache.
Thư mục build/ này chính là thứ ta sẽ đẩy lên Vercel/Netlify — nó không cần Node.js để
chạy, chỉ là file tĩnh.
2.3. ⚠️ Biến REACT_APP_API_URL bị "nướng cứng" vào bundle
Ở Bài 34 bạn đã đổi baseURL hardcode thành biến môi trường. Nhắc
lại tình trạng gốc của dự án, yotea-fe/src/api/instance.js:3-5:
const instance = axios.create({
baseURL: "http://localhost:8080/api",
});
Và sau khi refactor ở Bài 34, dòng này trở thành:
// đã sửa ở Bài 34 — đọc từ biến môi trường
baseURL: process.env.REACT_APP_API_URL,
⚠️ Chỗ cực kỳ dễ sai khi deploy: Với Create React App, mọi biến
REACT_APP_*được thay bằng giá trị chuỗi ngay lúcnpm run buildrồi đóng băng vào file JS. Trình duyệt của khách không đọc.envlúc chạy — nó chỉ chạy chuỗi đã bị "nướng cứng" vào bundle. Hệ quả: bạn phải đặt đúngREACT_APP_API_URLtrỏ tới backend thật TRƯỚC KHI build. Build xong mới đổi biến thì phải build lại, sửa.envkhông có tác dụng.
Cụ thể, trước khi build cho production, file yotea-fe/.env (hoặc .env.production) phải là:
# yotea-fe/.env.production ← file bạn tự tạo
REACT_APP_API_URL=https://yotea-api.onrender.com/api
Chứ không phải http://localhost:8080/api. Nếu quên, web deploy xong sẽ trắng trang vì
nó cố gọi localhost (máy khách chẳng có backend nào) — đây là lỗi #1 trong mục 🐞 cuối bài.
💡 Mẹo kiểm tra nhanh: build xong, mở
build/static/js/main.<hash>.js, bấmCtrl+Fgõonrender.com(hoặc domain backend của bạn). Nếu tìm thấy → đã nướng đúng. Nếu chỉ thấylocalhost:8080→ bạn quên đổi biến trước khi build.
3. Chuẩn bị backend cho production
3.1. Biến môi trường (đã làm ở Bài 34)
Bài 33 đã chỉ ra: secret "TuongVy" bị hardcode ở 4 nơi, connection string MongoDB hardcode
trong app.js. Bài 34 đã gom chúng vào .env. Nhắc lại các biến backend cần có khi lên prod:
| Biến | Ví dụ giá trị | Thay cho chỗ hardcode nào |
|---|---|---|
PORT |
do hosting tự cấp | app.js:51 fallback 8080 |
MONGO_URI |
mongodb+srv://... (chuỗi Atlas, mục 3.2) |
app.js:46 mongodb://localhost:27017/yotea |
JWT_SECRET |
chuỗi ngẫu nhiên dài | "TuongVy" (ký + verify token) |
CLIENT_URL |
https://yotea.vercel.app |
dùng để giới hạn CORS (mục 3.4) |
🔒 Ghi chú bảo mật:
.envkhông bao giờ được commit lên GitHub (phải nằm trong.gitignore). Trên hosting, bạn nhập các biến này vào dashboard chứ không đẩy file.envlên.
3.2. MongoDB Atlas — database đám mây miễn phí
MongoDB local (localhost:27017) chỉ sống trên máy bạn. Máy chủ backend ngoài internet không
với tới nó được. Giải pháp: dùng MongoDB Atlas — dịch vụ database đám mây của chính hãng
MongoDB, có bậc miễn phí M0 (512 MB, quá đủ cho đồ án).
Các bước tạo cluster:
- Vào mongodb.com/cloud/atlas, đăng ký tài khoản (miễn phí).
- Bấm Build a Database → chọn gói M0 Free → chọn nhà cung cấp (AWS) và vùng gần bạn (ví dụ Singapore) → Create.
- Màn hình Security Quickstart hiện ra:
- Create a database user: đặt username và password (ghi lại password, sẽ nhét vào
connection string). Tránh ký tự đặc biệt như
@,:,/trong password vì chúng làm hỏng chuỗi URI. - Add IP address (whitelist): đây là bước hay bị quên. Atlas mặc định chặn mọi kết nối;
bạn phải khai địa chỉ IP được phép vào. Vì backend hosting có IP hay đổi, chọn Allow access
from anywhere (
0.0.0.0/0) cho đồ án học tập.
- Create a database user: đặt username và password (ghi lại password, sẽ nhét vào
connection string). Tránh ký tự đặc biệt như
⚠️
0.0.0.0/0nghĩa là "cho mọi IP trên đời kết nối tới database". Với đồ án học thì tiện, nhưng production thật không nên làm vậy — chỉ nên whitelist đúng dải IP của máy chủ backend. Bù lại, database vẫn được bảo vệ bằng username/password, nên đừng dùng mật khẩu yếu.
- Lấy connection string: bấm Connect → Drivers → chọn Node.js. Bạn nhận được chuỗi kiểu:
mongodb+srv://yotea_user:<password>@cluster0.ab1cd.mongodb.net/?retryWrites=true&w=majority
Thay <password> bằng mật khẩu thật, và thêm tên database yotea ngay trước dấu ?:
mongodb+srv://yotea_user:matkhauthat@cluster0.ab1cd.mongodb.net/yotea?retryWrites=true&w=majority
Chuỗi này chính là giá trị của biến MONGO_URI. Để ý: local dùng schema mongodb://, còn Atlas
dùng mongodb+srv:// (có thêm +srv).
💡 Mẹo: Database trên Atlas ban đầu rỗng — không có sản phẩm, danh mục, topping nào. Bạn phải tự tạo lại dữ liệu (đăng ký admin, thêm sản phẩm qua trang admin), hoặc dùng công cụ
mongodump/mongorestoređể bê dữ liệu local lên. Đừng hoảng khi thấy web deploy xong trống trơn.
3.3. Chạy production bằng node/pm2 thay vì nodemon
Nhớ lại script backend, yotea-be/package.json:7:
"start": "nodemon ./src/app.js --exec babel-node -e js",
nodemon sinh ra để theo dõi file và restart mỗi khi bạn sửa code — tuyệt cho lúc dev,
nhưng vô nghĩa (và tốn tài nguyên) trên production vì code prod không đổi. babel-node cũng
biên dịch lại mỗi lần chạy, chậm.
Cách chuẩn cho production là build trước bằng Babel rồi chạy bằng node thuần:
// hướng thêm vào yotea-be/package.json — bạn tự viết thêm
"scripts": {
"start": "nodemon ./src/app.js --exec babel-node -e js",
"build": "babel src -d dist",
"serve": "node dist/app.js"
}
npm run build→ Babel dịchsrc/thành CommonJS trong thư mụcdist/.npm run serve→node dist/app.jschạy bản đã dịch, nhanh và ổn định.
⚠️ Chỗ dự án gốc còn thiếu:
yotea-be/package.jsonkhông có scriptbuild— nghĩa là dự án gốc chưa hề có đường deploy production chuẩn. Đây là việc bạn phải tự thêm.
pm2 là một cấp cao hơn: nó là trình quản lý tiến trình giúp app tự khởi động lại khi crash, chạy nền, và ghi log. Trên máy chủ VPS bạn sẽ gõ:
npm install -g pm2
pm2 start dist/app.js --name yotea-api
pm2 save
Còn trên các hosting như Render/Railway (mục 5), bạn không cần đụng tới pm2 — nền tảng tự lo việc giữ app sống. Bạn chỉ cần khai lệnh build và lệnh start trong dashboard.
3.4. ⚠️ CORS production: đừng để mở cho mọi domain
Nhìn lại middleware CORS của dự án, yotea-be/src/app.js:25-26:
app.use(express.json());
app.use(cors());
cors() gọi không kèm option nghĩa là gắn header Access-Control-Allow-Origin: * —
bất kỳ website nào trên internet cũng gọi được API Yotea từ trình duyệt. Lúc dev thì tiện,
nhưng production nên giới hạn chỉ cho phép đúng domain frontend của bạn:
// hướng sửa cho production — bạn tự viết thêm
app.use(
cors({
origin: process.env.CLIENT_URL, // ví dụ https://yotea.vercel.app
credentials: true,
})
);
🔒 Vì sao quan trọng: để
*nghĩa là một trang web lừa đảo bất kỳ cũng có thể nhúng JavaScript gọi tới API của bạn thay mặt người dùng đang đăng nhập. Giới hạnoriginvề đúng domain frontend là hàng rào cơ bản chống loại lạm dụng đó.
💡 Mẹo: Nếu bạn có nhiều domain frontend (ví dụ cả
yotea.vercel.applẫn domain riêng), truyền một mảng origin, hoặc một hàm kiểm tra origin nằm trong danh sách trắng hay không.
4. Ba lựa chọn deploy (đều có bậc miễn phí)
Không cần thuê server đắt tiền. Ba mảnh của Yotea có ba nhóm dịch vụ miễn phí quen thuộc:
| Mảnh | Dịch vụ gợi ý | Bậc miễn phí | Ghi chú |
|---|---|---|---|
| Frontend (file tĩnh) | Vercel hoặc Netlify | Có, hào phóng | Kết nối repo GitHub, tự build & phát hành qua CDN |
| Backend (Node/Express) | Render hoặc Railway | Có (Render free tier ngủ sau 15 phút không dùng) | Chạy được server luôn-bật |
| Database (MongoDB) | MongoDB Atlas | Có (M0, 512 MB) | Đã làm ở mục 3.2 |
💡 Về "ngủ đông" của Render free: service miễn phí của Render tự ngủ sau ~15 phút không có request; request đầu tiên sau khi ngủ mất ~30–50 giây để "thức dậy". Với đồ án học thì chấp nhận được — chỉ cần biết để không tưởng là web bị lỗi khi lần đầu tải chậm.
Từ đây, ta chọn một combo cụ thể để hướng dẫn: Vercel (frontend) + Render (backend) + Atlas (database). Đây là combo phổ biến nhất cho người mới vì cả ba đều đăng nhập bằng GitHub và thao tác gần như chỉ bấm nút.
4.1. Deploy backend lên Render
- Đẩy dự án lên GitHub (nếu chưa). Kiểm tra
node_modules/KHÔNG bị commit (xem checklist mục 6 — dự án gốc lỡ commit hơn 8000 filenode_modules). - Vào render.com, đăng nhập bằng GitHub.
- Bấm New + → Web Service → chọn repo Yotea → chọn Root Directory là
yotea-be. - Điền cấu hình:
- Build Command:
npm install && npm run build(chạybabel src -d dist). - Start Command:
npm run serve(tứcnode dist/app.js).
- Build Command:
- Mục Environment → Add Environment Variable, thêm từng biến (không đẩy file
.envlên):
MONGO_URI = mongodb+srv://yotea_user:matkhauthat@cluster0.ab1cd.mongodb.net/yotea?retryWrites=true&w=majority
JWT_SECRET = <chuỗi-ngẫu-nhiên-dài-của-bạn>
CLIENT_URL = https://yotea.vercel.app
(Không cần đặt PORT — Render tự cấp và app đọc qua process.env.PORT, đúng như app.js:51.)
6. Bấm Create Web Service. Render clone repo, chạy build, rồi start. Vài phút sau bạn có URL kiểu
https://yotea-api.onrender.com. Mở https://yotea-api.onrender.com/api/products để kiểm tra.
4.2. Deploy frontend lên Vercel
- Vào vercel.com, đăng nhập bằng GitHub.
- Add New... → Project → chọn repo Yotea → Root Directory là
yotea-fe. - Vercel tự nhận diện Create React App (Framework Preset = Create React App), tự điền:
- Build Command:
npm run build - Output Directory:
build
- Build Command:
- Mục Environment Variables, thêm:
REACT_APP_API_URL = https://yotea-api.onrender.com/api
Đặt biến này TRƯỚC khi bấm Deploy — nhớ mục 2.3: biến bị nướng cứng vào bundle lúc build.
5. Bấm Deploy. Vercel chạy npm run build, phát hành thư mục build/ lên CDN, cho bạn URL
kiểu https://yotea.vercel.app.
6. Quay lại Render, cập nhật CLIENT_URL thành đúng URL Vercel vừa nhận, để CORS (mục 3.4) cho
frontend gọi API. Render sẽ tự deploy lại backend.
💡 Mẹo về React Router: Yotea dùng
createBrowserRouter(Bài 15). Với các trang con như/thuc-don, nếu người dùng F5 (tải lại) trên đó, một số host trả 404 vì không có file/thuc-don. Vercel xử lý sẵn cho Create React App (mọi đường dẫn trả vềindex.html). Nếu deploy nơi khác như Netlify, bạn cần thêm file cấu hình rewrite/* → /index.html.
5. Checklist trước khi deploy (rất quan trọng)
Đây là lúc gom mọi thứ đã học ở Bài 33 và 34 lại. Đừng bấm Deploy khi còn ô chưa tick.
| ✅ | Việc | Vì sao | Bài liên quan |
|---|---|---|---|
| ☐ | Đã tách .env, .env nằm trong .gitignore |
Không lộ secret lên GitHub | 34 |
| ☐ | Đã đổi secret "TuongVy" thành chuỗi ngẫu nhiên dài, đặt qua JWT_SECRET |
Ai đọc source cũ đều ký được token admin giả | 33 #4 |
| ☐ | Đã vá lỗ hổng phân quyền (updateInfo tự phong admin, users trả password, order mở toang) | Web thật sẽ bị khai thác thật | 33 #1,#2,#3 |
| ☐ | Đã bỏ node_modules/ khỏi git, có .gitignore cho cả FE lẫn BE |
Dự án gốc lỡ commit hơn 8000 file; repo phình, deploy chậm | 33 |
| ☐ | Đã giới hạn CORS về CLIENT_URL thay vì cors() mở * |
Chặn domain lạ gọi API | mục 3.4 |
| ☐ | Đã đổi baseURL frontend sang REACT_APP_API_URL trỏ backend thật, và build lại |
Nếu không, web trắng trang vì gọi localhost |
mục 2.3 |
| ☐ | Ảnh Cloudinary đã dùng tài khoản của mình (đổi levantuan/kkio3wiw) |
Dự án gốc hardcode tài khoản người khác, có thể bị xoá bất cứ lúc nào | 32 |
| ☐ | Đã có script build/serve cho backend |
Dự án gốc chỉ chạy được dev | mục 3.3 |
| ☐ | MongoDB Atlas đã tạo cluster, đã whitelist IP, đã có MONGO_URI |
Backend không có DB thì mọi API treo | mục 3.2 |
⚠️ Chỗ dự án gốc cực kỳ dễ bị bỏ sót — Cloudinary. Nhắc lại từ Bài 32, hàm upload ảnh
yotea-fe/src/utils/index.jshardcode cloud namelevantuanvà presetkkio3wiw— không phải tài khoản của bạn. Deploy production mà giữ nguyên thì: (1) ảnh có thể chết nếu chủ tài khoản xoá preset; (2) bạn đang bơm ảnh vào Cloudinary của người lạ. Hãy tạo tài khoản Cloudinary miễn phí của riêng bạn, tạo unsigned preset, và thay hai giá trị đó.
6. 🛠️ Tự tay làm — deploy database + frontend
Mục tiêu phần này: cuối phần, bạn có một database Atlas chạy thật, và (tuỳ chọn) một trang Yotea sống trên internet mà bạn gửi link cho bạn bè mở được.
Bước 1 — Đưa dự án lên GitHub (bỏ node_modules)
Tạo/kiểm tra file .gitignore ở gốc mỗi thư mục:
# yotea-be/.gitignore ← file bạn tự tạo (dự án gốc thiếu)
node_modules/
dist/
.env
# yotea-fe/.gitignore (dự án gốc đã có sẵn phần này)
node_modules/
build/
.env
.env.production
Nếu node_modules đã lỡ bị commit trước đó, gỡ nó khỏi git (không xoá file trên đĩa):
# đứng tại gốc repo
git rm -r --cached yotea-be/node_modules
git commit -m "Remove node_modules from git"
Rồi đẩy lên GitHub:
git push origin main
Bước 2 — Tạo MongoDB Atlas (làm thật ngay bây giờ)
Làm theo mục 3.2:
- Đăng ký Atlas, tạo cluster M0 Free.
- Tạo database user (nhớ username + password).
- Network Access → Add IP → Allow access from anywhere (
0.0.0.0/0). - Connect → Drivers → copy connection string, thay
<password>, thêm/yoteatrước?.
Kết quả bạn có một chuỗi MONGO_URI dùng được. Test nhanh trên máy: tạm đặt biến này vào
yotea-be/.env rồi npm start — nếu terminal in Connected to MongoDB là Atlas đã thông.
Bước 3 — Build frontend trỏ về backend thật
Tạo yotea-fe/.env.production (backend của bạn có thể chưa deploy — dùng URL Render dự kiến, hoặc
làm bước này lại sau khi có URL thật):
# yotea-fe/.env.production ← file bạn tự tạo
REACT_APP_API_URL=https://yotea-api.onrender.com/api
Rồi build:
# đứng tại yotea-fe
npm run build
Kiểm tra bundle đã nướng đúng URL (mục 2.3): mở build/static/js/main.<hash>.js, tìm onrender.com.
Bước 4 — Deploy frontend lên Vercel
Làm theo mục 4.2: Add Project → chọn repo → Root yotea-fe → thêm biến REACT_APP_API_URL
→ Deploy. Nhận URL https://<tên-bạn>.vercel.app.
💡 Nếu backend chưa lên Render, frontend vẫn deploy được nhưng gọi API sẽ lỗi. Bạn có thể làm Bước 4 (backend, mục 4.1) trước, lấy URL Render, rồi mới quay lại đặt
REACT_APP_API_URLvà deploy frontend. Thứ tự đúng: Atlas → Render (backend) → Vercel (frontend).
Bước 5 — Nối ba mảnh và thử thật
- Trên Render, đặt
CLIENT_URL= URL Vercel (cho CORS). - Mở URL Vercel trên điện thoại (dùng 4G, không dùng wifi nhà bạn) — nếu vào được nghĩa là web đã thật sự công khai, không còn phụ thuộc máy bạn.
- Đăng ký một tài khoản trên web deploy → thử xem sản phẩm, thêm giỏ hàng, đặt đơn.
7. ✅ Kiểm chứng kết quả
Bạn deploy thành công khi tất cả các mục sau đúng:
- Mở URL backend
https://yotea-api.onrender.com/api/productstrên trình duyệt → nhận về JSON (dù là mảng rỗng[]nếu database Atlas chưa có dữ liệu):
[]
- Mở URL frontend
https://yotea.vercel.app→ trang chủ Yotea hiện lên bình thường, không trắng trang, không báo lỗi đỏ trong Console (F12). - Mở tab Network (F12) → tải lại trang → các request API trỏ tới domain Render, không còn
localhost:8080, và trả về status 200. - Đăng ký / đăng nhập được; token lưu trong
localStoragenhư lúc dev. - Gửi link cho một người bạn (hoặc mở bằng mạng 4G điện thoại) → họ vào được.
💡 Mẹo debug tại chỗ: mọi lỗi deploy đều để lại dấu vết ở một trong hai chỗ: Console/Network của trình duyệt (lỗi phía frontend/CORS) hoặc Logs của Render (lỗi phía backend/database). Mở cả hai lên trước khi đoán mò.
8. 🐞 Lỗi thường gặp
| Triệu chứng | Nguyên nhân | Cách sửa |
|---|---|---|
Trang trắng, Console báo gọi localhost:8080 thất bại |
Quên đổi REACT_APP_API_URL trước khi build; bundle vẫn trỏ localhost |
Đặt đúng biến rồi build lại & deploy lại (mục 2.3) |
Console báo blocked by CORS policy |
Backend chưa cho phép origin của frontend | Đặt CLIENT_URL = URL Vercel trong cors({ origin }) và trên dashboard Render (mục 3.4) |
Backend log MongooseServerSelectionError / Could not connect |
Atlas chưa whitelist IP, hoặc MONGO_URI sai password |
Network Access → thêm 0.0.0.0/0; kiểm tra password trong chuỗi URI (mục 3.2) |
Backend crash ngay khi start, log undefined is not a valid secret hoặc tương tự |
Quên đặt biến môi trường trên dashboard | Vào Render → Environment → thêm đủ MONGO_URI, JWT_SECRET, CLIENT_URL |
| API trả JSON đúng nhưng web vẫn trống dữ liệu | Database Atlas rỗng (khác database local) | Tự tạo dữ liệu trên web deploy, hoặc mongorestore từ local lên Atlas (mục 3.2) |
F5 trên /thuc-don ra 404 |
Host chưa rewrite mọi route về index.html |
Vercel + CRA xử lý sẵn; nơi khác cần thêm rule rewrite /* → /index.html (mục 4.2) |
| Lần đầu tải web chậm ~30–50 giây rồi sau đó nhanh | Render free tier vừa "ngủ dậy" | Bình thường với bậc miễn phí; nâng gói trả phí nếu cần luôn-bật (mục 4) |
| Ảnh không lên / lỗi upload | Vẫn dùng Cloudinary levantuan/kkio3wiw của người khác |
Đổi sang tài khoản Cloudinary của bạn (mục 5) |
Deploy build fail, log babel: command not found |
Backend thiếu script build, hoặc babel nằm ngoài dependency được cài |
Thêm script build: "babel src -d dist" và đảm bảo babel-cli được cài (mục 3.3) |
9. 📝 Bài tập
Bài 1. Bạn build frontend, deploy lên Vercel, nhưng web trắng trang và Console báo request tới
http://localhost:8080/api/products bị lỗi. Bạn vào .env trên máy sửa REACT_APP_API_URL thành
URL Render rồi refresh trình duyệt — vẫn trắng trang. Vì sao? Sửa đúng như thế nào?
Vì với Create React App, biến REACT_APP_* được thay bằng giá trị và đóng băng vào bundle ngay
lúc npm run build. Sửa .env sau khi đã build không thay đổi được file JS đã phát hành —
trình duyệt vẫn chạy chuỗi localhost:8080 cũ đã nướng cứng.
Sửa đúng: đổi biến trước, rồi build lại (npm run build) và deploy lại. Trên Vercel, bạn
đặt REACT_APP_API_URL trong Environment Variables của dự án rồi bấm Redeploy để Vercel
build lại với biến mới. Kiểm chứng bằng cách tìm chuỗi domain backend trong build/static/js/main.<hash>.js.
Bài 2. Backend đã deploy lên Render, mở https://yotea-api.onrender.com/api/products bằng trình
duyệt thì thấy JSON bình thường. Nhưng frontend gọi API lại bị chặn với lỗi CORS. Nghịch lý ở đâu?
Không nghịch lý. Khi bạn gõ thẳng URL vào thanh địa chỉ, trình duyệt tải trực tiếp — không có khái niệm "origin khác", nên CORS không áp dụng. CORS chỉ kiểm khi một trang web ở domain A (frontend Vercel) dùng JavaScript gọi tới domain B (backend Render).
Vì backend gốc để cors() mở * sẽ không bị lỗi này; lỗi CORS xuất hiện khi bạn đã giới hạn
origin (mục 3.4) nhưng đặt sai giá trị CLIENT_URL (ví dụ thiếu https://, thừa dấu / cuối,
hoặc chưa cập nhật sau khi Vercel đổi URL). Sửa: đặt CLIENT_URL khớp chính xác origin của
frontend (không kèm đường dẫn, không dấu / thừa) rồi để Render deploy lại.
Bài 3. (suy ngẫm) Liệt kê ba lỗ hổng từ Bài 33 mà bắt buộc phải vá
trước khi deploy public, và giải thích vì sao lúc chạy localhost chúng "vô hại" nhưng lên internet
lại thành thảm hoạ.
Ba lỗ hổng nguy hiểm nhất cần vá trước:
updateInfocho tự phong admin (routes/users.jsthiếuisAdmin+const update = req.body): bất kỳ khách đăng nhập nào gửi{"role": 1}là thành admin.GET /api/userscông khai trả cảpassword: lộ toàn bộ email, sđt, địa chỉ, chuỗi băm mật khẩu.order/orderDetailgần như mở toang: ai cũng liệt kê và xoá đơn hàng của mọi khách.
Lúc chạy localhost, chỉ mình bạn truy cập được máy → không ai khai thác. Khi lên internet, URL
công khai cho cả thế giới — bất kỳ ai cũng gọi được API, dò được endpoint, và tự động hoá tấn công.
Nói cách khác, deploy không tạo ra lỗ hổng, nó chỉ mở cửa cho người khác chạm vào những lỗ hổng
vốn đã tồn tại. Đó là lý do checklist bảo mật (mục 5) phải xong trước nút Deploy.
Cùng nhóm gốc bệnh — mass assignment (const update = req.body) và không kiểm chủ sở hữu —
đã bàn ở Bài 33 và vá ở Bài 34.
📌 Tóm tắt
localhost= chính máy bạn; muốn cả thế giới vào phải đặt web lên máy chủ công khai có domain.- Ba mảnh Yotea tách ra ba nơi khi deploy: frontend (Vercel) · backend (Render) · database (Atlas).
npm run buildsinh thư mụcbuild/gồm file tĩnh đã bundle + minify, khác hẳn server devnpm start.- ⚠️
REACT_APP_API_URLbị nướng cứng vào bundle lúc build — phải đặt đúng backend thật trước khi build, đổi sau phải build lại. - Backend production: đọc config từ biến môi trường, database dùng MongoDB Atlas (nhớ whitelist IP), chạy bằng
node dist/pm2chứ khôngnodemon. - ⚠️ CORS production phải giới hạn
originvề đúng domain frontend, không đểcors()mở*. - Checklist trước deploy gom cả bảo mật (Bài 33) lẫn refactor (Bài 34): tách
.env, đổi secret, vá lỗ hổng, bỏnode_moduleskhỏi git, giới hạn CORS, đổi baseURL, đổi Cloudinary. - Lỗi deploy hay gặp: trắng trang do sai API URL · CORS bị chặn · quên đặt biến trên dashboard · Atlas chưa whitelist IP.
Từ khoá tra cứu thêm: create react app build, environment variables react production, MongoDB Atlas free tier, deploy express render, deploy react vercel, CORS origin whitelist, pm2 process manager
🎉 Bạn đã hoàn thành giáo trình!
Xin chúc mừng bạn! Từ Bài 01 đến đây, bạn đã đi trọn một vòng của một web full-stack thật:
- Backend: Express, Mongoose, CRUD, slug, bộ lọc query, quan hệ & populate, mã hoá mật khẩu, JWT, phân quyền, Swagger.
- Frontend: React, Router v6, layout, Tailwind, axios, Redux Toolkit, thunk, redux-persist, RTK Query.
- Nghiệp vụ: đăng nhập, danh sách & chi tiết sản phẩm, giỏ hàng, thanh toán, tài khoản, bình luận/đánh giá, trang quản trị.
- Trưởng thành nghề: bạn đã tự rà soát bảo mật một dự án thật, refactor nó, tự tay xây một chức năng end-to-end (Topping, rồi Voucher ở đồ án cuối), và giờ là đưa nó lên internet.
Quan trọng hơn cả kỹ thuật: bạn đã học cách đọc một codebase có thật — kể cả những chỗ nó làm sai — và biết vì sao sai, sửa thế nào. Đó là kỹ năng theo bạn suốt sự nghiệp, ở bất kỳ dự án nào.
Học tiếp gì bây giờ? Vài hướng tự nhiên nối từ những gì bạn vừa làm:
- TypeScript — thêm kiểu tĩnh cho cả BE lẫn FE, bắt lỗi ngay lúc gõ code thay vì lúc chạy. Nhớ đống bug "undefined is not a function" trong giáo trình? TypeScript chặn phần lớn chúng.
- Next.js — framework React "đàn anh" của CRA: server-side rendering, routing theo file, tốt cho SEO.
- Testing — Jest + React Testing Library (frontend) và Supertest (API). Dự án Yotea gần như không có test; viết test là bước trưởng thành lớn tiếp theo.
- Docker — đóng gói backend + database vào container để "chạy giống nhau trên mọi máy", tiền đề của deploy chuyên nghiệp.
Hãy lấy chính Yotea làm sân tập: chuyển nó sang TypeScript, viết test cho vài API, hay Docker hoá nó. Không có cách học nào nhanh bằng cải tạo một dự án mình đã hiểu tường tận.
Cảm ơn bạn đã đi cùng tới dòng cuối. Chúc bạn viết nên những sản phẩm thật của riêng mình. 🧋
All rights reserved