Cloudflare Pages hosts static frontends and Cloudflare Workers runs serverless functions at the edge, both with generous free tiers that cover most side projects, internal tools, and even light production traffic. No EC2, no NAT Gateway, no ALB. This walkthrough covers deploying a static site to Pages, backing it with a Workers API, and wiring both into a GitHub Actions pipeline for automatic deploys.
Prerequisites
- A Cloudflare account (free tier is sufficient)
- A domain added to Cloudflare, or use the default
*.pages.dev/*.workers.devsubdomain - Node.js 18+ and
npminstalled locally wranglerCLI:npm install -g wrangler- A GitHub repository for the project
- Cloudflare API token with
Pages:EditandWorkers Scripts:Editpermissions
Step 1: Authenticate wrangler
wrangler is Cloudflare’s CLI for deploying Workers and Pages, and for local development against the edge runtime.
wrangler login
This opens a browser flow that stores an OAuth token locally. For CI, skip this and use an API token instead (Step 5).
Verify the account is linked:
wrangler whoami
Step 2: Scaffold a Worker
Create a new Worker project. This becomes your API backend.
npm create cloudflare@latest my-api -- --type=hello-world --lang=ts
cd my-api
The generated wrangler.toml controls routing, environment variables, and bindings:
name = "my-api"
main = "src/index.ts"
compatibility_date = "2026-06-01"
[vars]
ENVIRONMENT = "production"
A minimal Worker handler:
export default {
async fetch(request: Request): Promise<Response> {
const url = new URL(request.url);
if (url.pathname === "/health") {
return new Response(JSON.stringify({ status: "ok" }), {
headers: { "content-type": "application/json" },
});
}
return new Response("Not found", { status: 404 });
},
};
Deploy it directly to confirm the setup works before wiring CI:
wrangler deploy
Verify:
curl https://my-api.<your-subdomain>.workers.dev/health
Note: the free tier includes 100,000 requests per day per account across all Workers, reset daily. CPU time per invocation is capped at 10ms on the free plan, which is enough for routing, auth checks, and lightweight JSON responses but not for heavy compute.
Step 3: Scaffold the Pages site
Pages serves static assets (HTML, JS, CSS, images) directly from Cloudflare’s CDN, with build integration for frameworks like Astro, Next.js, or plain Vite.
npm create vite@latest my-site -- --template vanilla-ts
cd my-site
npm install
npm run build
Deploy the built output directly with wrangler, or connect the repo through the Cloudflare dashboard for git-based deploys (recommended, since it gives you preview URLs per branch):
wrangler pages deploy dist --project-name=my-site
Step 4: Connect Pages to the Worker API
Rather than hardcoding the .workers.dev URL in the frontend, use a Pages Function to proxy /api/* requests to the Worker. This keeps everything under one domain and avoids CORS entirely.
Create functions/api/[[path]].ts in the Pages project:
export const onRequest: PagesFunction = async (context) => {
const url = new URL(context.request.url);
const apiUrl = `https://my-api.<your-subdomain>.workers.dev${url.pathname.replace("/api", "")}`;
return fetch(apiUrl, context.request);
};
Redeploy Pages and the API is now reachable at https://my-site.pages.dev/api/health with no separate CORS configuration needed.
Step 5: Automate deploys with GitHub Actions
Generate a scoped API token in the Cloudflare dashboard (My Profile > API Tokens > Create Token, using the “Edit Cloudflare Workers” template as a base, then add Pages edit permission). Store it as a GitHub secret named CLOUDFLARE_API_TOKEN, along with CLOUDFLARE_ACCOUNT_ID.
name: Deploy to Cloudflare
on:
push:
branches: [main]
jobs:
deploy-worker:
runs-on: ubuntu-latest
defaults:
run:
working-directory: my-api
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- run: npx wrangler deploy
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
deploy-pages:
runs-on: ubuntu-latest
needs: deploy-worker
defaults:
run:
working-directory: my-site
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- run: npm run build
- run: npx wrangler pages deploy dist --project-name=my-site
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
Every push to main now deploys the Worker first, then the Pages site. Ordering matters here since the Pages Function proxy depends on the Worker being live.
Verify the pipeline by checking the Actions run and then hitting the health endpoint:
curl https://my-site.pages.dev/api/health
Troubleshooting
Pages Function returns 522 or timeout. The proxy fetch inside
functions/api/[[path]].tsis calling the wrong Worker URL, usually a typo in the subdomain or a Worker that failed to deploy. Confirm withwrangler deployments listin the Worker project.
Free tier CPU limit exceeded (
Error 1102). The Worker is doing more than routing and light JSON handling, JWT verification with expensive crypto, or synchronous loops over large payloads. Move heavy compute to a paid plan with the 50ms+ CPU limit, or offload to a queue-backed Worker.
GitHub Actions deploy succeeds but the site is stale. Cloudflare’s CDN cache can serve the previous asset version for a few minutes after deploy. Purge cache manually from the dashboard, or set
Cache-Control: no-cacheonindex.htmlspecifically while allowing long-lived caching on hashed asset filenames.
wrangler deployfails with 10021 “no compatibility_date set”. Add acompatibility_datetowrangler.toml. Cloudflare requires this to lock in runtime behavior and avoid silent breaking changes on future deploys.
Summary
Cloudflare Pages and Workers together give you a static frontend, an edge API, and a CDN, all on the free tier with no servers to patch or scale. The GitHub Actions pipeline above deploys both on every push to main, with the Worker going first so the Pages Function proxy always has a live backend to call. This setup comfortably handles side projects, internal dashboards, and low-to-moderate traffic production apps. Once request volume or CPU time outgrows the free tier, the same wrangler.toml and Actions workflow carry over unchanged, the only difference is a billing plan switch in the Cloudflare dashboard.
Was this article helpful?
Need help setting this up for your team?
nerdSolv designs and deploys cloud-native infrastructure on Azure, AWS, and GCP.
Talk to us →