Skip to content
+1 (813) 212-3723 support@layeronecloud.com
Self-hosting VPS guides

How to Run a GitLab Runner on a VPS with the Docker Executor

Install a self-managed GitLab Runner on Ubuntu, register the Docker executor, close every inbound service port, constrain concurrency, and secure CI jobs.

7 min read

A self-managed GitLab Runner moves CI/CD work onto a VPS you control. It is not the GitLab application itself: the runner polls GitLab over outbound HTTPS, starts job containers with the Docker executor, uploads the result, and waits for more work. That design means a basic runner needs no public application port.

This guide targets Ubuntu 24.04 LTS on x86-64, the official GitLab Runner apt repository, Docker Engine, and one non-privileged Docker job at a time. GitLab explicitly recommends installing a runner separately from the GitLab server. Treat it as a remote-code-execution host, because every accepted pipeline is code running on this VPS.

GitLab Runner VPS requirements and LayerOne cost

Item Practical starting point
Minimum RAM 2 GB for one small lint or test job; language toolchains may need much more
Recommended RAM 4 GB for one general-purpose Docker job with host headroom
Expected CPU usage Runner manager is usually below 1–2% of one vCPU while idle; a compile, test, compression, or image build can sustain 100% of every vCPU assigned
Storage requirement 40 GB is a small floor; use 60 GB or more for the OS, job workspaces, images, layers, and cache, then monitor actual churn
Exact LayerOne SKU gc.small (layerone-4g): 2 vCPU, 4 GB RAM, 60 GB disk
Expected monthly cost $0.0109/hour, about $8.00 for 730 hours at the catalog's monthly equivalent

CI sizing is determined by the job, not by gitlab-runner itself. A Java build, browser test, Android build, container image build, or security scanner can consume many times the resources of a small Python lint job. Start with concurrent = 1, record representative peak CPU, RAM, and disk use, then resize the VPS or add isolated runners.

LayerOne bills the VPS hourly from prepaid credit. The monthly equivalent above does not include taxes, optional services, external registry or GitLab charges, or transfer outside the account allowance. Check Pricing for the current catalog.

1. Harden the host and install Docker

Complete Your first hour on a new server, then install Docker Engine and the Compose plugin using Docker on a VPS. Keep the host dedicated to CI; do not place a production database, application secrets, or a control panel on the same server.

Verify Docker before adding the runner:

sudo docker version
sudo docker run --rm hello-world

2. Install GitLab Runner from GitLab's repository

GitLab's official repository setup script is downloadable so you can inspect it before running it:

sudo apt update
sudo apt install -y ca-certificates curl
curl -L "https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.deb.sh"   -o /tmp/gitlab-runner-repository.sh
less /tmp/gitlab-runner-repository.sh
sudo bash /tmp/gitlab-runner-repository.sh
sudo apt install -y gitlab-runner
gitlab-runner --version
sudo systemctl status gitlab-runner --no-pager

The Docker executor needs permission to reach the local Docker daemon. The docker group is effectively root-equivalent on this host, which is one reason the runner must be isolated:

sudo usermod -aG docker gitlab-runner
sudo systemctl restart gitlab-runner
sudo -u gitlab-runner -H docker version

Never expose Docker's unauthenticated TCP socket on ports 2375 or 2376.

3. Register one scoped runner without leaking its token

In GitLab, create a project runner or narrowly scoped group runner. Copy the runner authentication token, whose current prefix is glrt-. Run interactive registration so the token does not land in shell history:

sudo gitlab-runner register

Use these answers:

Prompt Value
GitLab instance URL https://gitlab.com/ or the root URL of your self-managed GitLab instance
Authentication token The glrt-... token from the runner setup page
Description layerone-docker-runner
Tags layerone (or a workload-specific tag)
Executor docker
Default Docker image ubuntu:24.04

Then verify both the local service and its GitLab connection:

sudo gitlab-runner list
sudo gitlab-runner verify
sudo gitlab-runner status
sudo journalctl -u gitlab-runner -n 100 --no-pager

verify proves that the registered manager can contact GitLab; a smoke pipeline proves that the executor can actually start a job.

4. Keep concurrency inside the VPS capacity

Open /etc/gitlab-runner/config.toml and keep the global setting at one job while establishing a baseline:

