這一課完成後:你會知道 deployment 不是把資料夾上傳而已;能把 frontend 與 API 部署成同一個 production origin,使用 D1 取代本機 SQLite file、用 migration 管理 schema、用 platform secret 管理敏感值,並讓 GitHub push 觸發可追蹤的 build / deploy。
先拆掉一個錯誤期待:Production 不是「另一台 localhost」
你目前的本機架構大概是:
Production 可能完全不是一台永遠開著的傳統主機。Serverless / edge runtime 常常由平台決定 process lifecycle、storage、network 與 deployment。
所以部署前先問四件事:
程式在哪個 runtime 執行?
資料真正存在哪裡?
Secret 從哪裡注入?
每次新版本怎麼安全地上線?
這一課的具體 production 架構
我們採用:
↓ push
Cloudflare Workers Build
↓
Worker + Static Assets
↓ same origin
D1 database
Cloudflare 目前建議新 full-stack Workers 專案使用 Workers Static Assets,把 HTML / CSS / JavaScript 和 Worker 一起部署;舊的 Workers Sites 已被標示為 deprecated,不適合新專案。
Cloudflare Workers Static Assets
為什麼這裡不用把 frontend 和 API 拆成兩個 domain?
前一課已經學過 CORS。Production 如果能做到:
https://your-app.example/
https://your-app.example/api/...
Frontend 與 API 同 origin,很多 cookie / CORS 複雜度都會下降。
這不是說 multi-origin 架構錯,而是 beginner project 沒必要主動增加一個問題。
把專案整理成可部署結構
web-app-playground/
├─ public/
│ ├─ index.html
│ ├─ styles.css
│ └─ script.js
├─ src/
│ └─ index.js
├─ migrations/
├─ wrangler.jsonc
├─ package.json
├─ package-lock.json
├─ .gitignore
└─ .dev.vars.example
public/ 是瀏覽器會拿到的靜態資源;src/index.js 是 backend Worker;migrations/ 保存 database schema 變更歷史。
Local Node server 和 Worker 的入口不同
本機 Node lesson 用:
createServer((request, response) => {
// ...
});
Worker 則使用 request → response 模型:
export default {
async fetch(request, env) {
return new Response("Hello");
}
};
你前面學的 method、URL、validation、Auth、database boundary 都還在;改變的是 runtime adapter。
先建立最小 Worker API
export default {
async fetch(request, env) {
const url = new URL(request.url);
if (
request.method === "GET" &&
url.pathname === "/api/status"
) {
return Response.json({ ok: true });
}
return new Response("Not found", {
status: 404
});
}
};
這仍然是:
用 wrangler.jsonc 描述 production bindings
最小概念:
{
"$schema": "./node_modules/wrangler/config-schema.json",
"name": "norelyn-web-app-practice",
"main": "./src/index.js",
"compatibility_date": "2026-09-16",
"assets": {
"directory": "./public",
"run_worker_first": ["/api/*"]
},
"d1_databases": [
{
"binding": "DB",
"database_name": "norelyn-web-app-practice",
"database_id": "YOUR_DATABASE_ID"
}
]
}
run_worker_first 讓 /api/* 先交給 Worker;其他 static files 則交給 assets。
Cloudflare 的 Static Assets 設定目前支援用 assets.directory 指定靜態資料夾,並可用 routing 設定控制哪些路徑先進 Worker。
現在的 Workers 對 Node compatibility 比以前完整很多
如果 compatibility date 在 2026-08-04 之後,Cloudflare Workers 目前會預設啟用其 Node.js compatibility layer;node:crypto 等多個 built-in API 可使用。
但這仍然不代表所有 Node server code 都能 100% 不改直接搬進 Worker。像本機 node:sqlite 檔案資料庫就不是 production D1。
Production database:把 SQLite file 換成 D1 binding
Local:
const db = new DatabaseSync("data.sqlite");
Worker:
const result = await env.DB
.prepare("SELECT id, title, done FROM tasks")
.all();
SQL 心理模型保留:
prepared statement
bound values
rows / results
改變的是 database driver 與 storage backend。
先建立 D1 database
npx wrangler d1 create norelyn-web-app-practice
指令完成後,Cloudflare 會提供 database binding / id 資訊,把對應值放回 Wrangler configuration。
Schema 不要靠「server 啟動時順便 CREATE TABLE」
Production 應該把 schema 變更變成明確 migration。
例如:
npx wrangler d1 migrations create \
norelyn-web-app-practice \
create_initial_schema
會建立版本化 SQL file。內容可以包含:
CREATE TABLE users (...);
CREATE TABLE sessions (...);
CREATE TABLE tasks (...);
Migration 的價值不是只有「建立 table」,而是留下:
Local database 和 remote database 要刻意分開
Cloudflare D1 支援 local development。你可以先在本機對 local D1 套 migration、跑 Worker,再決定何時套到 remote production。
不要因為「只是改一欄」就直接手動在 production dashboard 改 schema,然後 Git 裡完全沒有紀錄。
Production secret 不放 Git,也不放 wrangler vars
如果未來接第三方 API,需要 secret:
npx wrangler secret put THIRD_PARTY_API_KEY
Cloudflare 官方也明確提醒:敏感值不要放在一般 vars 設定,應使用 secrets。
Local development 則可以使用不 commit 的 .dev.vars 或 .env。
建立 .dev.vars.example,而不是把真正 secret commit
# .dev.vars.example
THIRD_PARTY_API_KEY=replace-me
APP_ENV=development
.gitignore:
.dev.vars
.env
.dev.vars.*
!.dev.vars.example
Repository 要能告訴下一個開發者「需要哪些設定」,但不能包含真正值。
把 frontend fetch 改成 same-origin relative URL
Local cross-origin 時你可能寫:
fetch("http://localhost:3000/api/tasks", {
credentials: "include"
});
Production same-origin 更適合:
fetch("/api/tasks", {
credentials: "same-origin"
});
這也避免把 production hostname 寫死在 frontend source。
Session cookie 在 HTTPS production 要補 Secure
本機 HTTP:
HttpOnly; SameSite=Lax; Path=/
Production HTTPS:
HttpOnly; Secure; SameSite=Lax; Path=/
Deployment 不是只確認首頁能開;Auth cookie 的屬性也要依 production protocol 調整。
先用 Wrangler 本機跑 production-like runtime
npx wrangler dev
至少驗證:
/ 能載入 static frontend。
/api/status 真的進 Worker。
D1 local binding 可查詢。
Register / login / logout 邏輯仍然成立。
沒有 secret 出現在 frontend bundle 或 log。
第一次 deploy 前,先把 Git 狀態整理乾淨
git status
git diff
git add .
git commit -m "prepare production deployment"
git push
再次檢查:
沒有 .env / .dev.vars。
沒有 local database file。
migration files 有進 Git。
wrangler config 沒有真正 secret。
所有 production-required files 都已 commit。
Cloudflare 可以直接連 GitHub 自動部署
Workers Builds 目前支援連 GitHub / GitLab;連接 repository 後,production branch 的 push 可以自動觸發 build 與 deploy。
Cloudflare 目前預設 deploy command 是:
npx wrangler deploy
而 non-production branch 可以建立 preview version,讓你在 main 前先驗證。
Auto deploy 很方便,但不要把 main 當測試按鈕
合理流程:
小型個人專案可以簡化,但核心觀念不變:source control 是 production change history。
Database migration 和 app deploy 是兩個不同動作
這點很容易忽略。
假設新程式開始查:
users.display_name
但 production DB 還沒有那一欄,新程式就會壞。
因此每次 schema change 都要問:
舊程式能不能先相容新 schema?
新程式上線時 database 是否 ready?
不要把「Git push 成功」誤認成「database 一定同步完成」。
部署完成後,做 smoke test
至少測:
首頁能開。
GET /api/status 回 200。
註冊新帳號。
登入後 /api/me 正確。
建立 task。
重新整理後 task 還在。
登出後 protected route 回 401。
user A 看不到 user B 的資料。
Network panel 沒有 unexpected 500。
看 production logs,但不要把 log 當資料庫
部署後的錯誤要從 server-side logs 找 request id、path、status 與 internal error。
但仍然不能 log:
原始 password。
完整 session token。
API key。
完整敏感 request body。
如果 deployment 壞了,先判斷是哪一層
Deploy 成功但 404 → routing / assets / Worker path
API 500 → runtime / secret / database
SQL error → migration / binding / query
Auth 失敗 → cookie / HTTPS / session / DB
Frontend 空白 → browser JS / fetch / render
「網站壞了」太模糊。先定位哪一層失敗。
Production URL 不是最後一步,還有 domain 與 HTTPS
Workers 會先提供可用的部署網址。之後如果接 custom domain,仍要重新檢查:
Frontend fetch 是否仍用 relative URL。
Cookie domain / Secure / SameSite 是否合理。
允許的 origin / CSRF policy 是否需要更新。
Redirect URL 或第三方 OAuth callback 是否需要換成 production domain。
不要把 provider-specific code 混到所有 business logic 裡
理想上,你可以讓這些概念保持清楚:
Worker fetch / D1 / env / assets → platform adapter
這樣未來如果換 deployment provider,不是整個 app 從頭重寫。
小挑戰:完成一次真正的 production deploy
需求 1:Frontend 和 API 使用同一個 production origin。
需求 2:Static assets 與 Worker 一起部署。
需求 3:建立 D1 database binding。
需求 4:Schema 透過 versioned migration 建立。
需求 5:Production secret 不出現在 Git。
需求 6:GitHub push 能觸發可追蹤的 deployment。
需求 7:Production 至少完成 register → login → create task → refresh → logout 的 smoke test。
需求 8:建立第二個帳號,驗證 owner boundary。
最後收斂:Deployment 是把系統放進另一個環境
↓ build / deploy
Production runtime
↓ bindings / secrets
Production database
↓ HTTPS domain
Real browser requests
↓ logs / monitoring / rollback
你真正部署的是一組互相依賴的系統,不是一個 HTML file。
下一課是 Web App Final:不再新增大主題,而是驗收你能不能從零完成一個有 frontend、API、database、Auth、permissions、secrets、migration 與 deployment 的小型產品。
完成條件:你能說明 local / production runtime 的差異,將 static frontend、API、D1、secrets 與 Git deployment 串起來,並能用 migration 與 smoke test 驗證 production app 真的可用,而不是只有「部署顯示成功」。