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.
- The
pagesandpages.publishkeys, andruleswithif, in.gitlab-ci.yml. - The
$CI_COMMIT_BRANCHand$CI_DEFAULT_BRANCHvariables. git -C work commit -am "..."andgit -C work switch: like GitLab, the runner only picks up what's committed, and the branch affectsrules.bash run-pipeline.sh: run the pipeline against the latest commit.curl -s http://127.0.0.1:<port>/or a browser: see what's actually published.
Done reading?
Mark the lesson as read to move your track progress forward.
Lesson marked as done.
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.
Tell the agent: use MCP izi, give me lesson cicd-practice-05-pages-deploy
Progress saved in this browser
Clear your history or open this on your phone, and the marks will be gone. Leave your email and they'll move to your account. No password needed.
Done. moved to your account. You can see your progress on any device. My progress