Skip to content

Standalone Static Site on Cloudflare Pages

Use this guide for a site that builds to static files, such as HTML, CSS, and JavaScript. Cloudflare Pages serves these files; The site does not need an application server.

Use Cloudflare Pages Direct Upload when GitLab runs the build and deploy jobs. Set the production branch when you create the Pages project.

For local work, install from the lockfile, run the checks, then build and preview the site. This project uses:

Terminal window
npm ci
npm run dev
npm run lint:docs
npm run check
npm run build
npm run preview

The build output is dist/. Keep deployment settings in step with the local build. Check the production URL, page links, assets, and access rules after each deploy. Keep a known-good commit so you can redeploy it if a release fails.

Treat all built files as public. Never put secrets or private content in the site. If the site is private, protect each hostname with Cloudflare Access. Check the custom domain, pages.dev address, and preview URLs. Use identity login for people and Access service tokens for agents. Store tokens in GitLab or an approved secret store. Set an expiry and rotate them.

No Docker image is required to build the static site. The build creates files that are uploaded to Cloudflare Pages. A Node.js image with npm may be useful in the GitLab deploy job to run Cloudflare’s Wrangler tool.

Docker Compose is not required. Use it only if local development needs other services, such as a mock API. Do not use Compose to deploy the site.

Run lint, tests, and the production build for merge requests and branches. Save dist/ as a GitLab artifact. The deploy job must use this artifact; it must not build the site again.

This project’s build checks are:

Terminal window
npm ci
npm run lint:docs
npm run check
npm run build

Deploy to production only from the protected main branch. Store these variables in GitLab CI/CD settings:

Variable Purpose
CLOUDFLARE_API_TOKEN Cloudflare API token with Account → Cloudflare Pages → Edit access. Mask and protect it.
CLOUDFLARE_ACCOUNT_ID Cloudflare account ID.
CF_PAGES_PROJECT_NAME Cloudflare Pages project name.
CF_PAGES_PROJECT_DENV GitLab environment name, such as production.
CF_PAGES_URL Site URL used by the build and GitLab environment.

Use a GitLab resource_group to prevent two production deploys from running at the same time. Protect the production environment and prevent older pipelines from replacing newer releases.

This is the complete pipeline used by this project in order to build a static Starlight site, using nodejs in both a validate and deploy stages Other examples may not require a build stage if the static assets are pre-existing.

Keep the example in sync with .gitlab-ci.yml:

stages:
- quality
- deploy
image: gitlab-reg.atech.host/operations/ops-oci-images/base/nodejs:24-alpine
variables:
ASTRO_TELEMETRY_DISABLED: "1"
NPM_CONFIG_CACHE: "$CI_PROJECT_DIR/.npm"
WRANGLER_LOG_PATH: "$CI_PROJECT_DIR/.wrangler/logs"
cache:
key:
files:
- package-lock.json
paths:
- .npm/
validate:
stage: quality
script:
- npm ci --prefer-offline
- npm run lint:docs
- npm run check
- npm run build
artifacts:
paths:
- dist/
expire_in: 1 week
deploy-to-cloudflare:
stage: deploy
resource_group: cloudflare-pages-production
needs:
- job: validate
artifacts: true
script:
- npm ci --prefer-offline
- test -n "$CLOUDFLARE_API_TOKEN" || (echo "Set CLOUDFLARE_API_TOKEN in GitLab CI/CD variables" && exit 1)
- test -n "$CLOUDFLARE_ACCOUNT_ID" || (echo "Set CLOUDFLARE_ACCOUNT_ID in GitLab CI/CD variables" && exit 1)
- test -n "$CF_PAGES_PROJECT_NAME" || (echo "Set CF_PAGES_PROJECT_NAME in GitLab CI/CD variables" && exit 1)
- test -n "$CF_PAGES_PROJECT_DENV" || (echo "Set CF_PAGES_PROJECT_DENV in GitLab CI/CD variables" && exit 1)
- test -n "$CF_PAGES_URL" || (echo "Set CF_PAGES_URL in GitLab CI/CD variables" && exit 1)
- |
sed -i "s|</body>|<div style='text-align: center; padding: 1rem; color: #666;'>Deployed $(date -u +'%Y-%m-%dT%H:%M:%SZ') - $CI_COMMIT_SHORT_SHA</div></body>|" dist/index.html
- npx wrangler pages deploy ./dist --project-name="$CF_PAGES_PROJECT_NAME" --branch="$CI_COMMIT_BRANCH" --commit-hash="$CI_COMMIT_SHA"
environment:
name: $CF_PAGES_PROJECT_DENV
url: $CF_PAGES_URL
rules:
- if: '$CI_COMMIT_BRANCH == "main"'

Note: The deploy job adds the time and commit ID to the home page before upload.