Using Gitlab's free private repo and CI/CD to deploy a static site to Github Pages for free

Have you ever desired to host your own personal blog, or a website to showcase your work, for free? All this while taking advantage of a globally distributed CDN to speed up the site’s delivery to your visitors?

Github Pages gives us exactly that!! It give us the ability to host static content (blog/website) for free. We can use that to showcase our work, highlight our achievements, share our resume, setup our personal blog, etc..

We can even use our own custom domain with the site 😎.

All this looks pretty cool. And considering you don’t have to spare a penny on it, makes it even more awesome.

So, what’s the problem here? ¯\_(ツ)_/¯

Well, the problem is if you’re on Github’s free plan, then the repo holding your site’s content needs to be public. It’s all OK if you are creating those static files (js, css, html) manually. But becomes problematic if you’re using any static site generator (like Jekyll, Hugo, MkDocs, etc.).

As much as there’s no harm in hosting your site’s public content on a public repo (js, css, html, images, etc.), hosting the input to your site-geneator is not advisable.

Why? Because it’s part of your development workflow. It might contain unfinished content, environment variables, secret keys, tokens, metadata, etc.. You certainly don’t want random people to be looking at such stuff, right?

This is what this post is all about. Keeping your source files (your site-generator’s input files) hidden, while exposing only the generated files.

All this with one-click change deployments!!!

We’ll learn how to use a private repo hosted on Gitlab, along with it’s CI/CD pipeline to automate the publishing of our site’s content (generated by your favorite static site generator)… all in one single git push !!! How awesome that is 😎.

What all we’ll do
  1. We’ll be setting up a personal blog website.
  2. We’ll use Hugo as our static site generator. If you don’t know about it, do read about it a bit. It’s pretty awesome!
  3. We’ll use one of the many, many freely available Hugo themes, called minimo, to beautify our blog. It’s a simple, but good looking theme.
  4. Finally, we’ll setup custom domain with the hosted site.

Creating accounts

A. Private repo on gitlab to hold our site’s source
  1. Go to gitlab and create a new account, or use an existing one if you have
  2. Create a new repo to host your site-generator’s files. Let’s call this repo as site-source
  3. Now clone this repo locally using git clone
  4. Follow hugo quickstart to setup base files. Make sure that all files are created in top-level cloned directory, and not a sub-directory
  5. We now have the base ready to save our site’s source files (these will be read by static site-generator)
B. Public repo on github to hold generated files
  1. Go to github and create a new account, or use an existing one if you have
  2. Create a new repo named where username is your github account’s username. Note: It’s important to add in this repo name to be able to use this for static site hosting
  3. Go to repo’s settings page and locate a section named GitHub Pages . Now choose the branch (master or main) and root (keep it / for now). Also, check the Enforce HTTPS checkbox. Leave custom domain input empty for now. We’ll come back to this later

Preparing for CI/CD

  1. Go to your gitlab repo (site-source), scroll down on left menu and choose Settings -> CI / CD.
  2. Expand Variables section.
  3. Now we will add a new github (it’s github, not gitlab) token here. This will be used by our CI/CD pipeline (in gitlab) to push generated site to our repo on github.
  4. To create a new token on github, visit this link:
  5. Click generate new token and choose all scopes under repo section. Finally, click generate token. Your new token will be dislayed on screen. Note this down safely. This will not be displayed again.
  6. Now come back to gitlab, and add a new variable named GITHUB_ACCESS_TOKEN with value of the generated token. Click Add Variable to save it.
  7. At this point our CI/CD pipeline is ready to do work.

Setting-up Gitlab CI/CD configuration file

  1. Gitlab (or any other CI/CD provider for that matter) makes use of a configuration file which tells the build server what and how to build the artifact(s).
  2. Artifact can be your generated site (as in this case), or compiled code, or anything else that can be deployed to be used by your intended audience.
  3. Build servers generally use yaml, toml or json formats for these files.
  4. We’ll use following .gitlab-ci.yml file for our purpose:
image: freakynit/hugo-cicd-docker:1.0.4

  - hugo version

    - rm -rf public
    - git clone --depth 1 https://GITHUB_USERNAME:$ public
    - hugo --config config.toml
    - cd public
    - git config "GITHUB_EMAIL"
    - git config --global "GITHUB_USER_FULLNAME"
    - git add -A
    - git commit -m "Build from $CI_SERVER_NAME $CI_PIPELINE_ID"
    - git push
      - public
    - master
  1. Make sure to replace GITHUB_USERNAME with your github username, GITHUB_EMAIL with your github email, and GITHUB_USER_FULLNAME with your full name.
  2. Note: DO NOT replace any variable that starts with $ (for example $GITHUB_ACCESS_TOKEN). These are replaced by gitlab’s build server while running the pipeline. It makes use of variables we have created earlier in our gitlab repo’s Variables section in settings.

CI/CD file explanation

The first and a very important thing to understand is that the build config file is run in the context of the current project. The current working directly, when build is running, is your repo’s root directory, i.e., top-level-directory. Thus, full source code is available to any command which can refer a file on local build box (generally a docker container these days)

  1. The first line:
