## Intro

GitLab CI/CD automates software delivery through pipelines defined in a .gitlab-ci.yml file, but testing changes directly on a production instance risks disrupting real projects and exposing sensitive data. A local lab creates a safe, isolated environment where you can experiment with pipeline configurations, debug failures, and learn CI/CD concepts without consequences. This guide walks through setting up a local GitLab instance and GitLab Runner on a single Linux machine, with practical examples that demonstrate core CI/CD concepts. By the end, you will have a repeatable setup for validating pipelines before they reach shared infrastructure.

## Version and Environment Inventory

Before starting, confirm your environment meets the requirements. The instructions assume a Linux host; Ubuntu 22.04 LTS is used in examples, but similar steps apply to other distributions with minor changes.

### Software Versions

<div class="my-stack-md overflow-x-auto">
<table class="min-w-[42rem] border-collapse text-left">
<thead><tr><th scope="col" class="border border-outline-variant bg-surface-container-low px-4 py-3 text-left font-label-md font-semibold text-on-surface">Component</th><th scope="col" class="border border-outline-variant bg-surface-container-low px-4 py-3 text-left font-label-md font-semibold text-on-surface">Version</th><th scope="col" class="border border-outline-variant bg-surface-container-low px-4 py-3 text-left font-label-md font-semibold text-on-surface">Notes</th></tr></thead>
<tbody><tr><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">GitLab CE</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">16.10.0</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Installed via Omnibus package</td></tr>
<tr><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">GitLab Runner</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">16.10.0</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Installed via official repository</td></tr>
<tr><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Docker</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">24.0.5</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Used as runner executor (optional)</td></tr>
<tr><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">OS</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Ubuntu 22.04 LTS</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Kernel 5.15</td></tr></tbody>
</table>
</div>

### Topology

A single machine hosts both GitLab and the runner. GitLab runs as a system service on HTTPS port 443 (or HTTP port 80 if no TLS is configured). The runner registers with GitLab using a registration token and executes jobs locally using the shell executor by default. For containerized jobs, a Docker executor can be configured if Docker is installed.

### Prerequisites

- 4 GB RAM minimum (8 GB recommended for smoother performance)

- 10 GB free disk space

- Sudo or root access on the machine

- Internet connectivity for downloading packages

### Installation Steps

First, update package lists and install required dependencies:

sudo apt update && sudo apt install -y curl openssh-server ca-certificates tzdata perl 
 Add the GitLab package repository and install GitLab CE. Replace http://gitlab.local with your machine's IP or hostname. The installation may take several minutes.

curl https://packages.gitlab.com/install/repositories/gitlab/gitlab-ce/script.deb.sh | sudo bash
sudo EXTERNAL_URL="http://gitlab.local" apt install gitlab-ce 
 Install GitLab Runner using the official repository script:

curl -L "https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.deb.sh" | sudo bash
sudo apt install gitlab-runner 
 Verify GitLab is running by checking the status of its services:

sudo gitlab-ctl status 
 Expected output shows a list of services with run status, for example:

run: gitlab-workhorse: (pid 1234) 10s; run: log: (pid 1233) 10s
run: logrotate: (pid 1235) 10s; run: log: (pid 1232) 10s
... 
 After installation, access the GitLab web interface by navigating to the configured URL. During the first visit, you will be prompted to set a password for the root user.

## Safe Configuration Path

Start with a minimal configuration that limits exposure and simplifies debugging. This section covers creating a test project, registering a runner, and defining a basic pipeline.

### Create a Project

Create a blank project in GitLab via the web UI or API. For automation, use the API with a personal access token (generate one under User Settings > Access Tokens with api scope).

curl --request POST --header "PRIVATE-TOKEN: <your_token>" \
 --data "name=my-test-project&visibility=private" \
 "http://gitlab.local/api/v4/projects" 
 Expected output includes the new project's details, such as "id":1 .

### Register a Runner

Register a runner with the shell executor to avoid extra dependencies. First, obtain the project's registration token from the web UI under Settings > CI/CD > Runners (or use the instance-level token for shared runners). Then run:

