這一課完成後:你會知道 deployment 不是把資料夾上傳而已;能把 frontend 與 API 部署成同一個 production origin,使用 D1 取代本機 SQLite file、用 migration 管理 schema、用 platform secret 管理敏感值,並讓 GitHub push 觸發可追蹤的 build / deploy。

先拆掉一個錯誤期待:Production 不是「另一台 localhost」

你目前的本機架構大概是:

Browser → localhost:3000 → Node process → data.sqlite

Production 可能完全不是一台永遠開著的傳統主機。Serverless / edge runtime 常常由平台決定 process lifecycle、storage、network 與 deployment。

所以部署前先問四件事:

程式在哪個 runtime 執行?

資料真正存在哪裡?

Secret 從哪裡注入?

每次新版本怎麼安全地上線?

這一課的具體 production 架構

我們採用:

GitHub repository
↓ 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。

概念可重用 ≠ bootstrap code 原封不動可重用

先建立最小 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
    });
  }
};

這仍然是:

Method + Path → Route → Logic → HTTP Response

用 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。

Workers Node.js compatibility

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 心理模型保留:

SELECT / INSERT / UPDATE / DELETE
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。

Cloudflare D1 Get started

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」,而是留下:

哪一次 schema 改了什麼 → 可以 review → 可以在不同環境重現

Local database 和 remote database 要刻意分開

Cloudflare D1 支援 local development。你可以先在本機對 local D1 套 migration、跑 Worker,再決定何時套到 remote production。

不要因為「只是改一欄」就直接手動在 production dashboard 改 schema,然後 Git 裡完全沒有紀錄。

Migration file in Git → local test → production apply

Production secret 不放 Git,也不放 wrangler vars

如果未來接第三方 API,需要 secret:

npx wrangler secret put THIRD_PARTY_API_KEY

Cloudflare 官方也明確提醒:敏感值不要放在一般 vars 設定,應使用 secrets。

Local development 則可以使用不 commit 的 .dev.vars 或 .env。

Wrangler configuration

建立 .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 前先驗證。

Workers Git integration

Auto deploy 很方便,但不要把 main 當測試按鈕

合理流程:

Feature branch → local test → commit → push → preview / checks → merge main → production deploy

小型個人專案可以簡化,但核心觀念不變:source control 是 production change history。

Database migration 和 app deploy 是兩個不同動作

這點很容易忽略。

假設新程式開始查:

users.display_name

但 production DB 還沒有那一欄,新程式就會壞。

因此每次 schema change 都要問:

Migration 何時 apply?
舊程式能不能先相容新 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 壞了,先判斷是哪一層

Build 失敗 → dependency / config / syntax
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 裡

理想上,你可以讓這些概念保持清楚:

Route / validation / auth rules / permission rules → application 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 是把系統放進另一個環境

Source code in Git
↓ 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 真的可用,而不是只有「部署顯示成功」。