Sign in
← CI/CD: hands-on
12 min · easy · Lesson 5 of 10

Deploy: your site on the internet at its own address

We add a deploy stage and a pages job. GitLab publishes the public folder, and the site opens at a link without a single manual step.

We're adding a deploy stage and a Pages job. GitLab publishes the public folder, and the site opens at a link with no manual action at all.

Why this matters

The site builds, the check passes, and an artifact with the public folder reaches the next stage. One last step is missing: putting that folder somewhere people can see it.

For static sites GitLab has built-in hosting called Pages. No separate server, domain or nginx setup needed.

How GitLab recognizes a Pages job

Pages isn't turned on with a checkbox in settings. The pipeline itself turns it on. A Pages job is a job whose result GitLab publishes as a site. To make a job a Pages job, you add the pages: true key to it. The job's name can be anything.

By default the public folder gets published. It must contain a non-empty index.html: GitLab serves it at the site's root address.

The final .gitlab-ci.yml in full:

stages:
  - build
  - test
  - deploy

build-site:
  stage: build
  script:
    - mkdir -p public
    - cp index.html style.css public/
  artifacts:
    paths:
      - public

check-build:
  stage: test
  script:
    - test -f public/index.html

deploy-pages:
  stage: deploy
  script:
    - echo "Publishing the contents of public/"
  pages: true
  rules:
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

deploy-pages doesn't build anything. The public folder arrives as an artifact from build-site, and since GitLab 17.10 GitLab adds the publish folder to a Pages job's artifacts:paths on its own. That's why a separate artifacts block isn't needed here.

If the build puts files somewhere other than public, name the folder via pages.publish:

create-pages:
  stage: deploy
  script:
    - echo "Publishing"
  pages:
    publish: dist

In older examples, and in half the articles online, a Pages job looks different: the job is named pages, and it has no pages key. That form still works, but GitLab has declared it deprecated. Use the pages key in new code.

The main thing to remember: if a job has neither the pages key nor the name pages, nothing gets published, and the pipeline stays green. GitLab shows no error, it simply doesn't know this job is publishing anything.

Only from the default branch

rules decide whether a job makes it into the pipeline. The if condition compares two predefined variables: $CI_COMMIT_BRANCH (the branch the pipeline is running on) and $CI_DEFAULT_BRANCH (the default branch, main for new projects). If the condition is false, the job simply isn't in the pipeline.

Without this condition, any branch with any experiment would overwrite the live site. Naming the branch through a variable is better than "main" in quotes: the config survives a branch rename and won't break on a project where the default branch is called something else.

What happens after you push

Push, and open the pipeline page. There are three stages, but somehow four jobs:

build    build-site    Passed
test     check-build   Passed
deploy   deploy-pages  Passed
         pages:deploy  Passed

You didn't write pages:deploy, GitLab added it after the Pages job succeeded. Your job prepares the files, and an internal job picks up the artifact and lays it out on the hosting. It needs no runner, GitLab runs it on its own side.

The whole pipeline takes about a minute and a half, and most of that time goes into starting up three containers.

Where to find the site's address

Deploy → Pages. On this page GitLab shows the site's address and an Access pages button.

The address looks like https://my-ci-site-a1b2c3.gitlab.io: the project name plus a random six-character tail. GitLab gives every new project a unique domain like this by default, so that someone else's site on a neighboring project in the group can't read your cookies.

The address stays with the project, but not forever: it's built from the project's name, and renaming the project changes it. There's also a Regenerate unique domain button next to Access pages. After using it, the old link stops working and returns 404.

Open it. There's your index.html, styled with style.css.

Check that the loop is closed

Change the heading in index.html, commit, push. Do nothing else.

After a minute and a half, refresh the site's page: the heading is new. Between your commit and the live site there isn't a single manual step left. That's what continuous deployment is, just at a very small scale.

Common mistakes

Pipeline is green, but Pages is empty. There's no pages:deploy in the pipeline, which means GitLab didn't recognize a Pages job. Check that the job has the pages key (or is named pages). The other option: the job didn't make it into the pipeline at all because of rules, it's missing from the job list.

The deploy stage didn't run. rules kicked in, which means you're pushing to a branch other than the default one, or the condition compares the branch to the wrong string. Check the branch name, main for new projects.

pages:deploy failed. There's no non-empty index.html at the root of the publish folder. For example, the build writes to dist, but Pages looks for public. Name the folder in pages.publish, or move the build. What actually ended up in the artifact is visible in Browse on the Pages job's page.

404 right after the first deploy. Wait a minute, the rollout takes time. Also check that the file is named index.html: without it there's nothing to serve at the root address.

GitLab's sign-in form opens instead of the site. Access to Pages is restricted to project members. This happens if the project was created private: after switching to Public, the Pages setting can stay as it was. Fix it under Settings → General → Visibility, project features, permissions: for Pages, pick Everyone with access instead of Only project members.

What's next

You have a repository, a three-stage pipeline, and a site that rolls out on its own. The scheme doesn't change from here, only the content grows: cp turns into a bundler, test -f turns into tests and linters, and at some point Pages gets replaced by an image registry and a real server.

If shared runners aren't available, the same pipeline works on your own runner: gitlab-runner is installed as a container, and the project gets a registration under Settings → CI/CD → Runners → New project runner. GitLab issues an authentication token, and the runner connects with it. From then on, the minutes are your own.

What to remember

The pages: true key plus a public folder with index.html. That's enough for static hosting, nothing else to configure. A different folder is set via pages.publish.

GitLab adds pages:deploy on its own, you don't write it. If it's missing from the pipeline, the site wasn't published, no matter how green the pipeline is.

The site's address lives under Deploy → Pages and looks like <project>-<hash>.gitlab.io.

rules with a branch check protect the live site from a rollout coming from an experiment.

What you'll need for the tasks

In the labs, a training runner on your laptop runs the pipeline: like GitLab Runner with the Docker executor, it runs every job in its own clean container. After a successful Pages job it, like GitLab, adds pages:deploy on its own and lays the site out in nginx on 127.0.0.1. There's no GitLab interface there, the runner prints the site's address in its output.

Done reading?

Mark the lesson as read to move your track progress forward.

Go through it with an AI agent

The agent explains anything unclear in plain words, gives you 2 labs for this lesson and drills you on interview questions. $49 one-time, lifetime access.

Get access for $49

Tell the agent: use MCP izi, give me lesson cicd-practice-05-pages-deploy