Ship It
Put your site on the internet, on your own domain, that redeploys itself every time you push — and only if the tests pass.
You built the thing. Now put it where people can reach it. This guide takes a static site
from your laptop to a real URL on your own domain, wired so every git push rebuilds
and redeploys it automatically — and a broken build or dead link blocks the deploy so
production only ever moves forward.
We use Cloudflare Pages for hosting (free, fast, global) and GitHub Actions for the
pipeline. This is the exact setup behind the site you’re reading. By the end, shipping is
one command: git push.
What you’ll have at the end
- Your site live at a real domain over HTTPS.
- A push-to-deploy pipeline:
git push→ build → deploy, no dashboards to click. - A test gate: the deploy only runs if the build succeeds (add link/smoke checks and those gate it too).
- Zero secrets in your repo — the two credentials live in GitHub, encrypted.
Before you start
- A project that builds to static files (this uses Astro’s
dist/; any static output works). Confirmnpm run buildproduces adist/folder locally first. - The code in a GitHub repo (
git init, push to a new repo if it isn’t already). - A Cloudflare account (free) → https://dash.cloudflare.com.
- A domain you can point at Cloudflare (or skip that — you get a free
*.pages.devURL). - ~30–60 minutes, most of it one-time setup you never touch again.
Step 1 — Confirm the build locally
The pipeline runs the same command CI will, so get it green on your machine first:
npm install
npm run build # must produce ./dist with your pages in it
npm run preview # optional: eyeball the production build at localhost:4321
If dist/ has your index.html and pages, you’re ready. If the build errors, fix it here
— never debug a build inside CI.
Step 2 — Get a Cloudflare API token and account ID
The pipeline needs permission to deploy on your behalf. Two values:
- API token — Cloudflare dashboard → My Profile → API Tokens → Create Token. Use the “Edit Cloudflare Workers” template, or a custom token with the Account → Cloudflare Pages → Edit permission. Copy the token (shown once).
- Account ID — on the dashboard’s right sidebar (or any zone’s Overview page). Copy it.
Treat the token like a password. It can deploy to your account. It goes into GitHub’s encrypted secrets in the next step — never into a file in the repo.
Step 3 — Store them as GitHub secrets
In your repo on GitHub: Settings → Secrets and variables → Actions → New repository secret. Add two:
CLOUDFLARE_API_TOKEN→ the token from Step 2CLOUDFLARE_ACCOUNT_ID→ the account ID from Step 2
GitHub encrypts these; the workflow reads them at runtime and they never appear in logs.
Step 4 — Create the Pages project once
Do this a single time so the project exists for the pipeline to deploy into. Locally:
npx wrangler pages project create my-site --production-branch main
Replace my-site with your project name (remember it — it goes in the workflow below). It
first opens a browser to authorize Wrangler; approve it. That’s the only manual deploy
you’ll ever do.
Step 5 — The deploy pipeline
Create .github/workflows/deploy.yml. On every push to main it builds, then deploys —
and the deploy needs the build job to pass first:
name: Test and deploy
on:
push:
branches: [main]
pull_request:
branches: [main] # PRs build (catch breakage) but don't deploy
workflow_dispatch:
permissions:
contents: read
jobs:
build:
name: Build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- run: npm ci
- run: npm run build # a failure here stops everything below
deploy:
name: Deploy to Cloudflare Pages
needs: build # only deploy if build passed
if: github.event_name == 'push' # don't deploy on PRs
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- run: npm ci
- run: npm run build
- name: Deploy
uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
command: pages deploy dist --project-name=my-site --branch=main
The whole point is
needs: build. The deploy job won’t start until the build job is green. Add test steps to thebuildjob (a link checker, smoke tests) and each one becomes a gate — production can’t regress past a passing suite. Changemy-siteto your project name in the deploy command.
Commit and push:
git add .github/workflows/deploy.yml
git commit -m "ci: deploy to Cloudflare Pages on push"
git push
Open your repo’s Actions tab and watch it run. When it’s green, your site is live at
https://my-site.pages.dev.
Step 6 — Put it on your own domain
If your domain’s DNS is on Cloudflare: in the dashboard go to Workers & Pages → your
project → Custom domains → Set up a custom domain, enter yourdomain.com, and Cloudflare
adds the DNS record and provisions HTTPS for you (a minute or two).
If your DNS is elsewhere, either move the domain’s nameservers to Cloudflare (recommended,
unlocks the one-click flow above) or add the CNAME Cloudflare shows you at your current
registrar.
Within a few minutes https://yourdomain.com serves your site, certificate and all.
You now have
A live site on your own domain that ships itself. Your deploy process is now just your git
workflow: push to main, tests run, and if they pass the new version goes live in under a
minute. No FTP, no manual builds, no “works on my machine.”
Make it yours (next ideas)
- Add real gates: put a link checker and smoke tests in the
buildjob so a dead link or broken page blocks the deploy, not just a failed compile. - Preview deploys: Cloudflare Pages builds a unique URL for every branch/PR — great for
reviewing a change before it hits
main. - Env vars: if your app needs secrets at runtime (an API key), set them in the Pages project’s Settings → Environment variables, not in the repo.
Troubleshooting
- Deploy step: “Project not found” → the
--project-namedoesn’t match, or Step 4 wasn’t run. Create the project once, then match the name exactly. - “Authentication error” on deploy → the
CLOUDFLARE_API_TOKENsecret is missing, misspelled, or lacks the Pages Edit permission. Recreate the token and re-add it. - Build passes locally but fails in CI → you likely rely on an uncommitted file or a
global dependency. CI starts clean: commit everything and pin deps with a lockfile
(
npm cineedspackage-lock.json). - Site deploys but the domain 404s → DNS hasn’t propagated or the custom domain isn’t attached to this project yet. Give it a few minutes; confirm the record in Cloudflare.