- Practical example
- Advanced GitLab CI for GitLab Pages
To get started with GitLab Pages, you can
use one of the project templates, a
or fork an existing example project. Therefore, you don’t need to
understand all the ins and odds of GitLab CI/CD to get your site
deployed. Still, there are cases where you want to write your own
script or tweak an existing one. This document guides you through
This guide also provides a general overview and clear introduction
for getting familiar with the
.gitlab-ci.yml file and writing
one for the first time.
GitLab CI/CD serves numerous purposes, to build, test, and deploy your app from GitLab through Continuous Integration, Continuous Delivery, and Continuous Deployment methods. You will need it to build your website with GitLab Pages, and deploy it to the Pages server.
To implement GitLab CI/CD, the first thing you need is a configuration
.gitlab-ci.yml placed at your website’s root directory.
What this file actually does is telling the GitLab Runner to run scripts as you would do from the command line. The Runner acts as your terminal. GitLab CI/CD tells the Runner which commands to run. Both are built-in in GitLab, and you don’t need to set up anything for them to work.
Explaining every detail of GitLab CI/CD
and GitLab Runner is out of the scope of this guide, but we’ll
need to understand just a few things to be able to write our own
.gitlab-ci.yml or tweak an existing one. It’s an
with its own syntax. You can always check your CI syntax with
the GitLab CI Lint Tool.
Let’s consider you have a Jekyll site.
To build it locally, you would open your terminal, and run
Of course, before building it, you had to install Jekyll in your computer.
For that, you had to open your terminal and run
gem install jekyll.
Right? GitLab CI + GitLab Runner do the same thing. But you need to
write in the
.gitlab-ci.yml the script you want to run so
GitLab Runner will do it for you. It looks more complicated than it
is. What you need to tell the Runner:
gem install jekyll jekyll build
To transpose this script to Yaml, it would be like this:
script: - gem install jekyll - jekyll build
So far so good. Now, each
script, in GitLab is organized by
job, which is a bunch of scripts and settings you want to
apply to that specific task.
job: script: - gem install jekyll - jekyll build
For GitLab Pages, this
job has a specific name, called
which tells the Runner you want that task to deploy your website
with GitLab Pages:
pages: script: - gem install jekyll - jekyll build
We also need to tell Jekyll where do you want the website to build,
and GitLab Pages will only consider files in a directory called
To do that with Jekyll, we need to add a flag specifying the
-d) of the
jekyll build -d public. Of course, we need
to tell this to our Runner:
pages: script: - gem install jekyll - jekyll build -d public
We also need to tell the Runner that this job generates
artifacts, which is the site built by Jekyll.
Where are these artifacts stored? In the
pages: script: - gem install jekyll - jekyll build -d public artifacts: paths: - public
The script above would be enough to build your Jekyll
site with GitLab Pages. But, from Jekyll 3.4.0 on, its default
template originated by
jekyll new project requires
Bundler to install Jekyll dependencies
and the default theme. To adjust our script to meet these new
requirements, we only need to install and build Jekyll with Bundler:
pages: script: - bundle install - bundle exec jekyll build -d public artifacts: paths: - public
That’s it! A
.gitlab-ci.yml with the content above would deploy
your Jekyll 3.4.0 site with GitLab Pages. This is the minimum
configuration for our example. On the steps below, we’ll refine
the script by adding extra options to our GitLab CI.
Artifacts will be automatically deleted once GitLab Pages got deployed. You can preserve artifacts for limited time by specifying the expiry time.
At this point, you probably ask yourself: “okay, but to install Jekyll
I need Ruby. Where is Ruby on that script?”. The answer is simple: the
first thing GitLab Runner will look for in your
.gitlab-ci.yml is a
Docker image specifying what do you need in
your container to run that script:
image: ruby:2.7 pages: script: - bundle install - bundle exec jekyll build -d public artifacts: paths: - public
In this case, you’re telling the Runner to pull this image, which contains Ruby 2.7 as part of its file system. When you don’t specify this image in your configuration, the Runner will use a default image, which is Ruby 2.6.
If your SSG needs NodeJS to build, you’ll
need to specify which image you want to use, and this image should
contain NodeJS as part of its file system. E.g., for a
Hexo site, you can use
Note: We’re not trying to explain what a Docker image is, we just need to introduce the concept with a minimum viable explanation. To know more about Docker images, please visit their website or take a look at a summarized explanation here.
Let’s go a little further.
If you use GitLab as a version control platform, you will have your
branching strategy to work on your project. Meaning, you will have
other branches in your project, but you’ll want only pushes to the
default branch (usually
master) to be deployed to your website.
To do that, we need to add another line to our CI, telling the Runner
to only perform that job called
pages on the
image: ruby:2.6 pages: script: - bundle install - bundle exec jekyll build -d public artifacts: paths: - public only: - master
Another interesting concept to keep in mind are build stages. Your web app can pass through a lot of tests and other tasks until it’s deployed to staging or production environments. There are three default stages on GitLab CI: build, test, and deploy. To specify which stage your job is running, simply add another line to your CI:
image: ruby:2.6 pages: stage: deploy script: - bundle install - bundle exec jekyll build -d public artifacts: paths: - public only: - master
You might ask yourself: “why should I bother with stages
at all?” Well, let’s say you want to be able to test your
script and check the built site before deploying your site
to production. You want to run the test exactly as your
script will do when you push to
master. It’s simple,
let’s add another task (job) to our CI, telling it to
test every push to other branches,
image: ruby:2.6 pages: stage: deploy script: - bundle install - bundle exec jekyll build -d public artifacts: paths: - public only: - master test: stage: test script: - bundle install - bundle exec jekyll build -d test artifacts: paths: - test except: - master
test job is running on the stage
will build the site in a directory called
this job will affect all the branches except
The best benefit of applying stages to different jobs is that every job in the same stage builds in parallel. So, if your web app needs more than one test before being deployed, you can run all your test at the same time, it’s not necessary to wait one test to finish to run the other. Of course, this is just a brief introduction of GitLab CI and GitLab Runner, which are tools much more powerful than that. This is what you need to be able to create and tweak your builds for your GitLab Pages site.
To avoid running the same script multiple times across
your jobs, you can add the parameter
in which you specify which commands you want to run for
every single job. In our example, notice that we run
bundle install for both jobs,
We don’t need to repeat it:
image: ruby:2.6 before_script: - bundle install pages: stage: deploy script: - bundle exec jekyll build -d public artifacts: paths: - public only: - master test: stage: test script: - bundle exec jekyll build -d test artifacts: paths: - test except: - master
If you want to cache the installation files for your
projects dependencies, for building faster, you can
use the parameter
cache. For this example, we’ll
cache Jekyll dependencies in a
when we run
image: ruby:2.6 cache: paths: - vendor/ before_script: - bundle install --path vendor pages: stage: deploy script: - bundle exec jekyll build -d public artifacts: paths: - public only: - master test: stage: test script: - bundle exec jekyll build -d test artifacts: paths: - test except: - master
For this specific case, we need to exclude
_config.yml file, otherwise Jekyll will
understand it as a regular directory to build
together with the site:
exclude: - vendor
There we go! Now our GitLab CI not only builds our website,
but also continuously test pushes to feature-branches,
caches dependencies installed with Bundler, and
continuously deploy every push to the
What you can do with GitLab CI is pretty much up to your creativity. Once you get used to it, you start creating awesome scripts that automate most of tasks you’d do manually in the past. Read through the documentation of GitLab CI to understand how to go even further on your scripts.
- On this blog post, understand the concept of
using GitLab CI
environmentsto deploy your web app to staging and production.
- On this post, learn how to run jobs sequentially, in parallel, or build a custom pipeline
- On this blog post, we go through the process of pulling specific directories from different projects to deploy this website you’re looking at, https://docs.gitlab.com.
- On this blog post, we teach you how to use GitLab Pages to produce a code coverage report.