21 tháng 8, 2026 · 8 phút đọc
Deploy dự án Frontend lên Cloudflare Workers: từ số không tới tên miền riêng
Vài năm trước, đưa một trang React lên mạng nghĩa là thuê VPS, cài nginx, xin chứng chỉ SSL, dựng cron gia hạn nó, rồi cầu trời đừng ai ngó tới mình vào lúc 3 giờ sáng. Bây giờ phần lớn công đoạn đó gói lại thành một lệnh.
Bài này đi từ con số không tới một trang chạy trên tên miền riêng, có luôn CI/CD. Tớ chọn Cloudflare Workers thay vì Pages, và ngay phần đầu sẽ nói rõ vì sao — vì đây là chỗ nhiều hướng dẫn cũ trên mạng đang dẫn bạn đi sai đường.
Workers hay Pages?
Cloudflare có hai sản phẩm nghe rất giống nhau. Pages ra đời trước, sinh ra để phục vụ trang tĩnh. Workers là runtime chạy code ngay tại biên mạng lưới.
Ranh giới đó giờ đã mờ. Workers phục vụ được file tĩnh thông qua Static Assets, và Cloudflare khuyến nghị dùng Workers cho dự án mới. Workers Sites — cách cũ để đẩy file tĩnh lên Workers — đã bị khai tử từ Wrangler v4.
Cái được lớn nhất là bạn không còn phải ghép hai sản phẩm với nhau nữa. Một dự án vừa phục vụ file tĩnh, vừa chạy API, vừa đọc được D1, KV hay R2, cùng một lần deploy.
Nếu bạn đang đọc một bài hướng dẫn bảo tạo "Pages project" thì bài đó viết trước thời điểm Static Assets ra mắt. Không sai, nhưng là con đường đang bị bỏ dần.
Hiểu một request đi qua đâu
Nắm sơ đồ dưới đây thì mọi tuỳ chọn cấu hình phía sau tự khắc dễ hiểu. Đây là toàn bộ vòng đời của một request.
Điểm mấu chốt nằm ở nhánh bên phải: khi request khớp một file có thật trong thư mục assets, Cloudflare trả file đó ra luôn và code Worker của bạn không hề chạy. Những request kiểu này miễn phí và không giới hạn, kể cả trên gói Free.
Nói cách khác, một trang tĩnh thuần dù có bao nhiêu lượt xem cũng không đụng tới quota Workers. Chỉ khi request rơi vào nhánh bên trái — không khớp file nào — thì tuỳ chọn not_found_handling mới quyết định điều gì xảy ra.
Deploy trang đầu tiên
Bước 1 — Dựng dự án
Nếu bắt đầu từ đầu, để Cloudflare dựng sẵn khung cho bạn:
npm create cloudflare@latest -- my-app --framework=reactLệnh này tạo dự án Vite + React kèm sẵn file cấu hình Wrangler. Đổi --framework thành vue, svelte hay astro tuỳ bạn.
Nếu đã có dự án sẵn rồi thì không cần dựng lại, chỉ cần thêm Wrangler vào:
npm install --save-dev wranglerBước 2 — Viết file cấu hình
Tạo wrangler.jsonc ở thư mục gốc dự án. Với một SPA, ba dòng dưới đây là đủ:
{
"name": "my-app",
"compatibility_date": "2026-08-21",
"assets": {
"directory": "./dist/",
"not_found_handling": "single-page-application"
}
}Từng dòng có nghĩa gì:
name— tên Worker, cũng là phần đầu của URL.workers.devbạn nhận được.compatibility_date— chốt phiên bản runtime. Đặt ngày hôm nay lúc khởi tạo rồi để yên; nó giữ cho code của bạn không vỡ khi Cloudflare cập nhật runtime.directory— thư mục build. Vite là./dist/, một số framework khác dùng./build/hoặc./out/.not_found_handling— xử lý khi không khớp file nào, chính là nhánh trái trong sơ đồ trên.
Bạn cũng có thể dùng wrangler.toml nếu thích cú pháp TOML hơn — nội dung tương đương, chỉ khác cách viết.
Bước 3 — Đẩy lên
npm run build
npx wrangler deployLần đầu chạy, Wrangler mở trình duyệt để bạn đăng nhập Cloudflare. Sau đó là xong — bạn có một URL dạng my-app.<subdomain>.workers.dev đang chạy thật.
Từ lần thứ hai trở đi, Wrangler so sánh và chỉ gửi những file đã đổi, nên deploy thường mất vài giây chứ không phải upload lại toàn bộ.
Chọn cấu hình đúng với loại dự án
Ba nhóm dưới đây phủ gần hết các dự án frontend bạn sẽ gặp.
SPA — Vite, React Router, Vue Router
Dùng "not_found_handling": "single-page-application". Mọi đường dẫn không khớp file nào sẽ nhận về index.html kèm mã 200, để router phía client dựng lại giao diện.
Đây chính là lý do trang bạn vào từ đầu thì chạy, nhưng bấm F5 ở /san-pham/123 lại ra 404 nếu quên đặt dòng này.
Trang tĩnh — Astro, Hugo, VitePress
Dùng "not_found_handling": "404-page" và đảm bảo thư mục build có file 404.html. Khác biệt so với SPA nằm ở mã trạng thái: ở đây đường dẫn không tồn tại trả về đúng 404 thay vì 200.
Chi tiết nhỏ nhưng quan trọng cho SEO — trả 200 cho trang không tồn tại sẽ khiến Google index cả những URL rác.
Full-stack — Next.js có SSR, Nuxt, SvelteKit
Nhóm này cần một adapter để biến build output thành thứ Workers chạy được. Với Next.js, đó là @opennextjs/cloudflare:
npm create cloudflare@latest -- my-next-app \
--framework=next --platform=workersAdapter nhận build output của Next rồi chuyển sang định dạng Worker. Yêu cầu Wrangler từ 3.99.0 trở lên.
Lưu ý khi phát triển: next dev vẫn là cách làm việc thoải mái nhất. Muốn truy cập tài nguyên Cloudflare ngay trong next dev, gọi initOpenNextCloudflareForDev trong file cấu hình Next.
Chạy thử ở máy
Đừng deploy để kiểm tra. Wrangler chạy được ngay tại máy:
npx wrangler devĐiểm đáng giá là lệnh này chạy trên workerd — đúng runtime của production, không phải giả lập bằng Node. Nghĩa là những khác biệt về API sẽ lộ ra ngay ở máy bạn chứ không đợi tới lúc lên thật.
Nếu dự án dùng Vite, Cloudflare có plugin riêng để vite dev chạy thẳng trong runtime Workers, giữ được hot reload quen thuộc.
Biến môi trường và secret
Giá trị công khai khai báo thẳng trong file cấu hình:
{
"vars": {
"API_BASE": "https://api.vidu.com"
}
}Giá trị bí mật thì không bao giờ nằm trong file cấu hình:
npx wrangler secret put DATABASE_URLChỗ này rất nhiều người hiểu nhầm. Với một SPA thuần tuý, mọi biến VITE_* đã bị nhúng thẳng vào bundle JavaScript lúc build. Người dùng mở DevTools là đọc được. Đặt secret của Worker cũng không cứu được điều đó.
Secret chỉ thực sự kín khi có code Worker chạy phía server đọc nó. Nếu bạn cần gọi API bằng khoá bí mật, hãy để Worker đứng ra gọi hộ, đừng để trình duyệt gọi trực tiếp.
Gắn tên miền riêng
Thêm routes vào file cấu hình, với điều kiện tên miền đã nằm trong tài khoản Cloudflare của bạn:
{
"routes": [
{ "pattern": "app.vidu.com", "custom_domain": true }
]
}Bản ghi DNS và chứng chỉ SSL được tạo tự động. Không phải đụng tới certbot, không phải nhớ ngày gia hạn.
Tự động deploy mỗi lần push
Có hai đường. Đơn giản nhất là bật Workers Builds trên dashboard, gắn với repo GitHub hoặc GitLab — không cần viết dòng cấu hình nào.
Nếu muốn tự kiểm soát, hoặc pipeline của bạn còn chạy test trước khi deploy, dùng GitHub Actions:
name: Deploy Worker
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- uses: actions/checkout@v6
- name: Build & Deploy Worker
uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}Hai secret này khai báo trong phần Secrets của repo. Tuyệt đối đừng commit token vào repo — nó cho phép deploy Worker lên toàn bộ tài khoản của bạn.
Giới hạn nên nhớ trước khi vấp
Hạng mục | Gói Free | Gói Paid |
|---|---|---|
Số file mỗi lần deploy | 20.000 | 100.000 |
Dung lượng một file | 25 MiB | 25 MiB |
Request tới file tĩnh | Miễn phí, không giới hạn | Miễn phí, không giới hạn |
Mức 100.000 file cần Wrangler từ 4.34.0 trở lên mới dùng được.
Ngưỡng 25 MiB mỗi file là chỗ hay vấp nhất. Video hoặc ảnh gốc chưa nén rất dễ vượt. Cách xử lý gọn là đưa file nặng lên R2 rồi nhúng link, thay vì nhét vào thư mục build.
Vài lỗi hay gặp
F5 ở route con ra 404. Thiếu
not_found_handling, hoặc đặt nhầm404-pagecho một SPA.Sửa code mà web không đổi. Quên chạy
npm run buildtrước khi deploy — Wrangler đẩy nội dung thư mục build, không tự build hộ bạn.Deploy được nhưng trang trắng. Thường là
directorytrỏ sai thư mục. Mở thư mục đó xem cóindex.htmlkhông là biết ngay.Khoá API lộ trong bundle. Xem lại phần biến môi trường phía trên — biến build-time của SPA không phải là secret.
Vậy rốt cuộc được gì
Một dự án frontend chạy trên mạng lưới toàn cầu, HTTPS sẵn, không server để vá, không hoá đơn tăng theo lượt truy cập tĩnh, và rollback bằng vài cú click. Chi phí khởi điểm bằng không.
Bước tiếp theo đáng thử: thêm một main vào cấu hình rồi viết chút code Worker để xử lý /api/*. Lúc đó frontend và backend của bạn nằm chung một lần deploy — và đó mới là chỗ Workers thật sự khác biệt so với một CDN tĩnh thông thường.
Các con số và cú pháp trong bài được đối chiếu với tài liệu chính thức của Cloudflare tại thời điểm viết. Cloudflare thay đổi khá nhanh, nên khi làm hãy kiểm tra lại: