Bối cảnh
Portfolio kiêm nơi tổng hợp kiến thức: 30 bài viết, 13 bài system design, 109 thuật ngữ. Tự dựng toàn bộ chuỗi từ code tới production thay vì đẩy lên hosting dựng sẵn, để hiểu từng mắt xích.
Next.js 15 (pages router) · React 19 · MUI 5 · TypeScript. Nội dung bài viết
đọc từ markdown trong repo; dữ liệu project lấy từ API json-server-blog
tách riêng.
Vấn đề
Ba ràng buộc định hình kiến trúc:
- Build không được phụ thuộc backend. API nằm ở service riêng; nếu nó chết lúc build thì trang vẫn phải dựng được.
- Deploy lên VPS tự quản, không dùng PaaS — tự lo image, registry, rollout.
- Nội dung nặng không được kéo vào bundle chung. 13 bài system design (~2000 dòng) và 109 thuật ngữ mà lọt vào chunk dùng chung thì mọi trang đều gánh, kể cả trang chủ.
Giải pháp
Chống phụ thuộc backend. Mọi lời gọi API đi qua safeFetchJson — lỗi mạng
trả null thay vì ném, chỗ gọi tự fallback về mảng rỗng. SSG + ISR
(revalidate) nên trang phục vụ từ bản tĩnh, dữ liệu tự làm mới nền.
Đóng gói. Dockerfile multi-stage trên node:20-alpine: deps → builder
→ runner. Container chạy bằng user thường (uid 1001) chứ không phải root.
Dùng output: standalone của Next nên image chỉ chứa đúng file cần chạy.
Đường ra production. Jenkins chạy ngay trên máy local, tách hai môi trường
theo branch — staging cổng 3001, prod cổng 3000. Image gắn tag
<env>-<build-number> để biết chính xác bản nào đang chạy. Bước đẩy lên ghcr
có retry 3 lần vì đăng nhập registry thỉnh thoảng lỗi keychain.
Giữ bundle gọn. Barrel constants/index.ts cố ý không re-export dữ liệu
system design, kèm test canh để lỗi này không tái phát — đây là lỗi đã từng
xảy ra và không công cụ nào trong repo bắt được, chỉ đọc kích thước chunk
trong output next build mới thấy.
Kết quả
- 26 route, phần lớn dựng tĩnh
- 156 test trong 8 file, chạy khoảng 2 giây
- Trang chủ 341 kB First Load JS, phần dùng chung 249 kB
- Dữ liệu system design nằm ngoài chunk của trang chủ — đối chiếu trực tiếp bằng danh sách chunk mà trang chủ nạp
- Đang chạy thật trên VPS sau nginx