## Intro

GitLab CI/CD pipelines automate the path from code commit to deployment, but when a pipeline fails, the error messages can be cryptic and the root cause elusive. Whether you are a developer pushing code, a DevOps consultant managing multiple projects, or a technical startup team building your first CI/CD workflow, systematic troubleshooting skills are essential. This guide provides practical, hands-on methods to diagnose and resolve common GitLab CI/CD failures. We will cover environment inventory, safe configuration changes, verification commands, failure modes and recovery, an operations checklist, and common pitfalls. By the end, you will be able to approach pipeline issues with confidence, minimize downtime, and implement robust fixes.

## Version and Environment Inventory

Before making any changes, establish a clear baseline of your GitLab environment. This prevents misdiagnosis and ensures you are working with the correct versions and components. Document all findings in a shared location such as the project wiki or a dedicated operations document.

### 1.1 Gather GitLab Instance Information

Start by identifying your GitLab instance version and edition. This information is available in the admin area or via the API. For self-managed instances, the admin area is at Admin Area > Overview > Dashboard. On GitLab.com, use the API endpoint.

curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/version"

Expected output:

{ "version": "16.7.0-ee", "revision": "abc1234" }

Note whether you are using GitLab.com or a self-managed instance. Self-managed instances allow more control but require you to manage upgrades and runner infrastructure. Also record the instance URL, the authentication method (SSO, LDAP, built-in), and any proxy settings. For example, if your company uses an internal proxy, note its address and port because it will affect network troubleshooting.

### 1.2 Identify GitLab Runner Versions

Runners execute jobs. List all runners and their statuses by running the following command on each runner host:

gitlab-runner list

Example output:

Runtime platform arch=amd64 os=linux pid=1234 revision=abcdef12 version=16.7.0 Listing configured runners ConfigFile=/etc/gitlab-runner/config.toml my-runner Executor=docker Token=xxxxxx URL=https://gitlab.example.com

For each runner, check its executor type (shell, docker, kubernetes, etc.) because troubleshooting steps differ. Use gitlab-runner verify to check registration:

gitlab-runner verify

Expected output:

Runtime platform arch=amd64 os=linux pid=1234 revision=abcdef12 version=16.7.0 Running in system-mode.

Verifying runner... is alive runner=xxxxxx

Note the runner's tags, which determine which jobs it can pick up. For example, a runner with tag docker will only run jobs that specify that tag. Also record the runner's concurrency setting and available resources (CPU, memory, disk) because these affect job performance.

To see all runners registered to your GitLab instance, use the admin API:

curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/runners/all"

This returns a JSON array with each runner's id, description, status, tags, and more. Save this output for reference.

### 1.3 Document Pipeline Topology

Map your pipeline stages and jobs. List the stages defined in .gitlab-ci.yml and identify external dependencies such as artifact repositories, deployment targets, or external services. Create a simple diagram using a tool like Mermaid or draw.io. For each stage, note the following:

- The runner tags required.

- The environment variables or secrets used.

- The external services accessed (e.g., npm registry, Docker registry, AWS S3).

- The expected duration and criticality.

Example topology for a web application:

<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">Stage</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">Jobs</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">Runner Tags</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">External Dependencies</th></tr></thead>
<tbody><tr><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">build</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">install deps, compile</td><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">npm registry, artifact storage</td></tr>
<tr><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">test</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">unit tests, lint</td><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">test database</td></tr>
<tr><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">deploy-stg</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">deploy to staging</td><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">staging server, SSH keys</td></tr>
<tr><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">deploy-prod</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">deploy to production</td><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">production server, approval rules</td></tr></tbody>
</table>
</div>
This inventory helps when tracing failures across stages. For example, if the deploy-stg job fails, you know to check the SSH key and staging server availability.

### 1.4 Review Prerequisites

Ensure the following prerequisites are met:

- GitLab instance reachable from runner machines. Test with ping and curl to the GitLab URL.

- Runner has necessary permissions (e.g., to pull images, access repositories). Check the runner's registration token and any project-specific settings.

- Network connectivity to external services (if used). Test with nc -zv registry.example.com 443 .

- Required secrets or variables configured in GitLab CI/CD settings. List all variables in Project Settings > CI/CD > Variables and verify they are set correctly and not expired.

Document any missing prerequisites and assign an owner to resolve them before proceeding.

## Safe Configuration Path

When modifying pipeline configuration, adopt a scoped, incremental approach to minimize risk and ease rollback. All changes should go through the version control system, and you should have a clear rollback plan before making any change.

### 2.1 Use Feature Flags and Conditional Logic

Introduce changes behind environment variables or rules. For example, to test a new deployment method on a specific branch, use rules in .gitlab-ci.yml :

