Wrangler CLI
Best for: Quick deploys, local testing
Setup time: 5 minutes
Auto-deploy: No (manual)
Run a single command from your terminal. Fastest way to get a site live.
You have a static site ready to go. Now you need to get it live on Cloudflare Pages with a custom domain, SSL, and automated deployments. This guide covers every deployment method, DNS configuration, and CI/CD setup.
Three ways to deploy to Cloudflare Pages, from simplest to most automated:
Wrangler CLI
Best for: Quick deploys, local testing
Setup time: 5 minutes
Auto-deploy: No (manual)
Run a single command from your terminal. Fastest way to get a site live.
Git Integration
Best for: Ongoing projects with a team
Setup time: 15 minutes
Auto-deploy: Yes (on push)
Connect GitHub/GitLab for automatic deployments on every push.
Dashboard Upload
Best for: One-off deploys, no Git
Setup time: 2 minutes
Auto-deploy: No (manual)
Upload files directly through the Cloudflare Dashboard.
The fastest way to deploy. Run a single command from your terminal.
Install Wrangler
npm install -g wrangler
# Or as a project dependencynpm install --save-dev wranglerAuthenticate
npx wrangler login# Opens browser -> authorize Cloudflare accountDeploy
# Deploy the public/ directory to Cloudflare Pagesnpx wrangler pages deploy public
# First deploy: creates the project (prompts for project name)# Subsequent deploys: updates the existing projectDeploy with options:
# Specify project name (skip prompt)npx wrangler pages deploy public --project-name=my-practice-site
# Deploy to a specific branch (creates preview deployment)npx wrangler pages deploy public --project-name=my-practice-site --branch=staging
# Deploy with a commit messagenpx wrangler pages deploy public --project-name=my-practice-site \ --commit-message="Updated contact page"Common package.json scripts:
{ "scripts": { "build": "npm run optimize:images && npm run build:site", "deploy": "npm run build && npx wrangler pages deploy public", "deploy:staging": "npm run build && npx wrangler pages deploy public --branch=staging", "cf:login": "npx wrangler login", "cf:deploy": "npx wrangler pages deploy public" }}Connect your GitHub or GitLab repository to Cloudflare Pages for automatic deployments on every push.
Go to Cloudflare Dashboard -> Workers & Pages -> Create
Select Pages tab -> Connect to Git
Authorize Cloudflare to access your GitHub/GitLab account
Select the repository
Configure build settings:
| Setting | Value | Notes |
|---|---|---|
| Project name | my-practice-site | Becomes my-practice-site.pages.dev |
| Production branch | main | Deploys from this branch go live |
| Framework preset | None (static) or Astro | Select your framework |
| Build command | npm run build | Or leave blank for static sites |
| Build output directory | public | Where your built files live |
| Root directory | / | Or subdirectory if monorepo |
Click Save and Deploy
After setup:
main — automatic production deploymentFor sites without Git, upload files directly through the Cloudflare Dashboard.
public/ folder contentsThe wrangler.toml file configures your Cloudflare Pages project. Place it in the project root.
# wrangler.toml - Cloudflare Pages configurationname = "my-practice-site"compatibility_date = "2026-02-11"pages_build_output_dir = "public"
# Optional: Cloudflare account ID (useful for CI/CD)# account_id = "your-account-id-here"
# Environment variables for build[vars]SITE_NAME = "My Medical Practice"SITE_URL = "https://www.mypractice.com"
# KV Namespace bindings (if using Cloudflare KV)# [[kv_namespaces]]# binding = "FORM_SUBMISSIONS"# id = "your-kv-namespace-id"
# D1 Database bindings (if using Cloudflare D1)# [[d1_databases]]# binding = "DB"# database_name = "my-database"# database_id = "your-database-id"| Field | Required | Description |
|---|---|---|
name | Yes | Project name (becomes <name>.pages.dev) |
compatibility_date | Yes | Cloudflare Workers runtime version date |
pages_build_output_dir | Yes | Directory containing built files to deploy |
account_id | No | Your Cloudflare account ID (auto-detected if logged in) |
[vars] | No | Environment variables available during build and in Functions |
[[kv_namespaces]] | No | Cloudflare KV storage bindings |
[[d1_databases]] | No | Cloudflare D1 database bindings |
Set these environment variables in your Cloudflare Pages project settings or wrangler.toml:
| Variable | Recommended Value | Why |
|---|---|---|
NODE_VERSION | 20 or 18 | Default 12.18.0 will break most modern builds |
NPM_VERSION | 10 | Matches Node.js 20 LTS |
HUGO_VERSION | 0.128.0 (if using Hugo) | Default Hugo is outdated; pin your version |
[vars]NODE_VERSION = "20"NPM_VERSION = "10"Go to Workers & Pages > Your Project > Settings > Environment Variables > Add NODE_VERSION = 20 for both Production and Preview.
| Issue | Cause | Solution |
|---|---|---|
| Build fails with syntax errors | Default Node.js 12 doesn’t support modern JS | Set NODE_VERSION=20 |
sharp module fails to install | Native dependencies need correct platform | Add npm_config_platform=linux and npm_config_arch=x64 env vars |
| Git submodules not cloned | Pages doesn’t clone submodules by default | Use git submodule update --init in build command |
| Build times out at 20 minutes | Large sites with many images | Optimize build scripts; pre-process images |
| Functions not deploying via Dashboard | Known Cloudflare bug | Use Wrangler CLI or Git integration instead |
Create a GitHub repository
# In your project directorygit initgit add .git commit -m "Initial commit"
# Create repo on GitHub (using GitHub CLI)gh repo create my-practice-site --private --source=. --pushConnect to Cloudflare Pages
Go to Cloudflare Dashboard -> Workers & Pages -> Create -> Pages -> Connect to Git -> Select GitHub -> Authorize the Cloudflare Pages GitHub App -> Select your repository -> Configure build settings -> Deploy.
Verify auto-deploy
# Make a changeecho "<!-- Updated -->" >> public/index.html
# Push to maingit add . && git commit -m "Test auto-deploy" && git push
# Watch Cloudflare Dashboard -> Pages -> Deployments# Should show "Building" -> "Success" within 1-2 minutesOnce connected, every push triggers a deployment:
| Git Action | Cloudflare Action | URL |
|---|---|---|
Push to main | Production deploy | my-site.pages.dev |
Push to staging | Preview deploy | staging.my-site.pages.dev |
Push to feature/xyz | Preview deploy | feature-xyz.my-site.pages.dev |
| Pull Request | Preview deploy | <hash>.my-site.pages.dev |
Every non-production branch gets its own preview URL. This is incredibly useful for staging and client review.
# Create a staging branchgit checkout -b staginggit push -u origin staging# Preview URL: staging.my-site.pages.dev
# Create a feature branchgit checkout -b feature/new-hero# Make changes...git push -u origin feature/new-hero# Preview URL: feature-new-hero.my-site.pages.dev
# Create a PR - Cloudflare posts the preview URL as a commentgh pr create --title "New hero section" --body "Updated hero design"Limit which branches trigger deployments:
main (or master)staging and release/*)www.mypractice.com)| Setup | Domain | DNS Record | Example |
|---|---|---|---|
| Root domain | mypractice.com | CNAME -> my-site.pages.dev | Main website |
| WWW subdomain | www.mypractice.com | CNAME -> my-site.pages.dev | Main website (www) |
| Landing page subdomain | lp.mypractice.com | CNAME -> my-lp.pages.dev | Landing pages |
| Staging subdomain | staging.mypractice.com | CNAME -> staging.my-site.pages.dev | Staging preview |
Most sites should work on both mypractice.com and www.mypractice.com:
mypractice.com as a custom domain on your Pages projectwww.mypractice.com as a custom domain on the same projectTo redirect one to the other (e.g., mypractice.com -> www.mypractice.com), use a Cloudflare Redirect Rule (Dashboard -> Rules -> Redirect Rules):
| Field | Value |
|---|---|
| Rule name | Redirect root to www |
| When incoming requests match | Hostname equals mypractice.com |
| Then | Dynamic redirect |
| Expression | concat("https://www.mypractice.com", http.request.uri.path) |
| Status code | 301 (Permanent) |
If your main site stays on WordPress but landing pages go to Cloudflare Pages:
mypractice.com -> WordPress (unchanged)www.mypractice.com -> WordPress (unchanged)lp.mypractice.com -> Cloudflare Pages (landing pages)DNS configuration:
Type Name Content ProxyCNAME lp my-landing-pages.pages.dev ProxiedTwo approaches depending on whether you want Cloudflare to manage all DNS:
Move your domain’s nameservers to Cloudflare. This gives you full control over DNS, CDN, security, and performance features.
Add domain to Cloudflare
Go to Cloudflare Dashboard -> Add a site -> Enter your domain (mypractice.com) -> Select the Free plan -> Cloudflare scans existing DNS records.
Review DNS records
Cloudflare imports your existing records. Verify critical records:
| Type | Name | Content | Proxy Status |
|---|---|---|---|
| A | @ | Your server IP (if WordPress still running) | Proxied |
| CNAME | www | my-site.pages.dev | Proxied |
| MX | @ | Mail server (e.g., aspmx.l.google.com) | DNS only |
| TXT | @ | SPF record | DNS only |
| TXT | @ | Google verification | DNS only |
Update nameservers at your registrar
Cloudflare provides two nameservers (e.g., ada.ns.cloudflare.com and bob.ns.cloudflare.com). Update these at your domain registrar:
| Registrar | Path |
|---|---|
| GoDaddy | My Products -> DNS -> Nameservers -> Change |
| Namecheap | Domain List -> Manage -> Nameservers -> Custom DNS |
| Google/Squarespace | My domains -> DNS -> Custom name servers |
| Cloudflare Registrar | Already set (no action needed) |
Propagation time: 1-24 hours (usually under 2 hours).
Verify activation
# Check nameserversdig NS mypractice.com +short
# Should show:# ada.ns.cloudflare.com.# bob.ns.cloudflare.com.Keep your existing DNS provider. Only point specific subdomains to Cloudflare Pages via CNAME records.
At your current DNS provider, add:
Type Name Value TTLCNAME www my-site.pages.dev 3600CNAME lp my-landing-pages.pages.dev 3600Use this when: You cannot or do not want to move nameservers (e.g., complex DNS setup, multiple services on the same domain).
| Record Type | Purpose | Proxy Status | Example |
|---|---|---|---|
| A | Points domain to IP address | Proxied (for web traffic) | @ -> 192.0.2.1 |
| AAAA | Points domain to IPv6 address | Proxied (for web traffic) | @ -> 2001:db8::1 |
| CNAME | Points domain to another domain | Proxied (for web traffic) | www -> site.pages.dev |
| MX | Mail server routing | DNS only (never proxy) | @ -> mail.google.com |
| TXT | Verification, SPF, DKIM | DNS only | @ -> v=spf1 include:... |
| SRV | Service records | DNS only | Various |
Cloudflare Pages provides free, automatic SSL certificates for all custom domains. No configuration needed.
If using Cloudflare DNS (full migration), configure the SSL mode:
| Mode | Description | Security | Recommendation |
|---|---|---|---|
| Off | No encryption | None | Never use |
| Flexible | Encrypted browser->Cloudflare, unencrypted Cloudflare->origin | Partial | Avoid |
| Full | Encrypted both ways, origin cert not validated | Good | Acceptable |
| Full (Strict) | Encrypted both ways, origin cert validated | Best | Recommended |
Ensure all HTTP traffic redirects to HTTPS:
Go to SSL/TLS -> Edge Certificates -> Enable Always Use HTTPS.
/* Strict-Transport-Security: max-age=31536000; includeSubDomains; preloadcurl -X PATCH "https://api.cloudflare.com/client/v4/zones/{zone_id}/settings/always_use_https" \ -H "Authorization: Bearer {api_token}" \ -H "Content-Type: application/json" \ --data '{"value":"on"}'After HSTS has been active for several weeks, submit your domain to the browser HSTS preload list so browsers always use HTTPS, even on the first visit.
preload directiveEnvironment variables store configuration values and secrets (API keys, site URLs) outside your code.
For non-secret values only:
[vars]SITE_NAME = "My Medical Practice"SITE_URL = "https://www.mypractice.com"CONTACT_EMAIL = "info@mypractice.com"# Set a secret (prompts for value, never stored in files)npx wrangler pages secret put SENDGRID_API_KEY --project-name=my-practice-site
# List secretsnpx wrangler pages secret list --project-name=my-practice-site
# Delete a secretnpx wrangler pages secret delete SENDGRID_API_KEY --project-name=my-practice-siteDifferent values for production and preview environments:
| Variable | Production | Preview | Purpose |
|---|---|---|---|
SITE_URL | https://www.mypractice.com | https://staging.mypractice.com | Canonical URL |
SENDGRID_API_KEY | SG.prod_key... | SG.test_key... | Email delivery |
FORM_RECIPIENT | info@mypractice.com | dev@mypractice.com | Form submissions |
ANALYTICS_ID | G-PROD123 | (not set) | Google Analytics |
DEBUG | false | true | Debug logging |
Environment variables are available in the env parameter of Cloudflare Pages Functions:
export async function onRequestPost(context) { const { env } = context;
const apiKey = env.SENDGRID_API_KEY; const recipient = env.FORM_RECIPIENT; const siteName = env.SITE_NAME;
// Use variables in your function logic const response = await fetch('https://api.sendgrid.com/v3/mail/send', { method: 'POST', headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ personalizations: [{ to: [{ email: recipient }] }], from: { email: `noreply@${env.SITE_URL.replace('https://', '')}`, name: siteName }, subject: `New form submission from ${siteName}`, content: [{ type: 'text/plain', value: 'Form data here...' }] }) });
return new Response(JSON.stringify({ success: true }), { headers: { 'Content-Type': 'application/json' } });}Every push to a non-production branch creates a unique preview deployment with its own URL.
flowchart TD
A[Push Code] --> B{Which Branch?}
B -->|main| C[Production Deploy]
B -->|staging| D[Preview Deploy]
B -->|feature/*| E[Preview Deploy]
B -->|Pull Request| F[Preview Deploy]
C --> G["my-site.pages.dev<br/>www.mypractice.com"]
D --> H["staging.my-site.pages.dev"]
E --> I["feature-name.my-site.pages.dev"]
F --> J["hash.my-site.pages.dev"]
Preview deployments are perfect for getting client approval before going live:
# Create a branch with changesgit checkout -b client-review/homepage-update# Make changes...git add . && git commit -m "Updated homepage hero and services"git push -u origin client-review/homepage-update
# Get the preview URL from Cloudflare Dashboard# Or use GitHub PR comments (auto-posted by Cloudflare)Share the preview URL with clients:
"Here's a preview of the updated homepage:https://client-review-homepage-update.my-site.pages.dev
Please review and let us know if you'd like any changes."Preview deployments use their own set of environment variables:
| Feature | Free Plan | Pro Plan |
|---|---|---|
| Preview deployments | Unlimited | Unlimited |
| Concurrent builds | 1 | 5 |
| Build minutes/month | 500 | 5,000 |
| Preview retention | 30 days | 30 days |
Every deployment to Cloudflare Pages is preserved. Rolling back to a previous version takes seconds.
Speed: Instant (seconds)
Git history: No change
Side effects: None
Best for: Emergencies when you need to revert immediately.
Speed: 1-2 minutes (triggers rebuild)
Git history: Creates revert commit
Side effects: Clean Git history
Best for: Planned rollbacks where you want a clean audit trail.
# Find the commit to revertgit log --oneline -10
# Revert the problematic commitgit revert abc1234
# Push to trigger auto-deploygit pushCloudflare Pages keeps a history of all deployments:
| Information | Available |
|---|---|
| Deployment ID | Yes |
| Timestamp | Yes |
| Git commit hash | Yes (if Git-connected) |
| Branch name | Yes |
| Build logs | Yes |
| Status (success/fail) | Yes |
| Preview URL | Yes (each deploy has a unique URL) |
Automate testing and deployment with GitHub Actions. This example runs tests before deploying, ensuring broken code never reaches production.
Create .github/workflows/deploy.yml:
name: Deploy to Cloudflare Pages
on: push: branches: [main, staging] pull_request: branches: [main]
jobs: test: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4
- name: Setup Node.js uses: actions/setup-node@v4 with: node-version: '20' cache: 'npm'
- name: Install dependencies run: npm ci
- name: Build run: npm run build
- name: Run tests run: npm test
- name: Run Lighthouse CI uses: treosh/lighthouse-ci-action@v12 with: urls: | http://localhost:4173/ configPath: '.lighthouserc.json' uploadArtifacts: true
deploy: needs: test if: github.event_name == 'push' runs-on: ubuntu-latest permissions: contents: read deployments: write steps: - name: Checkout uses: actions/checkout@v4
- name: Setup Node.js uses: actions/setup-node@v4 with: node-version: '20' cache: 'npm'
- name: Install dependencies run: npm ci
- name: Build run: npm run 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 public --project-name=my-practice-siteAdd these secrets to your GitHub repository:
Go to GitHub -> your repo -> Settings -> Secrets and variables -> Actions
Add the following secrets:
| Secret Name | Value | How to Get |
|---|---|---|
CLOUDFLARE_API_TOKEN | Your API token | Cloudflare Dashboard -> My Profile -> API Tokens -> Create Token -> “Edit Cloudflare Workers” template |
CLOUDFLARE_ACCOUNT_ID | Your account ID | Cloudflare Dashboard -> any domain -> Overview -> right sidebar |
Click Create Token
Use the “Edit Cloudflare Workers” template
Modify permissions:
| Resource | Permission |
|---|---|
| Account -> Cloudflare Pages | Edit |
| Account -> Account Settings | Read |
| Zone -> Zone | Read (if using custom domains) |
| Zone -> DNS | Edit (if managing DNS via API) |
Click Continue to summary -> Create Token
Copy the token immediately (shown only once)
Create .lighthouserc.json for automated Lighthouse scoring:
{ "ci": { "collect": { "numberOfRuns": 3, "startServerCommand": "npm run serve", "startServerReadyPattern": "ready", "url": [ "http://localhost:4173/", "http://localhost:4173/about/", "http://localhost:4173/contact/" ] }, "assert": { "assertions": { "categories:performance": ["error", { "minScore": 0.9 }], "categories:accessibility": ["warn", { "minScore": 0.9 }], "categories:best-practices": ["warn", { "minScore": 0.9 }], "categories:seo": ["warn", { "minScore": 0.9 }] } }, "upload": { "target": "temporary-public-storage" } }}flowchart TD
A[Push to main] --> B[GitHub Actions Triggers]
B --> C[Job: test]
C --> C1[Install dependencies]
C1 --> C2[Build site]
C2 --> C3[Run validation tests]
C3 --> C4["Run Lighthouse CI (must score 90+)"]
C4 --> D{Tests pass?}
D -->|Yes| E[Job: deploy]
D -->|No| F[Deployment blocked<br/>Team notified]
E --> E1[Build site]
E1 --> E2[Deploy via Wrangler]
E2 --> G[Live on production]
style F fill:#fee,stroke:#f66
style G fill:#efe,stroke:#6a6
A typical setup for medical practice sites with a staging environment for review:
Production
Branch: main
URL: www.mypractice.com
Vars: Production API keys, real email addresses
Deploy: Automatic on push to main (after CI passes)
Staging
Branch: staging
URL: staging.mypractice.com
Vars: Test API keys, dev email addresses
Deploy: Automatic on push to staging
Preview
Branch: feature/* or PR branches
URL: <hash>.my-site.pages.dev
Vars: Same as staging
Deploy: Automatic on push
flowchart LR
A[Feature Branch] -->|Push| B[Preview Deploy]
B -->|Create PR| C[Code Review]
C -->|Approve & Merge| D[Staging Branch]
D -->|Auto-deploy| E[Staging Review]
E -->|Approve & Merge| F[Main Branch]
F -->|Auto-deploy + CI| G[Production]
# 1. Create feature branch from maingit checkout main && git pullgit checkout -b feature/update-services
# 2. Make changes, test locallynpm run build && npm run serve# Preview at http://localhost:4173
# 3. Push feature branch (creates preview deployment)git push -u origin feature/update-services# Preview URL: feature-update-services.my-site.pages.dev
# 4. Create PR for reviewgh pr create --title "Updated services page" --body "New pricing, updated descriptions"# Cloudflare posts preview URL on the PR
# 5. After approval, merge to staging for final reviewgit checkout staging && git merge feature/update-servicesgit push# Review at staging.mypractice.com
# 6. After staging approval, merge to main for productiongit checkout main && git merge staginggit push# Live at www.mypractice.comMap custom domains to specific branches:
Step 1: Add custom domains in Cloudflare Pages settings
| Domain | Branch | Purpose |
|---|---|---|
www.mypractice.com | main | Production |
staging.mypractice.com | staging | Staging |
dev.mypractice.com | develop | Development |
Step 2: Add DNS records (if domain is on Cloudflare DNS)
Type Name Content ProxyCNAME www my-site.pages.dev ProxiedCNAME staging staging.my-site.pages.dev ProxiedCNAME dev develop.my-site.pages.dev ProxiedUse environment variables to control build behavior per environment:
// astro.config.mjs or build scriptconst isProd = process.env.CF_PAGES_BRANCH === 'main';const isStaging = process.env.CF_PAGES_BRANCH === 'staging';
export default { site: isProd ? 'https://www.mypractice.com' : isStaging ? 'https://staging.mypractice.com' : 'https://dev.mypractice.com',
// Disable analytics in non-production integrations: isProd ? [analytics()] : [],};Cloudflare Pages built-in environment variables:
| Variable | Description | Example |
|---|---|---|
CF_PAGES | Always 1 on Cloudflare Pages | 1 |
CF_PAGES_BRANCH | Current branch name | main |
CF_PAGES_COMMIT_SHA | Full commit hash | abc123def456... |
CF_PAGES_URL | Deployment URL | https://abc123.my-site.pages.dev |
npm test)npm run test:lighthouse)_headers file in place (cache + security headers)robots.txt and sitemap.xml presentpublic/404.html)npx wrangler login)npx wrangler pages deploy public)<project>.pages.devdig NS yourdomain.com +short)_headersCLOUDFLARE_API_TOKEN, CLOUDFLARE_ACCOUNT_ID)# Authenticationnpx wrangler login # Login via browsernpx wrangler whoami # Check current user
# Deploymentnpx wrangler pages deploy public # Deploy public/ directorynpx wrangler pages deploy public \ --project-name=my-site # Specify project namenpx wrangler pages deploy public \ --branch=staging # Deploy as preview (staging branch)
# Project managementnpx wrangler pages project list # List all Pages projectsnpx wrangler pages project create \ my-site # Create a new projectnpx wrangler pages deployment list \ --project-name=my-site # List deployments
# Secretsnpx wrangler pages secret put \ API_KEY --project-name=my-site # Set a secretnpx wrangler pages secret list \ --project-name=my-site # List secretsnpx wrangler pages secret delete \ API_KEY --project-name=my-site # Delete a secret
# Local development (with Functions)npx wrangler pages dev public # Local dev server with Functions support# Check nameserversdig NS mypractice.com +short
# Check A recorddig A mypractice.com +short
# Check CNAME recorddig CNAME www.mypractice.com +short
# Check SSL certificateopenssl s_client -connect www.mypractice.com:443 -servername www.mypractice.com \ </dev/null 2>/dev/null | openssl x509 -noout -subject -dates
# Check HTTP headerscurl -I https://www.mypractice.com/| Task | Dashboard Path |
|---|---|
| View deployments | Workers & Pages -> your project -> Deployments |
| Add custom domain | Workers & Pages -> your project -> Custom domains |
| Set environment vars | Workers & Pages -> your project -> Settings -> Environment variables |
| Configure build | Workers & Pages -> your project -> Settings -> Builds & deployments |
| Rollback deployment | Workers & Pages -> your project -> Deployments -> … -> Rollback |
| View build logs | Workers & Pages -> your project -> Deployments -> click deployment |
| DNS management | Your domain -> DNS -> Records |
| SSL settings | Your domain -> SSL/TLS -> Overview |