Skip to main content

GitLab CI/CD for Cloudflare Pages: Automated Deployments with Preview Environments

Remember when the previous post ended with "Automating deployments — When I find some more time..."? Well, I found the time. Let me walk you through how I set up a GitLab CI/CD pipeline that deploys this website to Cloudflare Pages, complete with preview environments on merge requests.

The Goal

Simple:

Prerequisites

You need two things from Cloudflare:

  1. Account ID — find this on the sidebar of any zone in your Cloudflare dashboard
  2. API Token — create one at dash.cloudflare.com/profile/api-tokens with these permissions:
Permission Access
Account · Cloudflare Pages Edit
Zone · Zone Read

Add both as CI/CD variables in your GitLab project (Settings → CI/CD → Variables):

Wrangler (the Cloudflare CLI) picks these up automatically, no extra configuration needed.

The Pipeline

The full .gitlab-ci.yml is surprisingly short. Two stages: build the static site with Eleventy, then deploy it with Wrangler.

Build Stage

stages:
  - build
  - deploy

variables:
  CLOUDFLARE_PROJECT_NAME: ascode-web
  NODE_VERSION: "20"

build:
  stage: build
  image: node:${NODE_VERSION}-alpine
  script:
    - npm ci
    - npx @11ty/eleventy
  artifacts:
    paths:
      - _site/
    expire_in: 1 hour
  rules:
    - if: $CI_MERGE_REQUEST_IID
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

Nothing fancy. Install dependencies, run Eleventy, and pass the _site/ output as an artifact to the deploy stage. The rules ensure it runs on both merge requests and pushes to main.

Preview Deploys

This is where it gets nice:

deploy:preview:
  stage: deploy
  image: node:${NODE_VERSION}-alpine
  needs:
    - build
  script:
    - npx wrangler pages deploy _site/
        --project-name="${CLOUDFLARE_PROJECT_NAME}"
        --branch="${CI_MERGE_REQUEST_SOURCE_BRANCH_NAME}"
        --commit-hash="${CI_COMMIT_SHORT_SHA}"
  environment:
    name: preview/$CI_MERGE_REQUEST_SOURCE_BRANCH_NAME
    url: https://${CI_MERGE_REQUEST_SOURCE_BRANCH_NAME}.${CLOUDFLARE_PROJECT_NAME}.pages.dev
  rules:
    - if: $CI_MERGE_REQUEST_IID

Every merge request gets its own preview URL based on the branch name. Create a branch called new-blog-post, and you get https://new-blog-post.ascode-web.pages.dev. The GitLab environment link shows up directly in the merge request, so reviewers can click through to the preview without having to figure out the URL.

Wrangler handles everything — if the Cloudflare Pages project does not exist yet, it creates it. It uploads the files, configures the deployment, and gives you the URL. No need to pre-configure anything in the Cloudflare dashboard.

Stopping Preview Environments

When a merge request is merged or closed, the preview environment should be cleaned up in GitLab:

stop:preview:
  stage: deploy
  image: node:${NODE_VERSION}-alpine
  needs: []
  script:
    - echo "Preview environment stopped"
  environment:
    name: preview/$CI_MERGE_REQUEST_SOURCE_BRANCH_NAME
    action: stop
  rules:
    - if: $CI_MERGE_REQUEST_IID
      when: manual
      allow_failure: true

Note that this only stops the GitLab environment — the Cloudflare preview deployment itself stays around until Cloudflare cleans it up (they do, eventually). If you want to actively delete it, you can add a wrangler pages deployment delete command, but I have not found the need for that.

Production Deploy

deploy:production:
  stage: deploy
  image: node:${NODE_VERSION}-alpine
  needs:
    - build
  script:
    - npx wrangler pages deploy _site/
        --project-name="${CLOUDFLARE_PROJECT_NAME}"
        --branch="main"
        --commit-hash="${CI_COMMIT_SHORT_SHA}"
  environment:
    name: production
    url: https://ascode.nl
  rules:
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

Merge to main → deploys to production. That's it. The --branch="main" flag tells Cloudflare this is the production deployment, which means it goes live on your custom domain.

A Note on Wrangler and npx

You might notice I am using npx wrangler instead of installing it as a project dependency. This keeps the blog repository clean — Wrangler is a deployment tool, not a site dependency. The npx command downloads it on the fly in the CI environment, which adds a few seconds to the deploy stage but keeps package.json focused on what matters: Eleventy and its plugins.

If the download time bothers you, add wrangler to your devDependencies and it will be installed during npm ci in the build stage. Either way works.

The Result

The workflow is now:

  1. Create a branch and write content
  2. Push and open a merge request
  3. Pipeline builds and deploys a preview — check the result
  4. Merge to main
  5. Pipeline deploys to production — live on ascode.nl

No more manual uploads to Cloudflare Pages. No more "when I find some more time." Just git push and go.