外觀

語言

· 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 會跑兩步:

  1. Build command(選填):Astro 填 npm run build。
  2. 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。