concurrent = 1

Do not enable privileged = true merely because a build asks for it. GitLab warns that privileged containers can escape the expected isolation and gain root-level control of the host. If a trusted protected pipeline truly needs Docker-in-Docker, put it on a separate, replaceable runner and follow GitLab's rootless/restricted privileged guidance.

Validate configuration before restarting:

sudo gitlab-runner lint --config /etc/gitlab-runner/config.toml
sudo systemctl restart gitlab-runner

5. Use an outbound-only firewall posture

The runner initiates its connection to GitLab and registries. No inbound CI service port is required. Replace the example administrator address:

ADMIN_CIDR="198.51.100.24/32"
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow from "$ADMIN_CIDR" to any port 22 proto tcp
sudo ufw enable
sudo ufw status verbose

Set the LayerOne cloud firewall inbound policy to DROP, allow TCP 22 only from the administrator CIDR, and add no public web, Docker, metrics, cache, or debug ports. The default outbound policy can remain ACCEPT; a runner normally needs DNS plus HTTPS access to GitLab, container registries, package repositories, and the destinations its approved jobs use.

Port Direction Policy
TCP 22 Inbound Administrator CIDR only; remove when administration is entirely over a private access path
TCP 443 Outbound GitLab API/long polling, registries, packages, and artifacts
UDP/TCP 53 Outbound DNS resolution when using conventional resolvers
TCP 2375/2376 Inbound Never expose the Docker API
Application test ports Inbound Do not publish; keep job services on Docker's internal network

Docker-published ports can bypass UFW. The upstream LayerOne firewall still blocks them when inbound is DROP, but jobs should not publish host ports at all unless the pipeline explicitly needs them.

6. Run a smoke pipeline

Add a temporary job to a project that is allowed to use the runner:

runner-smoke:
  image: ubuntu:24.04
  tags:
    - layerone
  script:
    - nproc
    - head -n 3 /proc/meminfo
    - df -h /
    - echo runner-ok

The job should be picked up by layerone-docker-runner and finish with runner-ok. During the job, inspect the host:

sudo docker stats --no-stream
free -h
df -h /

If the job is stuck, confirm its tag matches, the runner is not paused, and gitlab-runner verify reports it alive.

7. Control disk growth

Images, stopped containers, build directories, and caches can fill a small CI disk quickly:

sudo docker system df
sudo du -sh /var/lib/docker /home/gitlab-runner 2>/dev/null
df -h /

Delete only resources you have confirmed are unused. For example, this removes unused images older than seven days but does not delete named volumes:

sudo docker image prune -af --filter "until=168h"

Do not make docker system prune --volumes a blind cron job. Build caches are disposable only if the pipeline can recreate them; artifacts that matter should be uploaded to GitLab or an artifact store rather than living only on the runner.

8. Back up, restore, replace, and upgrade deliberately

Update the runner from the official repository during a quiet window:

sudo apt update
sudo apt install --only-upgrade gitlab-runner
sudo gitlab-runner verify
sudo systemctl status gitlab-runner --no-pager

Runner hosts should be replaceable. A runner backup normally needs only /etc/gitlab-runner/config.toml and any deliberately retained cache; build artifacts and source should already exist in GitLab or an artifact store. The configuration contains a runner authentication token, so encrypt the backup and restrict access.

For a clean restore, install a fresh host and register it again instead of restoring Docker's entire data directory. If restoring config.toml, keep the old manager offline, run gitlab-runner verify, and remove the old manager from GitLab after replacement so a stolen or duplicate token cannot reconnect.

Security checklist

  • Scope the runner to the smallest project or group that needs it.
  • Use protected runners/tags for deployment jobs and protected secrets.
  • Never run unreviewed fork pipelines on a runner that can reach production.
  • Leave Docker executor privileged off; do not mount /var/run/docker.sock into job containers.
  • Keep long-lived cloud credentials and SSH keys off the host filesystem.
  • Put runners in a separate network segment; do not attach them to a trusted private network without explicit need.
  • Keep one job at a time until measurements justify more concurrency.
  • Monitor free disk, OOM kills, job queue time, and runner version.

Official references

Still stuck

Chat with us from the portal.

Ask the assistant from the Chat bar. During business hours you can ask for a person and a human joins live.