deploy: script: ./deploy.sh rules:

- if: '$CI_COMMIT_BRANCH == "main" && $ENABLE_NEW_DEPLOY == "true"'

Set ENABLE_NEW_DEPLOY as a variable in the project settings, initially "false". This allows quick rollback by toggling the variable to "false" or removing it. The owner of this decision is the DevOps lead (e.g., Priya Shah, Engineering Lead), who reviews the variable weekly to decide whether to keep the feature or remove it.

### 2.2 Keep Configurations in Version Control

All pipeline configuration should be in .gitlab-ci.yml stored in the repository. Use merge requests for changes and require review. This provides a natural audit trail and rollback via revert. For example, if a change breaks the pipeline, you can revert the merge request with a single click in the GitLab UI or by running git revert <commit-sha> .

### 2.3 Use Includes for Modular Changes

Break large configurations into reusable snippets using include :

include:

- local: '/ci/templates/deploy.yml'

When changing a specific part, only modify that file, reducing impact. Keep the include files in the same repository under a ci/ directory. This also makes it easier to review changes because the diff is limited to one file.

### 2.4 Test Changes in a Safe Environment

For self-managed instances, consider using a staging GitLab instance or a separate project to validate changes before applying to production. For GitLab.com, use a separate branch and limited trigger rules. For example, create a test project that mirrors the production project's CI configuration and use it to test new runner configurations or pipeline changes.

### 2.5 Example: Scoped Change to a Job

Suppose you need to update a Docker image version in a job. Instead of directly editing the job in the main branch, create a feature branch:

build: image: node:16 script: npm run build

Change to:

build: image: node:18 script: npm run build

Push to a branch and observe the pipeline. If it fails, revert the commit and push again. This limits the impact to the feature branch and allows you to test before merging to main. The owner for this change is the developer making it, and the expected review turnaround is within one business day.

## Verification and Diagnostics

After making changes, verify that the pipeline behaves as expected. Use logs and diagnostic commands to confirm. Always compare the new behavior with the documented baseline from Section 1.

### 3.1 Check Pipeline Status and Logs

Use the GitLab UI to view pipeline logs. For command-line access, use the API:

curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/:id/pipelines/:pipeline_id/jobs"

Example output includes job statuses and IDs. To fetch a specific job log:

curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/:id/jobs/:job_id/trace"

The trace is plain text, which you can grep for errors. For example, to find all error lines in a job log, save the trace to a file and run:

grep -i "error" job_trace.log

This helps quickly identify the failing command.

### 3.2 Verify Runner Connectivity

On the runner machine, test connectivity to GitLab:

gitlab-runner --debug run --config /etc/gitlab-runner/config.toml

Look for "Checking for jobs..." and "Received job" messages. If the runner cannot connect, check network firewall and token. Also run a simple curl from the runner host to the GitLab URL:

curl -I https://gitlab.example.com

Expected output: HTTP/1.1 302 Found or 200 OK . If you get a connection timeout, check DNS and firewall rules.

### 3.3 Diagnose Docker-related Failures

For Docker executors, ensure the runner can pull images. Run a simple job with docker pull :

docker pull node:18

Expected output:

18: Pulling from library/node Digest: sha256:... Status: Image is up to date for node:18

If pull fails, check registry authentication and runner configuration. For private registries, ensure the runner has the correct credentials in its config.toml or environment variables. Test authentication manually:

docker login registry.example.com

Then pull again. If the image is large, consider using a local mirror or cache.

### 3.4 Validate Pipeline Syntax

Before committing, validate .gitlab-ci.yml syntax using the GitLab CI Lint tool available in the UI under CI/CD > Pipelines > CI Lint, or via API:

curl --header "Content-Type: application/json" --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/ci/lint" --data '{"content": "job:\n script: echo hello"}'

Expected output includes "status": "valid" or errors. Always lint the full file content, not just a snippet. You can also use the gitlab-ci-lint command line tool if available.

### 3.5 Inspect Runner Logs

Runner logs often contain detailed error messages. Location depends on installation:

- Systemd: journalctl -u gitlab-runner

- Linux package: /var/log/gitlab-runner/

- Docker container: docker logs <container>

Example:

journalctl -u gitlab-runner -n 50

Look for patterns like "ERROR: Job failed: execution took longer than", "ERROR: Preparation failed", etc. These patterns indicate common issues. For example, "execution took longer than" suggests a job timeout, which you can increase in the job definition with timeout: 2h .

## Failure Modes and Recovery

Understanding common failure modes and how to recover from them is crucial for maintaining uptime. The following table summarizes the most frequent failure modes, symptoms, and causes.

