The architecture of modern Continuous Integration and Continuous Deployment (CI/CD) pipelines often centers on the containerization of build environments. However, the fundamental component of this ecosystem, the GitLab Runner, is a highly versatile, single-binary application written in the Go programming language. While the industry standard frequently leans toward Docker-based execution to ensure environment parity, there are critical scenarios—ranging from local development testing to high-performance bare-metal requirements—where running the GitLab Runner without Docker is not only possible but preferred. The GitLab Runner functions as the bridge between the GitLab server (whether it is GitLab.com, GitLab Self-Managed, or GitLab Dedicated) and the actual execution of job scripts. Because it is distributed as a single binary without any external dependencies, it possesses the unique ability to operate directly on the host operating system, leveraging native shell environments such as Bash or PowerShell Core. This capability allows for the execution of jobs via the "shell" executor, which runs commands directly on the host's terminal, providing a level of hardware access and performance that containerization sometimes abstracts away.
Architectural Fundamentals of the GitLab Runner
The GitLab Runner is designed to be lightweight and highly scalable. It follows a specific execution flow to ensure that every job requested by the GitLab server is handled with integrity. The process begins with a registration phase, where the runner communicates with the GitLab instance using a registration token. Once registered, the runner enters a loop, constantly polling the GitLab server for new jobs.
The internal mechanics of the runner rely on several key features that remain consistent regardless of whether the runner is containerized or running natively on a host machine.
- Concurrent job execution: The runner can process multiple jobs simultaneously, maximizing the utilization of available CPU and RAM.
- Multi-token support: It can utilize multiple tokens across different servers, including per-project configurations.
- Concurrency limits: Administrators can strictly limit the number of concurrent jobs per specific token to prevent resource exhaustion.
- Environment customization: Users have the ability to customize the job running environment extensively.
- Automatic configuration reloading: The runner can ingest new configuration settings without requiring a full service restart.
- Prometheus integration: An embedded Prometheus metrics HTTP server allows for real-time monitoring of runner health and performance via referee workers.
The following table outlines the core operational characteristics of the GitLab Runner:
| Feature | Description | Impact on Workflow |
|---|---|---|
| Language | Go (Golang) | Enables a single-binary distribution with no dependencies. |
| Supported Shells | Bash, PowerShell Core, Windows PowerShell | Allows for native script execution on Linux, macOS, or Windows. |
| Deployment Models | Local, Docker, SSH, Cloud/Virtualization | Provides flexibility for diverse infrastructure needs. |
| Monitoring | Embedded Prometheus metrics server | Enables deep observability into job-specific data. |
Native Installation Methods Across Operating Systems
To run the GitLab Runner without Docker, the binary must be installed directly onto the host operating system. The installation process varies significantly depending on the target environment, whether it is a developer's local macOS machine or a production-grade Debian/Ubuntu server.
Debian and Ubuntu Systems
For Linux distributions based on Debian, the most efficient method is utilizing the official GitLab repositories. This ensures that the runner can be managed via the standard package manager, receiving updates and security patches through the system's existing update cycles.
- Download and execute the repository script:
curl -L https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.deb.sh | sudo bash - Install the package using the Advanced Package Tool:
sudo apt-get install gitlab-runner
The consequence of using this method is that the runner is integrated into the system's service manager (systemd), allowing it to start automatically upon boot and behave as a persistent background daemon.
CentOS and Red Hat Based Systems
CentOS environments require the use of the RPM repository script to ensure compatibility with the YUM package manager.
- Execute the repository setup script:
curl -L https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.rpm.sh | sudo bash - Install the runner via YUM:
sudo yum install gitlab-runner
macOS Environments
On macOS, the most seamless approach for developers is through the Homebrew package manager. This is particularly useful for local testing of CI/CD pipelines.
- Install the runner using Homebrew:
brew install gitlab-runner
Manual Binary Installation for Linux AMD64
In specialized environments where a package manager is unavailable or where a specific version is required, the manual download method is the most direct. This involves fetching the specific 64-bit Linux binary and placing it in a directory included in the system's PATH.
- Download the binary to the local bin directory:
sudo curl -L --output /usr/local/bin/gitlab-runner https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-linux-amd64
By placing the binary in /usr/local/bin/, the user ensures that the gitlab-runner command is globally accessible, facilitating easier integration with other automation tools and shell scripts.
Local Job Execution via the Shell Executor
One of the most powerful features of a non-Docker installation is the ability to use the exec command to run jobs locally. This is an essential tool for developers who need to validate their .gitlab-ci.yml configurations without pushing code to a remote server.
The exec command allows for two primary modes of local execution: the docker executor (which requires Docker to be present) and the shell executor. When running without Docker, the shell executor is the primary mechanism.
Executing Jobs with the Shell Executor
The shell executor runs the commands defined in the CI/CD configuration directly in the host's shell. This means if your .gitlab-ci.yml contains a command like npm install, it will execute using the Node.js installation present on your actual machine.
To execute a job named localbuild using the shell executor, use the following command:
gitlab-runner exec shell localbuild
The impact of this mode is profound. Unlike a containerized environment which provides a clean, isolated slate, the shell executor uses the host's environment. This means any environment variables, installed libraries, or system configurations present on the machine will influence the job outcome. This is highly effective for debugging "works on my machine" scenarios but requires careful management to ensure the host environment remains stable.
Comparison of Local Execution Modes
| Command | Executor Type | Requirement | Primary Use Case |
|---|---|---|---|
gitlab-runner exec docker <job> |
Docker | Docker Engine | Testing container-based CI pipelines locally. |
gitlab-runner exec shell <job> |
Shell | Native Shell (Bash/PowerShell) | Testing logic that depends on host-level tools/libraries. |
Resource Management and Server Architecture
A critical point of confusion in CI/CD architecture is the relationship between the GitLab server and the GitLab Runner. It is a best practice to maintain a strict separation between these two entities.
The Necessity of Server Separation
Even if a user is running GitLab itself within a Docker container, the GitLab Runner should ideally reside on a separate physical or virtual machine. This separation is driven by several technical and operational requirements:
- Hardware Resource Contention: GitLab is a resource-intensive application. It requires significant CPU and RAM to manage repositories, web interfaces, and database operations. Running a Runner on the same machine can lead to a "noisy neighbor" effect, where a heavy build job starves the GitLab server of the resources it needs to remain responsive.
- Scaling Capabilities: By separating the Runner, an organization can scale its build capacity independently of its GitLab instance. You can add ten more Runner servers to handle increased build load without ever touching the GitLab server configuration.
- Security Isolation: If a build job is compromised (e.g., through a malicious dependency), having the Runner on a separate machine limits the lateral movement capability of an attacker toward the core GitLab server and its sensitive data.
Resource Requirements
A common question regarding non-Docker installations is the resource footprint. It is important to note that whether the GitLab Runner is installed natively on Linux or running inside a Docker container, the amount of resources required for the runner process itself remains essentially the same. The resource consumption is primarily determined by the jobs being executed, not the runner's installation method. For instance, a job that compiles a massive C++ project will consume high CPU and RAM regardless of whether the runner is a native process or a containerized one.
Troubleshooting and Log Management in Native Environments
When running the GitLab Runner as a native application or a service, log management differs from the Docker-centric approach. In a Docker setup, one would typically use docker logs gitlab-runner. In a native installation, the location of logs is determined by how the runner was initialized.
If the runner is installed as a system service on a Linux machine, logs are typically handled by the system logger or redirected to a specific file defined in the service configuration.
- Checking service status on Linux:
sudo systemctl status gitlab-runner - Viewing system logs for the runner:
journalctl -u gitlab-runner
For macOS users who installed via Homebrew, the service management follows the brew services pattern:
- Checking service status:
brew services list - Restarting the service:
brew services restart gitlab-runner
Technical Analysis of the Runner-Server Interaction
The interaction between the runner and the GitLab server is governed by a specific sequence of API calls. Understanding this sequence is vital for troubleshooting connectivity issues in non-Docker environments.
The sequence begins with the registration phase:
1. The GitLab Runner initiates a POST /api/v4/runners request to the GitLab server.
2. This request includes the registration_token.
3. Upon successful validation, the GitLab server responds, and the runner is assigned a runner_token.
Once registration is complete, the runner enters the job-handling loop:
1. The GitLab Runner performs a polling request to GitLab to check for new jobs.
2. When a job is available, the server provides the job details.
3. The runner executes the job using the configured executor (in this case, the shell).
4. The runner reports the execution results (status, logs, etc.) back to the GitLab server via API.
This lifecycle ensures that the runner remains a "pull-based" agent, which is highly advantageous for security. Since the runner initiates the connection to the GitLab server, you do not need to open inbound ports on your runner's firewall, making it much easier to integrate runners located behind corporate NATs or in private networks.
Conclusion
The decision to run the GitLab Runner without Docker is a strategic one that offers significant advantages in terms of direct hardware access, reduced abstraction layers, and simplified local debugging. By utilizing the shell executor, developers can bridge the gap between their local development environments and the CI/CD pipeline, ensuring that the scripts executed in the cloud are compatible with the tools used on the ground. However, this power comes with the responsibility of environment management. Unlike Docker, which provides a clean and ephemeral environment for every job, a native shell executor relies on the stability and configuration of the host system. Therefore, maintaining clean, reproducible, and well-documented host environments is paramount when moving away from containerized runners. Ultimately, the GitLab Runner's ability to exist as a standalone, single-binary Go application makes it one of the most flexible components in the modern DevOps toolkit, capable of adapting to everything from a developer's laptop to a massive, bare-metal high-performance computing cluster.