· 17 分鐘閱讀 · Cloudflare Workers 架站 教學
Cloudflare Workers 教學 2026:Astro 部署與 wrangler 設定
Cloudflare Workers 教學(2026):用 Workers Builds 部署 Astro SSR,wrangler.jsonc 逐欄說明、secrets 與建置變數、邊緣快取、log,附真實設定與踩過的錯。
Cloudflare Workers 是 Cloudflare 的 serverless 平台,程式跑在它全球的機房,不用自己管主機。2026 年的上手流程:用 npm create cloudflare@latest 建專案,或在既有的 Astro 專案跑 npx astro add cloudflare;設定寫在 wrangler.jsonc;部署用 npx wrangler deploy,想 push 就自動上線,可以接 Workers Builds;機密用 wrangler secret put,log 打開 observability 之後在後台查,或用 npx wrangler tail 即時看。不少舊教學還在用的 wrangler publish,Wrangler v4 已經拿掉了。
我的網站 kelvinwang.info 就是 Astro SSR 跑在 Workers 上,版本是 Astro 6、@astrojs/cloudflare 13、Wrangler 4,文中的設定都摘自這個 repo,機密值一律不寫。平台行為以 Cloudflare 和 Astro 官方文件為準,查核日 2026-11-03。
寫這篇時 npm 上最新是 Astro 7.4、@astrojs/cloudflare 14.4;我的網站還在 Astro 6/adapter 13。下面用到的 main、handle()、cloudflare:workers 寫法在 v14 文件裡仍然成立,新專案以官方文件為準。
Workers 是什麼、適合做什麼
Workers 在 V8 isolate 裡執行你的程式。isolate 是很輕的沙盒,同一個 runtime 可以同時跑幾百、幾千個,啟動也快(How Workers works)。官方列的用途有四類:前端靜態資源、後端 API、AI 推論、排程和背景工作(Workers 總覽)。
我的網站三類都用到:
- 公開頁面(首頁、文章、關於)在建置時就產生成靜態檔,當 static assets 送出。
- 登入後才能用的後台,和一個每頁都要讀 Supabase 的區塊,由 Worker 即時渲染(SSR)。
- 另一支背景 Worker
studio-social,靠 Cron Triggers 和 Queues 發 Threads、X 貼文。
要 24 小時常駐的服務放不進 Workers,像 n8n 這種長駐伺服器。這類服務怎麼安排,我在 Zeabur 遷移指南寫過,要放 VPS 或 Containers。
為什麼選 Cloudflare 加 Astro,我在 Day 1 和 Day 2 寫過。
舊教學哪裡過時
搜尋「Cloudflare Workers 教學」,常會看到幾年前的文章,有幾處已經跟現在不一樣:
wrangler publish已移除。 v3 標成 deprecated,v4 正式拿掉,改用wrangler deploy(v3 → v4 遷移說明、deprecations)。node_compat也一起移除,改用nodejs_compat相容旗標。- 設定檔建議用
wrangler.jsonc。wrangler.toml還能用,但官方建議新專案用 JSON,有些新功能只支援 JSON 設定檔(Configuration)。 - Astro 的 Cloudflare adapter 不再支援 Pages。 Astro 6 的升級說明寫明 adapter 移除了 Cloudflare Pages 支援,建議改部署到 Workers(Astro Cloudflare adapter)。
Astro.locals.runtime.env已移除。 環境變數改成import { env } from 'cloudflare:workers'。
最後一點我自己踩過:2026 年 6 月 11 日,登入檢查還從 locals.runtime.env 讀變數,一讀就出錯,當天改成從 cloudflare:workers 讀(commit 75ba367)。
建立專案:Astro 加 Cloudflare adapter
新專案用 C3 建,它會跑 Astro 的建立工具,再幫你設定好 Cloudflare:
npm create cloudflare@latest -- my-astro-app --framework=astro
既有的 Astro 專案,加 adapter:
npx astro add cloudflare
這個指令會把 output 設成 'server',所有頁面預設都在 Worker 裡渲染;不需要即時渲染的頁面,在頁面加 export const prerender = true 改成建置時產生(Cloudflare 的 Astro 指南)。Astro 6 以上需要 Node.js 22.12.0 以上;Workers Builds 預設的 Node 版本符合這個要求。
我的 astro.config.mjs(節錄,註解略改):
export default defineConfig({
// SSR by default so middleware always runs for signed-in routes.
// Public pages opt back into prerendering with `export const prerender = true`.
output: 'server',
site: 'https://kelvinwang.info',
adapter: cloudflare({ imageService: 'compile' }),
vite: {
// One id per build: the edge-cache key carries it.
define: { __BUILD_ID__: JSON.stringify(Date.now().toString(36)) },
// twitter-text's bare punycode import would otherwise become a Node builtin
// that Cloudflare's runtime cannot load.
resolve: { alias: [{ find: /^punycode$/, replacement: 'punycode/' }] },
},
});
output: 'server'讓 middleware 每次都會跑,後台的登入檢查靠它。公開頁面再一頁一頁設prerender = true。imageService: 'compile':圖片在建置時處理,只用在預先產生的頁面;SSR 頁面不轉換圖片。__BUILD_ID__給邊緣快取當 key 用,用途在快取那一節。punycode的 alias:我用的twitter-text套件 import 了punycode,不處理的話,會被當成 Node 內建模組,Workers 上載入不了。
Astro 6 的 astro dev 和 astro preview 改用 Cloudflare 的 Vite plugin,直接在 Workers runtime(workerd)裡跑,本機和線上的行為比以前接近。但 workerd 不支援 CommonJS,有些套件在開發或建置時會出錯,Astro 文件有預先編譯的做法。
wrangler.jsonc 逐欄說明
這是我網站的 wrangler.jsonc,省略了 env 上方的三行註解、改寫了一行註解,staging 網域換成範例,裡面沒有機密:
{
"compatibility_date": "2026-06-10",
"compatibility_flags": ["global_fetch_strictly_public"],
"name": "personal-website",
"main": "./src/worker.ts",
"assets": {
"directory": "./dist",
"binding": "ASSETS",
// Pages (not hashed bundles) go through src/worker.ts first for redirects.
"run_worker_first": ["/*", "!/_astro/*", "!/icons/*"]
},
"observability": {
"enabled": true
},
"env": {
"staging": {
"name": "personal-website-staging",
"routes": [{ "pattern": "staging.example.com", "custom_domain": true }],
"observability": { "enabled": true }
}
}
}
name、main、compatibility_date 是部署必填的三個欄位。compatibility_date 決定 runtime 用哪一天的行為,建專案時填當天日期,之後要用新行為再往後調(Configuration)。
compatibility_flags 個別開關 runtime 功能。我開的 global_fetch_strictly_public 讓 fetch() 打自己網域時,跟一般網路請求一樣從 Cloudflare 前門進來;沒開的話,請求會直接送到該網域的源站,跳過綁在那個網址上的 Worker 和安全設定(Compatibility flags)。
main 是 Worker 的入口。從 Astro 6 起,做法是指向 adapter 提供的 @astrojs/cloudflare/entrypoints/server,或指向自己寫的入口檔,在裡面呼叫 @astrojs/cloudflare/handler 的 handle()(Astro adapter 升級說明)。Cloudflare 的 Astro 指南在手動設定的範例裡還寫 ./dist/_worker.js/index.js,兩份文件寫法不一樣,我是照 Astro 的文件做。adapter v14 另外提供 @astrojs/cloudflare/fetch 的 cf() 和 finalize(),給想在自己的入口檔裡用 astro/fetch 組路由的人用(Astro adapter)。
我一開始用 adapter 的入口,2026 年 9 月 29 日改成自己的 src/worker.ts(commit 17b926d)。原因是 prerender 出來的頁面不會經過 Astro 的 middleware,舊網址的轉址只能在 Worker 入口處理。簡化版如下(完整檔案還有 staging 的 noindex 標頭和給背景 Worker 用的 entrypoint):
import { handle } from '@astrojs/cloudflare/handler';
import { edgeCached, isCacheablePage } from '@/lib/edge-cache';
export default {
async fetch(request, env, ctx) {
const url = new URL(request.url);
const r = redirectFor(url, request.headers.get('cookie'), request.headers.get('accept-language'));
if (!r) return isCacheablePage(url)
? edgeCached(request, ctx, () => handle(request, env, ctx))
: handle(request, env, ctx);
return new Response(null, { status: 301, headers: { location: r.location } });
},
};
assets 指定靜態檔的資料夾和 binding 名稱。run_worker_first 預設是 false:網址對得到靜態檔,就直接回傳,不跑 Worker。我改成路徑清單,除了有 hash 的 /_astro/* 和 /icons/*,其他請求都先進 Worker,才能做轉址。! 開頭是排除規則,優先於一般規則(Static assets binding)。
這樣設有成本。官方文件寫,靜態資源的請求免費、不限量,但符合 run_worker_first 的請求一定會執行 Worker,按 Workers 價格計費;Free 方案超過每日請求額度時,這些請求會收到 429,不會退回去送靜態檔(Billing and limitations)。清單只放真的需要 Worker 先處理的路徑。
observability.enabled 打開 Workers Logs,細節在 log 那一節。
env.staging 是第二個環境。每個環境部署出來都是一支獨立的 Worker,名稱預設是 <名稱>-<環境>(Environments)。routes 加 custom_domain: true,Cloudflare 會幫這個子網域建好 DNS 記錄和憑證(Custom Domains)。要注意,vars 這類 binding 不會從上層繼承,每個環境都要自己寫一份;我的背景 Worker 在 staging 底下就把 vars 重寫了一次。
用 Astro 這類走 Cloudflare Vite plugin 的框架,環境要在建置時用 CLOUDFLARE_ENV 選;到 wrangler deploy 才設沒有作用(Vite plugin 的 Cloudflare environments)。所以我的 staging 是這樣部署:
CLOUDFLARE_ENV=staging npm run build && npx wrangler deploy
npm run build 會產生 dist/server/wrangler.json,這是攤平後的最終設定;npx wrangler deploy 會讀這一份,不是讀根目錄的 wrangler.jsonc。
環境變數與 secrets:分清三種
我的網站把變數分成三類:
| 類型 | 在哪設定 | 什麼時候讀得到 | 我的例子 |
|---|---|---|---|
| 建置變數 | 後台 Worker → Settings → Build 的 Build variables and secrets | 只在建置時 | PUBLIC_SUPABASE_URL、PUBLIC_SUPABASE_ANON_KEY |
| 執行期變數 | wrangler.jsonc 的 vars |
執行時,明文 | 背景 Worker 的 STUDIO_FACEBOOK_GRAPH_VERSION |
| Secrets | npx wrangler secret put <KEY> |
執行時,設定後在後台和 Wrangler 都看不到值 | DEPLOY_HOOK_URL、SUPABASE_SERVICE_ROLE_KEY |
依據:Workers Builds 的建置變數在執行時讀不到,執行期變數要另外到 Settings → Variables & Secrets 設(Builds configuration);vars 不加密,適合放一般設定(Environment variables);secret 也是環境變數,差別是設定之後看不到值(Secrets)。
我為什麼需要建置變數:文章頁是 prerender,建置時要從 Supabase 讀文章。Astro 的 import.meta.env 在建置時就會被替換成實際的值(Astro 環境變數),Workers Builds 的建置環境一定要有這兩個變數(我的 README 有記);沒設的話,文章頁在建置時讀不到 Supabase,建置就會失敗。
執行期在程式裡這樣讀:
import { env } from 'cloudflare:workers';
const value = env.SOME_VAR;
幾個官方文件寫到、容易漏掉的地方:
wrangler secret put會建立新版本並立刻部署。用 gradual deployments 的話,改用wrangler versions secret put。- 在後台改過的環境變數,下一次
wrangler deploy會被設定檔蓋掉。要保留後台改的值,在設定檔加keep_vars: true。 - 本機開發的 secrets 放
.dev.vars或.env,兩個只能擇一;有.dev.vars時,.env的值不會進到env。兩種檔案都不要進 git,我的.gitignore擋掉了全部的.env*,只留下.env.example。 - 改了設定檔就跑一次
wrangler types,產生env的 TypeScript 型別。我包成npm run generate-types。
部署:Workers Builds 與 wrangler deploy
接 GitHub,push 就部署
既有的 Worker 到後台 Workers & Pages → 選 Worker → Settings → Builds → Connect,選好 repo 和分支(Builds)。每次 push,Workers Builds 會跑兩步:
- Build command(選填):Astro 填
npm run build。 - Deploy command:預設
npx wrangler deploy,會用package.json裡指定的 Wrangler 版本。
後台 Worker 的名稱必須跟 wrangler.jsonc 的 name 一樣,不然建置會失敗。有開預覽建置的話,非正式分支會改跑 Preview command(預設 npx wrangler preview),產生預覽網址,不會動到正式環境。
我的網站從 2026 年 9 月 29 日接上 Workers Builds,之後 push 到 main 就是部署。我把這件事寫進 repo 的 CLAUDE.md,讓 AI 工具一進專案就知道 push 等於上線,也知道手動部署的備案。CLAUDE.md 怎麼寫,可以看我的 Claude Code 教學。
確認部署有沒有成功,我看 GitHub commit 上的 check run「Workers Builds: personal-website」,失敗再到後台 Deployments → View build history 看 build log(保留 90 天)。我沒辦法用 CLI 讀 build log,所以要重現失敗時,就另外 clone 一份乾淨的 repo、不放 .env 來建置。
2026 年 10 月 30 日有一次 push 之後沒有觸發建置,我推了一個空 commit 重新觸發(commit 51eeb63)。
Deploy Hooks:不 push 也能重建
Settings → Builds → Deploy Hooks 可以建一個網址,對它送 POST 就會觸發指定分支的建置(Deploy Hooks)。我網站的文章發布流程,在有設定 Deploy Hook 時會打這個網址,讓靜態文章頁重建。
curl -X POST "https://api.cloudflare.com/client/v4/workers/builds/deploy_hooks/<DEPLOY_HOOK_ID>"
這個網址不用另外驗證,網址本身就是憑證,所以 repo 的設計是把它存成 secret DEPLOY_HOOK_URL,不放在 vars。
手動部署:wrangler deploy
自動部署出問題時的備案:
npm run build && npx wrangler deploy
手動部署有兩條規則:
- 手動部署要從最新的 main。 落後 main 的 worktree 跑
wrangler deploy,會把線上較新的版本蓋掉;部署前先git fetch,從origin/main最新的 commit 建置。 - 第二支 Worker 不會跟著部署。 Workers Builds 只部署接了 repo 的那支 Worker。背景 Worker 要另外手動部署:
npx wrangler deploy --config workers/studio-social/wrangler.jsonc
網站和背景 Worker 共用 provider 程式,只部署其中一支,另一支會繼續跑舊程式。兩支要一起部署。
SSR 頁面怎麼做邊緣快取
Worker 在 Cloudflare 快取的前面執行(How the Cache works),Worker 自己產生的回應,預設不會被 CDN 快取,每個請求都會跑一次 Worker。如果每次渲染都要查資料庫,有兩種做法:
Cache API(caches.default) |
Workers Cache | |
|---|---|---|
| 怎麼開 | 在程式裡 cache.match()、cache.put() |
設定檔加 "cache": { "enabled": true } |
| 範圍 | 只存在當下那個機房,不會複製到別的機房 | 分上下兩層,下層在靠近使用者的機房,上層彙整全網 |
| 命中時 | Worker 還是會執行,只是不重新渲染 | 不執行 Worker,直接回傳 |
| 控制方式 | 自己決定 key 和存什麼 | 照回應的 Cache-Control |
來源:Cache API、Workers Cache。Workers Cache 沒有另外的價格,但一打開,所有打到這支 Worker 的請求都按 Workers 請求數計費,連平常免費的靜態資源請求和 service binding 呼叫也算;命中快取時不收 CPU 時間(Pricing)。另外,官方寫部署在自訂網域的 Worker 可以正常使用 Cache API;在後台編輯器和 Playground 的預覽裡,Cache API 沒有效果。
我用的是 Cache API,下面是我網站快取模組的簡化版(省略型別):
/** Seconds a 200 stays in the edge cache. */
export const EDGE_TTL = 900;
/** The header the pages send; browsers always revalidate. */
export const BROWSER_CACHE_CONTROL = 'public, max-age=0';
export async function edgeCached(request, ctx, render, cache = workersCache()) {
if (!cache || request.method !== 'GET') return render();
const url = new URL(request.url);
const key = new Request(`${url.origin}${url.pathname}?build=${BUILD}`);
const hit = await cache.match(key).catch(() => undefined);
if (hit) return withCacheControl(hit, BROWSER_CACHE_CONTROL);
const res = await render();
if (res.status === 200) {
ctx.waitUntil(cache.put(key, withCacheControl(res.clone(), `public, max-age=${EDGE_TTL}`)).catch(() => {}));
}
return res;
}
- 只存 200。資料庫出錯回 503、頁面不存在回 404,這些都不存,下一個請求重新渲染。
- 快取存 15 分鐘,所以同一個機房每一頁每 15 分鐘最多查一次 Supabase。送給瀏覽器的是
max-age=0,瀏覽器每次都重新驗證。 cache.put()放在ctx.waitUntil()裡,回應先送出去,寫快取在背景完成。- key 只用網址路徑,查詢字串不影響這些頁面。
key 裡的 build= 是 2026 年 10 月 25 日補上的(commit f8f3953)。原本的 key 只有網址,部署新版本之後,快取裡還是舊版的 HTML,指向舊版的 CSS 和 JS 檔名,那些檔案新版已經沒有了,最多 15 分鐘頁面會沒有樣式。現在每次建置產生一個新的 __BUILD_ID__,新部署自然讀不到舊快取。
這也省掉了清快取的麻煩:cache.delete() 只會清掉當下那個機房的資料(Cache API、How the Cache works),要全網清除得另外處理。我靠 build id 和 15 分鐘的 TTL,沒有另外寫清除快取的程式。
Cron Triggers 與 Queues:背景 Worker
背景 Worker studio-social 的設定(節錄):
{
"name": "studio-social",
"main": "src/index.ts",
"workers_dev": false,
"preview_urls": false,
"triggers": { "crons": ["* * * * *", "0 19 * * *"] },
"services": [{ "binding": "SITE", "service": "personal-website", "entrypoint": "StudioSocialInternal" }],
"queues": {
"consumers": [
{ "queue": "studio-social-publish", "max_batch_size": 1, "max_retries": 3,
"retry_delay": 60, "dead_letter_queue": "studio-social-publish-dlq" }
]
}
}
workers_dev: false和preview_urls: false:這支 Worker 沒有公開網址,只能透過 service binding 和排程觸發。- Cron Triggers 用 UTC 時間(Cron Triggers)。
0 19 * * *是台灣時間每天凌晨 3 點,跑資料清理和 token 更新;* * * * *每分鐘檢查有沒有該發的貼文。程式要有scheduled()handler 才會接到排程。 - 每分鐘的排程只負責把到期的工作丟進 Queue,每則訊息只處理一個步驟,失敗重試 3 次,再失敗就進 dead letter queue。
services讓背景 Worker 直接呼叫網站 Worker 匯出的StudioSocialInternal,不經過公開網址。所以要先部署網站,背景 Worker 才找得到這個 entrypoint。
Log 與除錯
wrangler.jsonc 加上 "observability": { "enabled": true } 再部署,Workers Logs 就會開始收 log:每次呼叫的紀錄、console.log、錯誤和未捕捉的例外,都能在後台 Worker 的 Logs 分頁查(Workers Logs)。新建立的 Worker 預設就開著 observability。保留天數依方案不同,官方表格寫 Free 3 天、Paid 7 天;2026 年 12 月 1 日起改用 Cloudflare Observability 的計價。
剛部署完想即時看請求,用:
npx wrangler tail
或後台 Logs 右側的 Live(Real-time logs)。流量大時,即時 log 可能會改成抽樣。
本機先用 npm run build 加 astro preview 在 workerd 裡跑一次,比直接上線再看 log 省事。不過有些差異只有 Workers runtime 才會出現,Node 底下的單元測試抓不到。我遇過最明顯的一次:
fetch(url, { redirect: 'error' }) 在 Node 可以用,在 Workers 會直接丟 TypeError,請求根本沒送出去。官方文件寫明 Workers 只實作 follow 和 manual(Request)。2026 年 10 月 8 日,網站上每一次 OAuth 交換都失敗、狀態碼是 0,原因就是這個。修法是改成 redirect: 'manual',再自己檢查 3xx(commit 1bca515),另外加了一個測試,掃原始碼確保沒有人再寫 redirect: 'error'。
費用與限制
價格和限制常調整,以官方頁面為準:Pricing、Limits。2026-11-03 查到的幾個重點:
- Free 方案每天 100,000 個請求,每次呼叫 10 ms CPU 時間。
- 靜態資源的請求免費、不限量;SSR 和
run_worker_first命中的請求才算 Worker 請求(沒開 Workers Cache 的前提下)。 - Free 方案每次請求最多 50 個 subrequest(Worker 對外發的
fetch,以及對 KV、R2、D1 等 Cloudflare 服務的呼叫)。
我的背景 Worker 在 2026 年 10 月 6 日是照 Free 方案的限制設計的:每則 Queue 訊息只做一個步驟,讓每次執行都在 subrequest 和 CPU 的上限內,README 也寫了,log 裡出現 CPU 超限錯誤就升級 Paid。整個網站一年的花費,我在 Day 4 算過;Supabase 的部分可以看 Supabase 費用怎麼算。
常見錯誤對照
| 症狀 | 原因 | 怎麼修 |
|---|---|---|
wrangler publish 不能用 |
Wrangler v4 已移除 | 改用 npx wrangler deploy |
| Workers Builds 第一次建置就失敗,建置時讀不到資料 | 建置變數沒設,執行期變數在建置時讀不到 | Settings → Build 加 Build variables |
讀 Astro.locals.runtime.env 出錯 |
Astro 6 移除了這個 API | 改用 import { env } from 'cloudflare:workers'(我遇過) |
設了 redirect: 'error' 的 fetch 全部失敗,狀態碼 0 |
Workers 不支援 redirect: 'error' |
改 redirect: 'manual',自己檢查 3xx(我遇過) |
| 部署後頁面短暫沒有樣式 | 邊緣快取還在送舊 build 的 HTML | 快取 key 加上 build id(我遇過) |
| push 之後沒有觸發建置 | 原因不明 | 推空 commit 或打 Deploy Hook 重新觸發(我遇過) |
| 建置失敗,提示 Worker 名稱不符 | 後台名稱和 wrangler.jsonc 的 name 不同 |
兩邊改成一樣 |
| 建置成功,網站卻是 404 或空白 | assets.directory 指錯資料夾,或沒跑 build command |
檢查 Build command 和 assets.directory |
npm ci 說 lockfile 不同步 |
lockfile 是較新的 npm 產生的 | 在 package.json 設 packageManager |
| 後台改的變數部署後不見了 | wrangler deploy 用設定檔覆蓋 |
設定檔加 keep_vars: true,或直接把變數寫進設定檔 |
後四列出自官方的 Troubleshooting builds 和 Configuration。
常見問題
Workers 和 Pages 該選哪個? 新的 Astro 專案選 Workers。Astro 的 Cloudflare adapter 已經移除 Pages 支援,Cloudflare 的 Astro 指南也是部署到 Workers。
wrangler.toml 還能用嗎?
能用。官方建議新專案用 wrangler.jsonc,有些新功能只支援 JSON 設定檔。
Free 方案夠用嗎?
看你有多少請求會執行到 Worker。沒開 Workers Cache 時,純靜態的頁面不算 Worker 請求;SSR 頁面和 run_worker_first 命中的路徑才算。超過每日額度時,run_worker_first 命中的請求會收到 429,其他 Worker 請求回 Error 1027。先估算這部分的流量,再對照官方的 Limits 頁。
wrangler deploy 和 Workers Builds 可以同時用嗎?
可以,兩種都會建立新版本並上線,後部署的會蓋掉先部署的。手動部署前先確認你建置的是最新的 main。