This post describes how to integrate a Hugo project repo with Gitlab CI and deploy the built site on Gitlab Pages automatically. If you host your static site on another platform/server, the actions you’ll need to do might be slightly different. In all cases, the major thing is to specify how the public folder of the project will be uploaded to the frontend server. These configurations are declarative and abide to the rules of the .gitlab-ci.yml specification.


Gitlab Pages is a free service to host one static web site per Gitlab project. The site will have a default domain consisting of our Gitlab username and project name:

Custom domains are supported.

Create .gitlab-ci.yml file

Gitlab CI recipes are declared in the .gitlab-ci.yml located in the root folder of the project. We must create it, if it doesn’t exist.

Below are the file contents from my blog.



  - hugo
  - master

  - hugo --environment production
    - public
  - master

On line 1 we say what docker image we’d like to use as a base for the container that will be created for us and started by the Gitlab runners. I use a predefined container with a fixed Hugo version (0.54.0), as I’d like to have reproducible builds and be sure that the container will be baked with exactly the same Hugo version, that I’m using locally for development.

On line 3 we define a git resolving strategy for submodules in case our theme is downloaded as a git submodule. In my case, I have followed the tutorial and added the theme as a submodule.

Next we define two CI jobs.

CI job test

On line 6 we start the definition of a job named test. The name test is an arbitrary string that we’ve chosen, it may be something else.

The script part on line 7 contains the commands that will be executed inside the container shell. There may be multiple commands on each new line, but for our current needs we only run hugo.

This has the same effect as running the hugo command locally inside the root folder of our project. The command will build our static web site and put it inside a public folder (will create it if doesn’t exist). If the build completes successfully, the command will terminate with exit code 0 and the job will pass ✅. It the build process fails, the command will terminate with exit code > 0 and the CI job will fail ❌.

In reality, with the test job just described, we haven’t really written any tests for our project (e.g. unit, intergration, etc.), but we implicitly say that if the build process of our static site succeeds, we consider the site OK and tested.

On line 9 we use the except word to specify when the job should not be executed. The master value says that the job should not be executed for a branch called master. Instead, it will be executed for all other branches that we may have and push commits to.

CI job pages

The CI job pages is more magical and subtle as it not only tests our site in the same way as we did in the test job, but it also automatically deploys the site to Gitlab Pages, if the build process is successful.

On line 14 we run the hugo command again, but this time with a flag to specify the environment configuration. The flag tells hugo to look for config.toml file inside the /config/production/ folder. We need this so we can specify different variables for production. For example, the baseURL variable may point to a real domain name like, while the default config.toml may set baseURL = "http://localhost:1313".

The magic with the automatic deployment happens on line 15 with the word artifacts. It says that the public folder (which was generated during the run of script), must not be deleted after the job is completed, but instead will be available afterwards. Whenever the Gitlab system finds this folder after the job completes, it will deploy it to the Gitlab Pages for our project. And bam… we have our site online.

When depoying to Gitlab Pages for the first time, the infrastructure needs 10-20 minutes to initialize the site and put it online. During this initial period we may see 404 Page Not Found error. We should wait for some time until the site goes live.

The last declaration in our .gitlab-ci.yml file is the word only. It defines another value which tells that the job should be executed only for commits in the master branch. So whenever we merge a branch to master or directly push commits to master the site will be redeployed to Gitlab Pages.