A local GitLab Runner lab gives you a safe, resettable place to test jobs, troubleshoot behavior, and learn by doing without touching shared infrastructure. In this guide you will:

- Install GitLab Runner for local use.

- Choose a safe configuration that limits blast radius.

- Run two practical examples using the shell and Docker executors.

- Verify results and capture diagnostics.

- Recognize common failure modes and recover fast.

- Use a repeatable checklist for day-to-day operations.

The core idea is simple: keep your first pilot narrow, measurable, and easy to inspect locally before any broader rollout. That approach speeds up learning and cuts rework.

## Version and Environment Inventory

Before touching anything, write down exactly what you will run. It turns "works on my machine" into repeatable steps your teammates can execute.

<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">Item</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">Example value</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">Why it matters</th></tr></thead>
<tbody><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 x86_64</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Determines packages, paths, and service tools</td></tr>
<tr><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Hostname</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">dev-ws-01</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Distinguishes logs and lab artifacts</td></tr>
<tr><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Lab user</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">runnerlab (non-root)</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Limits privileges and scope</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.x (amd64 binary)</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Reproducible flags and behavior</td></tr>
<tr><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Optional runtime</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Docker 24.x, group=docker</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Required for Docker executor experiments</td></tr>
<tr><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">CPU/RAM budget</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">2 vCPU, 4 GB RAM</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Sets expectations for job duration</td></tr></tbody>
</table>
</div>
Tips:

- Use a dedicated non-root account for all lab actions.

- Keep lab files under a single directory tree so you can back it up or delete it in one step.

- If you will experiment with Docker, ensure your user is in the docker group and that images are pre-pulled when the network is slow or restricted.

## Safe Configuration Path

A safe local lab is intentionally small and controlled. The following choices keep you fast and safe:

- Scope: Start with local "exec" runs (no registration to a GitLab server). You can layer on registration later if you want to test full pipelines, but local exec is enough for most experiments.

- Identity: Create a dedicated non-root user, for example "runnerlab".

- Directories: Use per-lab directories like ~/lab/work , ~/lab/cache , and ~/lab/logs . Do not use system directories.

- Executor choices:

- Shell executor: simplest, uses your host tools. Great for command prototyping and quick checks.

- Docker executor: optional, helpful when you need a clean toolchain or OS-level isolation.

- Privileges: Avoid privileged containers and avoid mounting sensitive host paths.

- Resource guardrails: Keep jobs small. If using Docker, avoid huge images and pre-pull what you need. On the host, avoid running memory-starving jobs.

A quick comparison to guide your choice:

<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">Executor</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">When to use</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">Pros</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">Cons</th></tr></thead>
<tbody><tr><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">shell</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Fast local commands and scripts</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Simple, no images needed</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Shares host toolchain and environment</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">Toolchain isolation and reproducibility</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Clean environment, easy language setup</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Needs Docker and images, possible permissions issues</td></tr></tbody>
</table>
</div>

## Implementation Steps

The commands below focus on Linux (Ubuntu/Debian-like). Adjust paths for other platforms.

### 1. Create a dedicated lab user

# Create a non-root user for lab work
sudo useradd -m -s /bin/bash runnerlab

# Create lab directories (as the lab user)
sudo -iu runnerlab bash -lc '
 mkdir -p ~/lab/{work,cache,logs}
 printf "Lab directories created under: %s\n" "$HOME/lab"
' 

### 2. Install GitLab Runner (standalone binary)

 # Download and install the GitLab Runner binary
sudo curl -L -o /usr/local/bin/gitlab-runner \
 https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-linux-amd64
sudo chmod +x /usr/local/bin/gitlab-runner

# Verify installation
gitlab-runner --version
# Expected result: a version line like "Version: 16.x" plus build info. 

### 3. (Optional) Install Docker for the Docker executor

 # Install Docker engine (Ubuntu example)
sudo apt-get update
sudo apt-get install -y docker.io

# Allow the lab user to access the Docker socket
sudo groupadd -f docker
sudo usermod -aG docker runnerlab

# Refresh group membership in the current shell (new login also works)
sudo -iu runnerlab bash -lc 'id && docker info >/dev/null && echo Docker OK'
# Expected result: the lab user is in the docker group and "docker info" succeeds without sudo. 

