GitLab.com gives every project a small allowance of shared runner minutes, then meters everything above. Once you do any real CI work, you either pay per minute or you bring your own runner. A self-hosted GitLab Runner on an offshore VPS gives you unlimited build minutes, full control over the executor type (shell, docker, docker-machine), and a clean network from which to pull dependencies, push artifacts, and reach private services. AnubizHost VPS plans hand you root access on Debian, Ubuntu, or AlmaLinux with 1 Gbps uplinks and no automated abuse triggers on common DevOps traffic patterns.
Need this done for your project?
We implement, you ship. Async, documented, done in days.
GitLab.com shared runners are convenient until they are not. Once you cross the included minutes threshold, the per-minute price for additional runner time adds up fast for any team running tests on every commit. More importantly, shared runners have egress controls, image registry rate limits, and a small fixed pool of CPU and memory per job. Long compile jobs, Docker image builds with large base layers, and integration tests that need bound ports all bump into those limits.
Self-hosted runners solve every one of those issues. You get the full CPU and RAM of your VPS dedicated to the job, the runner pulls from any registry without auth shenanigans, and the egress IP is yours. For sensitive projects, the build artifacts and intermediate files never leave a server you own.
Running your runner offshore adds the privacy layer. Your build pipeline does not run on shared Google or AWS compute that ties back to your GitLab.com account location. Combine an offshore runner with an offshore git remote (your own Gitea or a self-hosted GitLab CE) and the entire dev pipeline is detached from US cloud surveillance.
Executor Choice - Shell vs Docker vs Kubernetes
The shell executor runs each job as a process on the VPS itself. It is simple and fast but leaves zero isolation between jobs - a malicious or buggy job can read every other build's secrets and modify the host system. Use shell executor only on a single-tenant runner dedicated to one trusted project.
The docker executor (recommended for most use cases) runs each job in a fresh container based on an image you specify in `.gitlab-ci.yml`. Jobs start in 1 to 3 seconds, are fully isolated from the host, and clean up automatically. Install Docker on your VPS with `curl -fsSL https://get.docker.com | sh` then register the runner with `gitlab-runner register --executor docker --docker-image alpine:latest`. A 4 GB RAM VPS comfortably runs 2 to 4 concurrent docker jobs.
For heavier needs, use docker-machine executor or Kubernetes executor to autoscale workers across multiple offshore VPSes. The docker-machine executor spins up additional Docker hosts on demand via cloud providers. The Kubernetes executor schedules each job as a Kubernetes pod on a cluster you control - useful if you already run k3s or a small k8s cluster across two or three offshore VPSes for production workloads.
Installing and Registering GitLab Runner on Debian
Add the GitLab Runner apt repository: `curl -L https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.deb.sh | bash`. Then install: `apt install -y gitlab-runner`. The package creates a `gitlab-runner` system user and a systemd service. Add this user to the docker group so the docker executor can spawn containers: `usermod -aG docker gitlab-runner` and restart with `systemctl restart gitlab-runner`.
Register the runner against your GitLab instance: `gitlab-runner register`. Provide the GitLab URL (`https://gitlab.com` or your self-hosted URL), the registration token from project Settings -> CI/CD -> Runners, a descriptive tag list, and the executor type. For docker executor, set the default image (alpine:latest is a common starting point) and decide whether to enable `--docker-privileged` for jobs that need to build their own Docker images via dind.
Edit `/etc/gitlab-runner/config.toml` to tune concurrency. Set `concurrent = 4` to allow up to 4 jobs in parallel on a 4-core VPS. Inside each `[[runners]]` block, set `[runners.docker]` options like `memory = "2g"` and `cpus = "1.5"` to cap individual jobs. Add `pull_policy = ["if-not-present"]` to cache base images locally and reduce egress to public registries. Restart with `systemctl restart gitlab-runner` and verify with `gitlab-runner status`.