sudo gitlab-runner register 
 When prompted, enter the following:

- GitLab instance URL (e.g., http://gitlab.local )

- Registration token from the project's CI/CD settings

- Description (e.g., local-shell-runner )

- Tags (optional, e.g., shell )

- Executor: shell

After registration, the runner configuration file /etc/gitlab-runner/config.toml will contain an entry similar to:

[[runners]]
 name = "local-shell-runner"
 url = "http://gitlab.local"
 token = "xxxxxxxxxxxx"
 executor = "shell" 

### Define a Basic Pipeline

 Create a .gitlab-ci.yml file in the project repository. This simple pipeline defines two stages ( build and test ) and prints environment information. The build-job creates an artifact that the test-job verifies.

stages:
 - build
 - test

before_script:
 - echo "Starting job $CI_JOB_NAME"

build-job:
 stage: build
 script:
 - echo "Building the project..."
 - mkdir -p output
 - echo "Build artifact" > output/build.txt
 artifacts:
 paths:
 - output/

test-job:
 stage: test
 script:
 - echo "Running tests..."
 - test -f output/build.txt && echo "Artifact found" 
 Commit and push this file to trigger the pipeline. The before_script runs before each job, printing the job name. The artifact from build-job is available in test-job because artifacts are passed between stages by default.

### Scoped Choices

- Use the shell executor initially to avoid Docker configuration complexities.

- Keep the project private to restrict access.

- Use a separate runner for each project to isolate resources (or use tags to direct jobs).

## Verification and Diagnostics

After pushing the pipeline configuration, verify that jobs run successfully and inspect artifacts.

### Pipeline Status

Check pipeline status via the web UI (CI/CD > Pipelines) or using the GitLab CLI ( glab ) if installed:

glab ci status 
 If using the API, retrieve the latest pipeline for project ID 1:

curl --header "PRIVATE-TOKEN: <your_token>" \
 "http://gitlab.local/api/v4/projects/1/pipelines/latest" 
 Look for "status":"success" in the JSON response.

### Job Logs

View job logs to confirm each step executed correctly. In the web UI, navigate to CI/CD > Pipelines, then click the pipeline to see its jobs. Click on a job to see its log. For a shell runner, logs are also stored on the runner machine in /var/log/gitlab-runner/ , but the UI provides the easiest access.

Example job log excerpt for build-job :

$ echo "Starting job build-job"
Starting job build-job
$ echo "Building the project..."
Building the project...
$ mkdir -p output
$ echo "Build artifact" > output/build.txt
Uploading artifacts for successful job 

### Runner Verification

 Ensure the runner is active and connected:

sudo gitlab-runner verify 
 Output:

Verifying runner... is alive 

### Common Diagnostic Commands

- sudo gitlab-ctl tail - View GitLab logs

- sudo gitlab-runner --debug run - Run runner in debug mode (foreground)

- curl -I http://gitlab.local - Check web server response headers

## Failure Modes and Recovery

 Common failures include runner connectivity issues, permission errors, and misconfigured pipelines. This section describes how to diagnose and recover.

### Runner Not Picking Up Jobs

Symptoms: Pipeline shows pending and the runner does not execute jobs.

Diagnosis: Check runner status:

sudo gitlab-runner status 
 Check runner registration:

sudo gitlab-runner list 
 Recovery: Ensure the runner is registered for the correct project and that the token matches. If not, re-register with the correct token:

sudo gitlab-runner register 
 Also verify network connectivity between runner and GitLab (ping, curl).

### Permission Denied Errors

Symptoms: Job fails with Permission denied when writing files.

Diagnosis: The shell executor runs as the gitlab-runner user, which may not have write permissions in the working directory (default: /home/gitlab-runner/builds ).

Recovery: Adjust permissions or use a different executor. For a temporary fix, add the gitlab-runner user to the appropriate group (e.g., www-data if building web files) and restart the runner:

sudo usermod -aG www-data gitlab-runner
sudo systemctl restart gitlab-runner 
 For persistent issues, consider using the Docker executor to isolate file permissions.

### Invalid YAML Configuration

Symptoms: Pipeline fails immediately with a YAML syntax error.

Diagnosis: Use a YAML linter or the GitLab CI Lint tool available in the web UI (CI/CD > Pipelines > CI Lint). Paste the .gitlab-ci.yml content to see validation errors.

Recovery: Fix the YAML and push again. Common issues include incorrect indentation, missing colons, or using tabs instead of spaces.

### Docker Executor Issues (if used)

Symptoms: Job fails with Cannot connect to the Docker daemon .

Diagnosis: The Docker service may not be running, or the runner lacks permissions to access the Docker socket.

Recovery: Start Docker and add the runner user to the docker group:

sudo systemctl start docker
sudo usermod -aG docker gitlab-runner
sudo systemctl restart gitlab-runner 
 Verify that the runner can run Docker containers:

sudo -u gitlab-runner docker run hello-world 

### Rollback

 To revert changes, simply edit .gitlab-ci.yml to the last known good version and push. For runner configuration, restore /etc/gitlab-runner/config.toml from a backup or re-register the runner.

## Operations Checklist

Use this checklist when setting up or modifying your local GitLab CI/CD lab.

<div class="my-stack-md overflow-x-auto">
<table class="min-w-[42rem] border-collapse text-left">
<thead><tr><th scope="col" class="border border-outline-variant bg-surface-container-low px-4 py-3 text-left font-label-md font-semibold text-on-surface">Task</th><th scope="col" class="border border-outline-variant bg-surface-container-low px-4 py-3 text-left font-label-md font-semibold text-on-surface">Command / Action</th><th scope="col" class="border border-outline-variant bg-surface-container-low px-4 py-3 text-left font-label-md font-semibold text-on-surface">Expected Result</th></tr></thead>
<tbody><tr><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Verify GitLab is running</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant"><code class="font-mono text-[0.9em] bg-surface-container px-1 py-0.5 rounded">sudo gitlab-ctl status</code></td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">All services show <code class="font-mono text-[0.9em] bg-surface-container px-1 py-0.5 rounded">run</code></td></tr>
<tr><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Check runner status</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant"><code class="font-mono text-[0.9em] bg-surface-container px-1 py-0.5 rounded">sudo gitlab-runner status</code></td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant"><code class="font-mono text-[0.9em] bg-surface-container px-1 py-0.5 rounded">gitlab-runner: Service is running</code></td></tr>
<tr><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">List registered runners</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant"><code class="font-mono text-[0.9em] bg-surface-container px-1 py-0.5 rounded">sudo gitlab-runner list</code></td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Shows runner with correct URL and executor</td></tr>
<tr><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Validate YAML syntax</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Use CI Lint in web UI</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">No syntax errors</td></tr>
<tr><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Push pipeline</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant"><code class="font-mono text-[0.9em] bg-surface-container px-1 py-0.5 rounded">git push origin main</code></td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Pipeline triggered and succeeds</td></tr>
<tr><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Check job logs</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Via web UI or API</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">No unexpected errors</td></tr>
<tr><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Verify artifacts</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Download from pipeline page</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Files present</td></tr>
<tr><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Cleanup old projects</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Delete project or pipeline</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Resources freed</td></tr></tbody>
</table>
</div>
Regularly update GitLab and the runner to the latest patch versions to maintain compatibility and security. For example, run sudo apt update && sudo apt upgrade gitlab-ce gitlab-runner monthly.

## Conclusion

A local GitLab CI/CD lab is a valuable asset for developing and testing pipelines safely. By following this guide, you have installed GitLab and a runner, created a test project, defined a basic pipeline, and learned how to verify and troubleshoot common issues. Next steps include experimenting with more complex pipeline features such as environments, multi-stage deployments, and containerized jobs. With this foundation, you can confidently validate CI/CD configurations before promoting them to production.