Backend5 min read

Deploy to Cloudflare Pages with GitHub Actions (and Skip the Build Limits)

How to set up CI/CD for Cloudflare Pages using GitHub Actions, bypassing the 500 builds/month limit on the free tier

Cloudflare Pages is an excellent free hosting platform for static sites. But there's a catch: the free tier limits you to 500 builds per month. If you're actively developing and pushing frequently, you can hit that limit fast.

The solution? Build on GitHub Actions (unlimited and free) and deploy the pre-built files directly to Cloudflare Pages.

Cloudflare Pages Free Tier Limits

Before we dive in, here's what you get for free:

FeatureFree Tier
Static hosting/bandwidthUnlimited
Builds per month500
Max deploy size1 GB
Max file size25 MB
Pages Functions100k requests/day

The key insight: only Cloudflare-initiated builds count against the 500 limit. Direct uploads via Wrangler CLI are unlimited.

The Strategy

Instead of:

GitHub Push → Cloudflare Builds → Cloudflare Deploys
                 (counts toward 500)

We do:

GitHub Push → GitHub Actions Builds → Wrangler Deploys
                 (unlimited free)        (doesn't count)

Step 1: Create the GitHub Actions Workflow

Create .github/workflows/deploy.yml in your repository:

name: Deploy to Cloudflare Pages
 
on:
  push:
    branches: [main]
  workflow_dispatch:
 
jobs:
  deploy:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      deployments: write
    steps:
      - uses: actions/checkout@v4
 
      - uses: pnpm/action-setup@v4
        with:
          version: 9
 
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: pnpm
 
      - name: Install dependencies
        run: pnpm install --frozen-lockfile
 
      - name: Build
        run: pnpm build
 
      - name: Deploy to Cloudflare Pages
        uses: cloudflare/wrangler-action@v3
        with:
          apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
          accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
          command: pages deploy dist --project-name=YOUR_PROJECT_NAME

Replace YOUR_PROJECT_NAME with your Cloudflare Pages project name.

If you use npm instead of pnpm, adjust the workflow:

- uses: actions/setup-node@v4
  with:
    node-version: 20
    cache: npm
 
- name: Install dependencies
  run: npm ci
 
- name: Build
  run: npm run build

Step 2: Create a Cloudflare API Token

  1. Go to Cloudflare Dashboard
  2. Click your profile icon → My Profile
  3. Navigate to API TokensCreate Token
  4. Use the "Edit Cloudflare Workers" template, or create a custom token with:
    • Permission: AccountCloudflare PagesEdit
  5. Copy the token (you won't see it again)

Step 3: Get Your Account ID

  1. Go to Cloudflare Dashboard → Workers & Pages
  2. Look at the right sidebar for Account ID
  3. Copy it

Step 4: Add Secrets to GitHub

  1. Go to your GitHub repository
  2. Navigate to SettingsSecrets and variablesActions
  3. Click New repository secret
  4. Add two secrets:
    • Name: CLOUDFLARE_API_TOKEN → Value: your API token
    • Name: CLOUDFLARE_ACCOUNT_ID → Value: your account ID

Step 5: Disable Cloudflare Auto-Build

To prevent Cloudflare from also building (and wasting your 500 limit):

  1. Go to Cloudflare Dashboard → Workers & Pages
  2. Select your Pages project
  3. Go to SettingsBuilds & deployments
  4. Find Automatic deployments and set it to Disabled

Alternatively, you can completely disconnect the Git repository from the same settings page.

How It Works

The cloudflare/wrangler-action does the heavy lifting. Under the hood, it runs:

npx wrangler pages deploy dist --project-name=YOUR_PROJECT_NAME

This uploads your pre-built dist folder directly to Cloudflare Pages. Since Cloudflare doesn't run the build, it doesn't count against your quota.

Local Deploys

You can also deploy manually from your machine:

# First time only - authenticate with Cloudflare
npx wrangler login
 
# Build your site
pnpm build
 
# Deploy
npx wrangler pages deploy dist --project-name=YOUR_PROJECT_NAME

Bonus: Preview Deployments for PRs

Want preview URLs for pull requests? Extend the workflow:

name: Deploy to Cloudflare Pages
 
on:
  push:
    branches: [main]
  pull_request:
    branches: [main]
  workflow_dispatch:
 
jobs:
  deploy:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      deployments: write
      pull-requests: write
    steps:
      - uses: actions/checkout@v4
 
      - uses: pnpm/action-setup@v4
        with:
          version: 9
 
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: pnpm
 
      - name: Install dependencies
        run: pnpm install --frozen-lockfile
 
      - name: Build
        run: pnpm build
 
      - name: Deploy to Cloudflare Pages
        id: deploy
        uses: cloudflare/wrangler-action@v3
        with:
          apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
          accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
          command: pages deploy dist --project-name=YOUR_PROJECT_NAME
 
      - name: Comment preview URL on PR
        if: github.event_name == 'pull_request'
        uses: actions/github-script@v7
        with:
          script: |
            github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: '🚀 Preview deployed to: ${{ steps.deploy.outputs.deployment-url }}'
            })

Cost Comparison

ApproachMonthly CostBuild Limit
Cloudflare Free$0500 builds
Cloudflare Pro$205,000 builds
GitHub Actions + Wrangler$0Unlimited

For most personal sites and small projects, the GitHub Actions approach is the clear winner.

Conclusion

By moving the build step to GitHub Actions, you get:

  • Unlimited builds for free
  • Faster builds (GitHub runners are often faster than Cloudflare's build system)
  • More control over your CI/CD pipeline
  • Preview deployments for pull requests

The only trade-off is a few minutes of initial setup. Worth it.