This guide shows you how to set up a GitLab Runner on a VPS and connect it to a GitLab project, so every push runs your .gitlab-ci.yml pipeline on hardware you control. You will install the runner, register it with a project token, choose an executor and watch the first pipeline finish.
Before you start
Have these ready:
- A Linux VPS running Ubuntu 22.04/24.04, Debian 12, AlmaLinux or Rocky 8–9, with root or
sudoaccess. - A GitLab account with at least Maintainer rights on the project you want to build.
- The project already pushed to GitLab, whether on GitLab.com or a self-managed instance.
- Outbound HTTPS from the VPS to your GitLab instance and to any container registries your jobs pull from.
Log in over SSH and update the package index:
sudo apt update && sudo apt upgrade -yNote: A runner is a build machine. Keep it patched and do not store personal data on it.
Install GitLab Runner on the VPS
Install the official package so systemd manages the service for you.
Ubuntu and Debian:
curl -L "https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.deb.sh" | sudo bash
sudo apt install -y gitlab-runnerAlmaLinux and Rocky:
curl -L "https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.rpm.sh" | sudo bash
sudo dnf install -y gitlab-runnerConfirm the binary and the service:
gitlab-runner --version
systemctl status gitlab-runner --no-pagerThe package creates a gitlab-runner system user and a systemd unit of the same name. The binary runs as that unprivileged user, which matters when you pick an executor.
Create the runner in GitLab and copy its token
Registration needs a token issued by GitLab. Recent releases use a runner authentication token starting with glrt-; older setups used a shared registration token. The UI wording changes between versions, so follow the current GitLab documentation for your release.
- Open your project in GitLab.
- Go to Settings → CI/CD → Runners.
- Select New project runner, add a description and tags such as
vps,docker. - Leave Run untagged jobs off unless you want this runner to take every job in the project.
- Create the runner and copy the token shown. You will not see it again.
Warning: That token lets a machine run jobs inside your project. Store it in a password manager or secrets store, never in the repository.
Register the runner on the VPS
Register with the token. The non-interactive form is easier to repeat and script.
Docker executor:
sudo gitlab-runner register \
--non-interactive \
--url "https://gitlab.com/" \
--token "glrt-REPLACE_WITH_YOUR_TOKEN" \
--executor "docker" \
--docker-image "alpine:latest" \
--description "vps-docker-runner"Shell executor:
sudo gitlab-runner register \
--non-interactive \
--url "https://gitlab.com/" \
--token "glrt-REPLACE_WITH_YOUR_TOKEN" \
--executor "shell" \
--description "vps-shell-runner"For a self-managed instance, replace the URL with something like https://gitlab.example.com/. Registration writes the result to /etc/gitlab-runner/config.toml and restarts the service.
Which executor to choose
- Docker runs every job in a fresh container built from the image you name. Jobs are isolated, cleanup is automatic and the host stays tidy.
- Shell runs job scripts directly as the
gitlab-runneruser. It is simple, but jobs can touch anything that user can reach and leftovers stay on disk.
Docker is the better default on a VPS. Pick shell only when a job genuinely needs the host, such as building a native package against system libraries.
Prepare the Docker executor
The Docker executor talks to a local Docker daemon. Install Docker Engine and give the runner user access to the socket:
curl -fsSL https://get.docker.com | sudo sh
sudo usermod -aG docker gitlab-runner
sudo systemctl restart gitlab-runnerCheck both:
sudo docker info | head -n 5
id gitlab-runnerWarning: Membership of the docker group is effectively root on the host. Anyone who can push a job to this runner can mount / or read other containers' volumes. Enable it only for projects you trust.
Tune the runner in /etc/gitlab-runner/config.toml:
concurrent = 4
check_interval = 3
[[runners]]
name = "vps-docker-runner"
url = "https://gitlab.com/"
executor = "docker"
[runners.docker]
image = "alpine:latest"
privileged = false
volumes = ["/cache"]concurrent caps how many jobs run at the same time — keep it close to your CPU count so one pipeline cannot starve the box. Apply changes with sudo systemctl restart gitlab-runner. If jobs need a shared build cache between containers, check the cache section of the GitLab Runner documentation.
Add a.gitlab-ci.yml pipeline
Commit a small pipeline to the default branch. This one builds and tests inside a throwaway Alpine container:
stages:
- build
- test
build-job:
stage: build
image: alpine:latest
script:
- echo "Building on $(hostname)"
- apk add --no-cache curl
- curl -sI https://example.com | head -n 1
test-job:
stage: test
image: alpine:latest
script:
- echo "Running tests"Push it:
git add .gitlab-ci.yml
git commit -m "Add CI pipeline"
git push origin mainOpen Build → Pipelines in GitLab. The job moves from pending to running, then passes. The first run pulls the image, so it takes longer than later ones.
Check it works
Run these on the VPS:
sudo gitlab-runner list
sudo gitlab-runner verify
systemctl is-active gitlab-runnerlist prints the runners from config.toml with their executors, and verify contacts GitLab to confirm each token is still valid. The real confirmation is a green pipeline in the project view, with your runner's name shown next to the job.
Troubleshooting
- "This job is stuck because you don't have any active runners online." The runner is registered but not picking up work. Run
sudo gitlab-runner verify,