### 4.1 Common Failure Modes

<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">Failure Mode</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">Typical Symptoms</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">Common Causes</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 unavailable</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Jobs stuck in pending; &quot;This job is stuck because the project doesn&#39;t have any runners online&quot;</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Runner offline, misconfigured tags, insufficient resources</td></tr>
<tr><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Authentication failure</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">&quot;Authentication failed&quot; when pulling images or accessing repositories</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Expired tokens, incorrect credentials, missing deploy keys</td></tr>
<tr><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Network timeouts</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">&quot;fatal: unable to access &#39;...&#39;: Failed to connect&quot;</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Firewall blocking, DNS issues, proxy misconfiguration</td></tr>
<tr><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Resource exhaustion</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Job killed, out-of-memory errors</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Insufficient runner memory or disk, too many concurrent jobs</td></tr>
<tr><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Script errors</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Non-zero exit code from script</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Missing dependencies, incorrect paths, environment variables not set</td></tr>
<tr><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Artifact upload failure</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">&quot;Uploading artifacts... failed&quot;</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Insufficient permissions, storage quota exceeded, network interruption</td></tr></tbody>
</table>
</div>

### 4.2 Rollback and Recovery Steps

Each failure mode has a specific recovery procedure. Always document the exact commands run and the results in the incident log.

Scenario 1: Runner unavailable

- Check runner status: gitlab-runner status

- If stopped, start it: sudo gitlab-runner start

- If runner is registered but not picking jobs, verify tags match job requirements. Check the job's tags in .gitlab-ci.yml and the runner's tags in config.toml .

- If misconfigured, edit config.toml and restart: sudo gitlab-runner restart .

Example: A job with tag docker is pending because the only online runner has tag shell . Fix by changing the job tag or adding a runner with the correct tag.

Scenario 2: Authentication failure

- For Docker registry, re-authenticate: docker login registry.example.com

- For Git repository access, re-add SSH keys or deploy tokens.

- Update CI/CD variables in project settings. Check for expired tokens and rotate them.

Example: A job fails to pull a private image with "Authentication failed". The fix is to update the DOCKER_AUTH_CONFIG variable with new credentials and re-run.

Scenario 3: Resource exhaustion

- Increase runner resources or reduce concurrent jobs ( concurrent in config.toml ).

- Add cleanup steps to jobs to free disk space, e.g., rm -rf ~/.cache in the script.

- Use gitlab-runner cleanup to remove old build directories.

- Monitor disk usage with df -h and memory with free -m .

Example: A job fails with "out of memory" when running tests. Increase the runner's memory or split the test suite into parallel jobs.

Scenario 4: Script errors

- Re-run job with debug logging: set CI_DEBUG_TRACE=true in project variables to get verbose output.

- Check for missing dependencies and add installation steps. For example, if python is missing, add apt-get install python3 or use a Docker image that includes it.

- Review the script for hardcoded paths and incorrect assumptions about the runner environment.

Scenario 5: Artifact upload failure

- Ensure artifacts: paths are correctly specified. Use absolute paths or paths relative to the build directory.

- Increase artifact size limit if needed (admin setting). Default is 100 MB.

- Check storage permissions. For local storage, ensure the GitLab user has write access to the artifacts directory.

### 4.3 Recovery Verification

After applying fixes, re-run the pipeline and verify all jobs pass. Check logs for expected outputs. Use the checklist below to confirm recovery.

- [ ] Pipeline status is green.

- [ ] All jobs completed within expected time.

- [ ] No error patterns in job logs.

- [ ] Artifacts uploaded correctly and can be downloaded.

- [ ] Deployment (if any) successful and application healthy.

The recovery owner (e.g., DevOps engineer) should document the incident and update the operations runbook with any new findings.

## Operations Checklist

Use this repeatable checklist for ongoing operations and troubleshooting. Assign a single owner for each checklist item and a review frequency. The default owner is the DevOps lead unless otherwise specified.

### Before Changes

- [ ] Document current GitLab and runner versions. Owner: DevOps lead. Frequency: monthly.

- [ ] Note pipeline topology and dependencies. Owner: Project lead. Frequency: quarterly or on major change.

- [ ] Ensure configuration is in version control. Owner: All developers. Frequency: every commit.

- [ ] Identify rollback points (e.g., previous commit, variable toggle). Owner: Developer making change. Frequency: per change.

### During Changes

- [ ] Make scoped changes (one change per merge request). Owner: Developer. Frequency: per change.

- [ ] Use rules to limit impact (e.g., only on specific branches). Owner: DevOps lead. Frequency: per change.

