Introduction
As you may have noticed this site is built using Hugo and Papermod theme. After playing around with Hugo and getting comfortable with it, I wanted to push the initial version of this site to the git repository and set up an automatic deployment pipeline to be able to easily publish updates to the web.
Since I am the lucky owner of a self-hosted GitLab instance which I’ve been using for more than a decade for my projects, it was an obvious decision to push this site there too. Same story with Cloudflare - I use their DNS services, and since they also provide static content hosting services - Pages, decided to give it a try.
Fast googling revealed that Cloudflare supports automatic deployments of Hugo-based sites from GitHub and GitLab…, but only for cloud-based SaaS version.
Luckily Cloudflare has Wrangler - command-line interface to manage Worker projects. You may ask what workers have to do with static pages? Well, they are managed by the same tool, see pages command documentation of Wrangler tool.
Below I will explain how to tie Wrangler together with GitLab CI/CD to perform deployment to Cloudflare Pages. Further steps assume you have knowledge and experience of using Cloudflare services, git, Hugo and GitLab CI/CD. I will not go into details for some of the steps.
Setting and Testing Locally
Below are the steps to prepare project, repository and test everything locally:
- Create Hugo project
- Change into project directory
- Login with Wrangler CLI into your Cloudflare account: npx wrangler login, follow instructions
- Create new Pages project, I will call mine pages-for-article, make sure your project git default branch matches with what you pass in--production-branchargument. Cloudflare will use it later to determine if you are publishing toproductionorpreviewenvironments:npx wrangler pages project create pages-for-article --production-branch main
- Initialize git repository, do this before deploying to Pages, as Wrangler uses git metadata for deployments
- Build Hugo project: hugo
- Now, run deployment: npx wrangler pages deploy public --project-name pages-for-article
- This is what you should see from Cloudflare dashboard Workers & Pagessection, noticemainbranch andProductionenvironment in the screenshot: 
- Add public/to.gitignore
- Commit and push your project to git
Now we are all set to move to the GitLab CI/CD configuration.
Setting GitLab CI/CD
Requirements
Before going into configuration details, I like to define my requirements for CI/CD pipeline:
- I want to have two environments - productionandpreview(staging)
- I want to deploy changes from default branch, tags or merge requests only
- I want to be able to see draft content in the previewenvironment
- I want to be able to deploy any changes except tags to previewenvironment
- I want to be able to deploy to productionenvironment changes only from default branch or tags
GitLab Project Configuration
Wrangler requires certain authentication environment variables to be present to operate from CI/CD. See Run Wrangler in CI/CD guide. Add these variables in the GitLab project under Settings -> CI/CD -> Variables. Make variables masked, unprotected and non-expandable.

Make sure that the project has at least one runner with a docker execution environment under Settings -> CI/CD -> Runners.
Pipeline Definition
GitLab uses .gitlab-ci.yml to describe jobs that make up the CI/CD pipeline. I will not go into detail of every aspect of that file, but rather highlight parts that are made up to fulfill above requirements. Complete .gitlab-ci.yml file is available at GitHub.
workflow:rules describes the requirement to create and run a pipeline only for the default branch, merge requests commits and tags, omitting commits to branches without merge requests.
Because of the requirement to have draft content available in the preview environment, two distinct build jobs are needed - build:staging and build:production, first one will build a site with drafts, second - without, see script section. Both jobs inherit from common .build template, which describes common build configuration - using alpine image for runtime environment and installing hugo using apk package manager, collecting build artifacts from public folder.
The most interesting part of .gitlab-ci.yml is the .deploy template. What it does - it runs Wrangler deploy command with additional arguments - project name, branch, commit hash, and commit message. Values for the latter three arguments are taken from corresponding variables defined in the same template, which in turn reference pre-defined GitLab CI/CD variables by default.
variables:
  CLOUDFLARE_BRANCH: "$CI_COMMIT_BRANCH"
  CLOUDFLARE_COMMIT_HASH: "$CI_COMMIT_SHA"
  CLOUDFLARE_COMMIT_MESSAGE: "$CI_COMMIT_MESSAGE"
You may ask - why is it so complex? As mentioned earlier, Wrangler can extract this information from git itself. Yes, indeed, it can, but Wrangler also makes a decision on where to deploy - production or preview environment based on the branch name. Default branch always goes to production, period, you can’t affect it, this basically breaks requirements 4, 5.
Luckily, Wrangler has these arguments and you can pass whatever you want making it possible to workaround behavior of always publishing default branch changes to production. And this is what is being done in deploy:staging and deploy:production which inherit from the .deploy template.
Here is a closer look at deploy:staging variables and rules section:
  variables:
    CLOUDFLARE_BRANCH: "$CI_MERGE_REQUEST_SOURCE_BRANCH_NAME"
  rules:
    - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH' 
      when: manual
      variables:
        CLOUDFLARE_BRANCH: "$CI_COMMIT_BRANCH-preview"
    - if: '$CI_COMMIT_TAG' 
      when: never
    - when: manual
- First rule makes it possible to deploy default branch changes to previewenvironment by adding-previewpostfix to the default branch name, this way Wrangler will treat it as a non-default branch.
- Second rule disallows deployment of tags to preview, since I will use tags as release milestones for this project.
- For all other cases, i.e. merge request commits - merge request source branch name will be used, as defined in variablessection.
Please note that preview deployments are manual actions and require launching these jobs from pipeline view in the GitLab by hand. Also for pipelines not to appear as “blocked” in GitLab, deploy:staging has allow_failure: true set.
Cool thing about Cloudflare Pages is that preview deployments can be accessed via <branch-name>.pages-for-article.pages.dev address. For default branch preview for this project it is https://main-preview.pages-for-article.pages.dev.
These are deploy:production rules:
  rules:
    - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
      when: manual
    - if: '$CI_COMMIT_TAG' 
      when: always
      variables:
        CLOUDFLARE_BRANCH: "$CI_DEFAULT_BRANCH"
        CLOUDFLARE_COMMIT_MESSAGE: "$CI_COMMIT_TAG - $CI_COMMIT_TAG_MESSAGE"
    - when: never
- First rule allows manually deploying anything from the default branch to the productionenvironment; this normally is not used, but sometimes may come in handy.
- Second rule - automatically deploys to productionenvironment when tag is created. Since Wrangler has no support for tags, passing actual tag name into branch name would result in tag being deployed intopreviewenvironment instead ofproduction, to workaround this - default branch name is fed into branch name, and actual tag name along with tag message is set in commit message.
That’s it, these rules satisfy my requirements set above, and I can use GitLab to track and publish my blog changes to Cloudflare pages.
Further Improvements
- I am using bare alpineandnodedocker images which downloadhugoandwranglerfor each job invocation. This negatively affects pipeline speed and generates excess traffic, a better approach would be to use pre-built images containing these tools. On the other hand, impact is negligible, as whole pipeline takes around 30 seconds to build and deploy.
- Add a separate deployment job to the previewenvironment without draft content to make it identical to what is going to be deployed to theproductionenvironment.
Conclusion
GitLab and Cloudflare are great services with lots of useful features and customization options. With some exploration and trial and error approach I was able to create CI/CD pipeline configuration that meets my requirements for publishing Hugo blog to Cloudflare Pages from a self-hosted GitLab instance.
Complete .gitlab-ci.yml file is available on GitHub.