## Practical Examples

 The examples use gitlab-runner exec which runs a job from a local .gitlab-ci.yml without registering a runner with a GitLab instance.

Important notes:

- Each example runs in your current working directory, so keep files under ~/lab/work .

- You can override build and cache locations with flags to keep everything inside your lab directory.

### Example 1: Shell executor, print system info

### 1. Prepare a working directory and minimal pipeline file

sudo -iu runnerlab bash -lc '
 mkdir -p ~/lab/work/example1 && cd ~/lab/work/example1
 cat > .gitlab-ci.yml <<"YAML"
lab_echo:
 stage: test
 script:
 - echo "Hello from shell executor"
 - uname -a
 - printf "PWD=%s\n" "$PWD"
 - echo "Done"
YAML
 printf "Created: %s/.gitlab-ci.yml\n" "$PWD"
' 

### 2. Run the job locally with the shell executor

 sudo -iu runnerlab bash -lc '
 cd ~/lab/work/example1
 gitlab-runner exec shell lab_echo \
 --builds-dir "$HOME/lab/work" \
 --cache-dir "$HOME/lab/cache" \
 2>&1 | tee "$HOME/lab/logs/example1_shell.log"
' 
 Expected result:

- Output begins with "Running with gitlab-runner..." and "Using Shell (bash) executor".

- Your echo, uname, and PWD lines appear.

- Final line shows "Job succeeded".

To re-run cleanly, just re-execute the same command; nothing is left behind except the log you chose to save.

### Example 2: Docker executor, run Python test

This example runs a tiny Python test in a container image to demonstrate isolation.

### 1. Prepare files

sudo -iu runnerlab bash -lc '
 mkdir -p ~/lab/work/example2/tests && cd ~/lab/work/example2
 # A tiny test file
 cat > tests/test_math.py <<"PY"
import math

def test_add():
 assert 2 + 2 == 4

def test_sqrt():
 assert math.isclose(math.sqrt(9), 3.0)
PY

 # Minimal pipeline that installs pytest and runs tests
 cat > .gitlab-ci.yml <<"YAML"
lab_pytest:
 image: python:3.11-slim
 stage: test
 before_script:
 - python --version
 - pip install --no-cache-dir pytest==7.4.4
 script:
 - pytest -q
YAML
 printf "Created example2 with tests and .gitlab-ci.yml\n"
' 

### 2. Pre-pull the image (optional but speeds up first run)

 sudo -iu runnerlab docker pull python:3.11-slim 

### 3. Run the job locally with the Docker executor

 sudo -iu runnerlab bash -lc '
 cd ~/lab/work/example2
 gitlab-runner exec docker lab_pytest \
 --builds-dir "$HOME/lab/work" \
 --cache-dir "$HOME/lab/cache" \
 --docker-pull-policy if-not-present \
 2>&1 | tee "$HOME/lab/logs/example2_docker.log"
' 
 Expected result:

- Output shows "Using Docker executor" and the python:3.11-slim image.

- Python version prints, pytest installs, and 2 tests pass.

- Final line shows "Job succeeded".

## Verification and Diagnostics

These checks confirm your lab is healthy and make troubleshooting faster.

<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">Check</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</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">Runner version</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 --version</code></td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Version line with 16.x info</td></tr>
<tr><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Shell example log exists</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">ls -l ~runnerlab/lab/logs/example1_shell.log</code></td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">File present with recent timestamp</td></tr>
<tr><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Docker example log exists</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">ls -l ~runnerlab/lab/logs/example2_docker.log</code></td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">File present with recent timestamp</td></tr>
<tr><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Docker group membership</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">id runnerlab</code></td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Shows &quot;docker&quot; in groups</td></tr>
<tr><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Docker connectivity</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 -iu runnerlab docker info</code></td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Succeeds without sudo</td></tr></tbody>
</table>
</div>
Additional tips:

- Increase verbosity: add --debug to any gitlab-runner command.

sudo -iu runnerlab bash -lc '
 cd ~/lab/work/example1
 gitlab-runner --debug exec shell lab_echo | sed -n "1,60p"
' 

- Force a clean workspace by removing build directories under your lab work path if a prior run left artifacts.

- For Docker executor, pre-pull images and verify image digests when you need reproducibility.

- Pin language tools or test framework versions in your job scripts so re-runs behave the same.

## Failure Modes and Recovery

 Common issues and quick fixes:

### 1. Permission denied on Docker socket

- Symptom: "Got permission denied while trying to connect to the Docker daemon socket"

- Fix:

sudo usermod -aG docker runnerlab
# New login required to pick up group; then verify
sudo -iu runnerlab docker info 

### 2. Command not found in shell job

- Symptom: A tool works in your login shell but not in the job.

- Cause: PATH differs or the tool is not installed for the lab user.

- Fix: Install the tool for the host or preface commands with a deterministic path, e.g., /usr/bin/env python3 . In .gitlab-ci.yml you can also set PATH or install prerequisites in before_script .

### 3. YAML or quoting errors

- Symptom: gitlab-runner fails to parse the job or the shell treats arguments incorrectly.

- Fix: Use proper YAML indentation and quote strings with spaces. Example:

 script:
 - bash -lc 'echo "Value: $MYVAR"' 

### 4. Docker image pulls are slow or fail

- Symptom: Timeouts or 429 errors when pulling.

- Fix: Pre-pull the image; use --docker-pull-policy if-not-present ; choose a smaller base image.

### 5. Residual files break re-runs

- Symptom: A previous run left build products that change behavior.

- Fix:

 sudo -iu runnerlab bash -lc '
 cd ~/lab/work/example2
 git clean -fdx || true
' 

### 6. Mismatched interpreter

- Symptom: "python: command not found" or shell differences.

- Fix: Use explicit interpreters ( python3 ). For non-bash defaults, run script lines through bash -lc .

### Rollback and recovery steps

- Revert to a known-good GitLab Runner version:

 # Keep a copy of a working binary
sudo cp /usr/local/bin/gitlab-runner /usr/local/bin/gitlab-runner.good
# If you upgrade and regress, roll back quickly
sudo mv /usr/local/bin/gitlab-runner.good /usr/local/bin/gitlab-runner
sudo chmod +x /usr/local/bin/gitlab-runner
gitlab-runner --version 

- Remove lab artifacts (soft reset):

 sudo -iu runnerlab bash -lc 'rm -rf ~/lab/work/* ~/lab/cache/* ~/lab/logs/*' 

- Uninstall completely (hard reset):

 # Stop any user services you may have created (if any)
# Then remove user and binary
sudo userdel -r runnerlab || true
sudo rm -f /usr/local/bin/gitlab-runner 
 Verification after recovery:

- Confirm the runner version is what you expect.

- Confirm the lab directories are recreated before re-running examples.

- Re-run Example 1 to validate the baseline.

## Operations Checklist

Use this checklist to keep experiments predictable and reviewable.

### Preparation

- Confirm OS, user, and versions match your inventory table.

- Ensure the lab user exists and can run gitlab-runner --version .

- For Docker tests, confirm docker info works for the lab user and required images are pulled.

### Pre-run checks

- Ensure .gitlab-ci.yml exists in the working directory and job names match.

- Optionally clear the working directory or run in a new folder.

- Decide on builds-dir and cache-dir flags so artifacts stay in the lab tree.

### Run

- Execute gitlab-runner exec shell JOB or gitlab-runner exec docker JOB .

- Capture stdout/stderr to a log under ~/lab/logs .

### After run

- Check the final "Job succeeded" line.

- Review the log for warnings and durations.

- Save or delete artifacts under your lab work path as needed.

### Periodic hygiene

- Prune old images if you use Docker and storage is tight.

- Clean cache directories to avoid stale dependencies.

- Update GitLab Runner deliberately; record the version bump in your inventory.

## Conclusion

You now have a small, safe GitLab Runner lab you can trust for testing and learning:

- A dedicated non-root user and lab directory keep experiments isolated.

- The shell executor gives you the fastest path to local validation.

- The Docker executor adds clean toolchain isolation when needed.

- Clear verification steps, expected outputs, and logs make results easy to review.

- Known failure modes have short, deterministic fixes and rollback.

- A lightweight checklist keeps operations repeatable.

Next steps:

- Extend your local jobs with realistic build and test steps, keeping each change small and measurable.

- Pin versions for tools and images so re-runs are consistent.

- When comfortable, register a runner against a non-production project to test full pipeline flows, still following the same safety principles.

A narrow, measurable pilot that runs locally end-to-end builds confidence quickly and reduces rework as you scale your use of GitLab Runner.