image: freakynit/hugo-cicd-docker:1.0.4

tells gitlab’s build server to use hugo-cicd-docker docker image to run our build pipeline. This docker image already includes the current latest version (0.80.0) of hugo. It also includes git and shell utilities to help us with our build process. You can see the contents of the Dockerfile used to generate this docker image here freakynit/hugo-cicd-docker:1.0.4

  1. Lines:
  - hugo version

tells build server to run an array of commands, that should run before each build job. We’ll come to build jobs just in a while. You can read more about it here: before_script. In this case it simply executes the command hugo version which just prints hugo’s version on stdout

  1. Line:

defines a build job. A build job is what builds artifacts (mostly). It’s a generic name and can be any valid string. The same is displayed on gitlab’s CI/CD pipelines page when a pipeline is running.

  1. Line:

after github_pages: tells our build server that the following lines are commands that needs to be executed one after the other.

  1. Build commands:
      1. rm -rf public -> remove existing public directory, if any. This directory gets created when we run our static site generator, and it holds generated site.
      1. git clone --depth 1 ... -> clone (checkout) our public github repo ( to local filesystem, in public directory
      1. hugo --config config.toml -> run our site-generator with config.toml (hugo’s own configuration file) to generate our site’s public-facing content. The generated content is by default placed in public directory. This config can be overwritten in hugo’s config file (config.toml). See hugo’s documentation on how to customize hugo’s builds
      1. cd public -> switch to hugo-generated site’s root directory (public)
      1. git config "GITHUB_EMAIL" -> configure git to use this email
      1. git config --global "GITHUB_USER_FULLNAME" -> configure username
      1. git add -A -> add all changed files. Remember?… The public directory actually is a git checkout-out (cloned) repo? It contains content from our previous build (if any previous build was run, or content was manually pushed). When we ran hugo --config config.toml command in step 3 above, it might have updated some files, added or deleted others. Thus, all these changes needs to be captured before pushing to our public repo on github
      1. git commit -m "Build from $CI_SERVER_NAME $CI_PIPELINE_ID" -> add commit message. CI_SERVER_NAME and CI_PIPELINE_ID variables are supplied by default to our build runner by gitlab’s build infrastructure. See gitlab’s documentation for other such variables: predefined environment variables
      1. git push -> push the generated site content to our github’s public repo

6. Lines:

      - public

tells our build server that artifacts (generated site content in our case) are generated in public directory. Build servers generally provde the ability to save generated artifacts for a pre-configured number of days. This is helpful when you want to rollback to a previous build in case your current one fails.

  1. The last section:
    - master

tells our build server to only run the build pipeline when a commit is pushed to master branch. This allows us to work on multiple features in different branches without triggering unnecessary builds for not-yet-completed features.

Visiting our site

  1. Go to to see your site live. Note that it might take a few minutes to reflect the changes. To verify wheter the latest site content in successfully pushed to our repo or not, go to your github repo’s commits section. See when was the last commit pushed. It should be after around when your build pipeline on gitlab ended. If this is not the case, there must be some error. Check pipeline’s build logs.

Adding custom domain

  1. Purchase a new domain if you don’t have one handly. You can use namecheap(my personal favorite), or godaddy (pretty popular alternative). There are hundreds of other providers too. Use google to find your preferred one.
  2. Go to your domain’s dns settings page. You’ll need to create 4 (1 will also work) type A records, and 1 cname record.
  3. Add following targets for A records:
  4. For cname, add host www with target: Make sure to replace username with your github username.
  5. Come back to your github repo’s settings -> github pages section and add the same domain name in custom domain section. Also, make sure Enforce HTTPS is checked.
  6. Now wait for a few minutes to a few hours, or even an entire day. This update can take time. If this is still not working after 24 hours, you’ll need to contact your domain registrar.

Extra stuff

A little guide on setting up hugo

Note: It’s better to follow hugo’s official quickstart. This is provided just for easy reference, and may not be the best quiskstart to start with hugo

1. Hugo Quiskstart
hugo new site sample-hugo-site-source
cd sample-hugo-site-source
git init
git remote add origin <YOUR GITLAB RPEO'S URL>
2. Minimo Theme
  git submodule add themes/minimo

  git submodule init
  git submodule update

  cp themes/minimo/exampleSite/config.toml .

  hugo new posts/

  # Now add some content to above page (note that it's in "content/posts" directory)
  # save the file
  # start server in hot-reload mode

  hugo server -D

  # visit http://localhost:1313/
3. Changes to config.toml
  1. baseURL -> Your site’s url ( OR https://your_custom_domain)
  2. title -> Title of your site
  3. Add staticDir = ["static"] after theme
  4. description under section -> description shown on your site
  5. Adjust params.copyright section if needed
  6. You can play around more such settings. Refer hugo’s official docs for this
4. Add .gitlab-ci.yml file as described before
5. Commit and push the changes

Make sure that section Preparing for CI/CD has been completed

That’s all, folks ¯\(ツ)

Drop me a mail at or DM me on twitter if you have any questions or need help on any step.