- [ ] Validate .gitlab-ci.yml syntax. Owner: Developer. Frequency: every commit to CI file.

### After Changes

- [ ] Monitor pipeline status. Owner: Developer. Frequency: immediate after change.

- [ ] Check runner logs for errors. Owner: DevOps engineer. Frequency: after each pipeline run or daily.

- [ ] Verify expected outputs in job logs. Owner: Developer. Frequency: after each pipeline run.

- [ ] Test rollback if necessary. Owner: DevOps lead. Frequency: as needed.

### Periodic Review

- [ ] Update runners and GitLab instance regularly. Owner: DevOps lead. Frequency: monthly or as per maintenance window.

- [ ] Review project CI/CD settings and variables. Owner: DevOps lead. Frequency: monthly.

- [ ] Clean up old artifacts and caches. Owner: DevOps engineer. Frequency: weekly.

- [ ] Review pipeline performance and optimize. Owner: DevOps lead. Frequency: quarterly.

## Common Pitfalls and How to Avoid Them

Even experienced teams fall into common traps when troubleshooting GitLab CI/CD. Here are the most frequent pitfalls, why they happen, and how to avoid or recover from them.

### Pitfall 1: Ignoring Runner Tags

Why it happens: Developers assume any runner can pick up a job, but jobs with specific tags only run on runners with matching tags. If a job is stuck in pending, the first check should be runner tags.

How to avoid: Document runner tags in the project README and in the pipeline topology. In .gitlab-ci.yml , use meaningful tags and minimize the number of tags per runner.

How to recover: Run gitlab-runner list to see runner tags and compare with the job's tags. Either change the job tag or add a runner with the correct tag.

### Pitfall 2: Hardcoding Secrets in .gitlab-ci.yml

Why it happens: It is convenient to put passwords or tokens directly in the configuration, but this exposes secrets in the repository and makes rotation difficult.

How to avoid: Use CI/CD variables with masking and protection. Store secrets in Project Settings > CI/CD > Variables and reference them as $VARIABLE_NAME . For sensitive files, use file-type variables.

How to recover: If a secret was leaked, immediately revoke it and rotate. Then remove the hardcoded value and replace with a variable. Scan the repository history for leaks and force-push a cleaned history if necessary.

### Pitfall 3: Not Using Caching Effectively

Why it happens: Teams often overlook caching, leading to slow pipelines and unnecessary load on external dependencies. Without caching, every job re-downloads dependencies.

How to avoid: Use cache and artifacts appropriately. Cache dependencies like npm modules or Maven repositories. Example:

cache: paths:

- node_modules/

How to recover: If pipeline is slow, check job logs for repeated dependency downloads. Add cache sections to the relevant jobs and re-run.

### Pitfall 4: Overlooking Runner Resource Limits

Why it happens: Runners may have insufficient memory or disk for build tasks, causing flaky failures. This is common when parallel jobs increase resource usage.

How to avoid: Monitor runner host metrics with tools like Prometheus or htop . Set appropriate concurrent and limit in config.toml . Use Docker resource limits if using Docker executor.

How to recover: Reduce concurrency, add more runners, or scale up the runner host. For Docker executor, set memory and cpus in the runner config.

### Pitfall 5: Neglecting Runner Updates

Why it happens: Teams often run outdated runners, which can lead to compatibility issues with newer GitLab features and security vulnerabilities.

How to avoid: Subscribe to GitLab release notes and schedule monthly runner updates. Use the same version for GitLab and runners when possible.

How to recover: Update the runner using the package manager: sudo apt-get update && sudo apt-get install gitlab-runner (or equivalent). Verify with gitlab-runner --version .

### Pitfall 6: Failing to Plan Rollbacks

Why it happens: Changes are made hastily without considering how to revert if something goes wrong, leading to extended downtime.

How to avoid: Always have a rollback plan before making changes. Use feature flags, version control, and incremental changes as described in Section 2. Keep previous working versions of config.toml and .gitlab-ci.yml .

How to recover: If a change breaks the pipeline, immediately revert the commit or toggle the feature flag. Document the rollback procedure in the runbook.

## Conclusion

Troubleshooting GitLab CI/CD pipelines involves a systematic approach: gather environment information, make safe configuration changes, verify with observable checks, and know how to recover from failures. By following the practical commands and checklists in this guide, you can reduce downtime and maintain robust CI/CD processes. Start by applying these techniques to your next pipeline failure, and gradually build a library of known issues and solutions for your team. Remember to keep configurations version-controlled and changes incremental for easier rollback. With practice, you will turn troubleshooting from a reactive scramble into a proactive, manageable routine. Assign clear owners for each checklist item and review the operations checklist regularly to ensure continuous improvement.