Intro
GitLab CI/CD is powerful, but small mistakes can stall delivery. This guide focuses on the errors you are most likely to see in real pipelines, why they happen, how to confirm root cause, and safe, copy-pasteable fixes. Keep it practical: reproduce the issue with the smallest example, apply the smallest safe change, then generalize.
What you will get:
- Clear explanations of common GitLab CI/CD error messages
- Minimal repros and job snippets you can adapt
- Safe-by-default fixes you can test locally before rollout
Workflow Overview
Use a repeatable flow that turns noise into signal:
- Reproduce and capture context
- Re-run the failed job with the same commit.
- Expand all collapsed sections in the job log. Read from the top: runner info, image pull, checkout, before_script, script, after_script, artifact/upload.
- Note the failing line and the phase it occurred in.
- Confirm the smallest repro
- Comment out non-essential steps.
- Hard-code variables that affect flow.
- If a service is involved (Docker, database, Kubernetes), test with a single job and a tiny script.
- Apply the smallest safe fix
- Prefer configuration fixes over scripting workarounds.
- Add guardrails (rules, paths, or explicit timeouts) to prevent recurrences.
- Verify and document
- Re-run the job, then a full pipeline.
- Add a short comment in .gitlab-ci.yml near the fix.
Common errors and fixes
Below are frequent failures, their causes, how to check, and safe fixes.
1) Invalid .gitlab-ci.yml or YAML structure
Symptoms
- Pipeline does not start, or you see: "This GitLab CI configuration is invalid".
- Job-level messages like: "before_script config should be a string or a list of strings".
Why it happens
- YAML indentation or list syntax errors.
- Using a map where a list is expected, or vice versa.
How to diagnose
- Reduce to a minimal .gitlab-ci.yml with a single echo job to isolate the fault.
- Check that all lists use dashes and all string lists are simple strings.
Safe fix example
stages: [lint, test]
variables:
NODE_ENV: 'test'
lint:
stage: lint
image: node:20-alpine
script:
- node --version
- npm ci
- npm run lint
test:
stage: test
image: node:20-alpine
script:
- npm ci
- npm test
Tips
- Avoid mixing tabs and spaces.
- Keep script as a list of strings, not a single multiline string.
2) Pipeline has no jobs
Symptoms
- Pipeline page shows: "There are no stages/jobs for this pipeline." or nothing runs for your branch/MR.
Why it happens
- rules or only/except exclude your ref.
- workflow: rules prevents pipeline creation.
How to diagnose
- Inspect the branch or MR event and environment variables.
- Temporarily add a catch-all job to verify matching.
Safe fix example using rules with a fallback
workflow:
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
- if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
- when: always # ensure a pipeline is created when in doubt
build:
stage: build
image: alpine:3.20
rules:
- changes:
- src/**
when: on_success
- when: manual # visible fallback if no changes matched
script:
- echo "Building..."
3) Job stuck or pending: no runners
Symptoms
- Job status is stuck in pending with: "This job is stuck because the project does not have any runners online assigned to it."
Why it happens
- No runner online, or runner tags do not match the job.
- Runner is not allowed to run on this project or protected branch.
How to diagnose
- Check job tags vs runner tags in Settings > CI/CD > Runners.
- Confirm runner is online and not paused.
Safe fix example: align tags or remove tag requirement
# If your runner has tag 'docker', the job must include it
build-image:
tags: ['docker']
image: docker:24
services: ['docker:24-dind']
variables:
DOCKER_HOST: 'tcp://docker:2375'
DOCKER_TLS_CERTDIR: ''
script:
- docker version
- docker build -t demo: ci .
Tips
- For protected branches, ensure the runner is marked as protected or the job is not restricted to protected refs.
4) Docker-in-Docker: cannot connect to Docker daemon
Symptoms
- "Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?"
Why it happens
- Docker service not started, wrong host, or TLS misconfigured.
How to diagnose
- Print env and try docker info.
- Ensure the service alias matches the DOCKER_HOST.
Safe fix example
image: docker:24
services:
- name: docker:24-dind
command: ['--mtu=1460'] # optional, resolves some network issues
variables:
DOCKER_HOST: 'tcp://docker:2375'
DOCKER_TLS_CERTDIR: ''
DOCKER_DRIVER: 'overlay2'
build:
stage: build
script:
- docker version
- docker build -t $CI_REGISTRY_IMAGE:ci-$CI_COMMIT_SHORT_SHA .
- echo 'Build complete'
Tips
- Disable TLS for the dind service inside CI and keep the job network isolated.
- If you use a shared runner, prefer small base images to reduce pull time.
5) Artifacts upload: file not found
Symptoms
- "Uploading artifacts... WARNING: No files to upload" or "Could not find a file matching ..."
Why it happens
- Path is wrong, generated files go to a different directory, or the job failed before producing them.
How to diagnose
- Add ls -R to confirm paths.
- Ensure the working directory is $CI_PROJECT_DIR.
Safe fix example
test:
stage: test
image: node:20-alpine
script:
- npm ci
- npm test -- --reporter=junit --reporter-options output=reports/junit.xml
- mkdir -p dist && echo ok > dist/health.txt
artifacts:
when: always
paths:
- reports/junit.xml
- dist/
expire_in: '7 days'
6) Cache does not persist between jobs
Symptoms
- Dependencies re-install every time; cache hit rate appears 0%.
Why it happens
- Cache key too broad, too narrow, or changes every run.
How to diagnose
- Print cache key and compare across jobs.
Safe fix example: content-based key with fallback
cache:
key:
files:
- package-lock.json
paths:
- node_modules/
policy: pull-push
install:
stage: .pre
image: node:20-alpine
script:
- npm ci --cache .npm --prefer-offline
artifacts:
paths: [.npm]
expire_in: '1 day'
7) Git submodules fail to fetch
Symptoms
- "fatal: could not read Username for 'https://gitlab.com': No such device or address"
- "fatal: repository not found" for submodules.
Why it happens
- Submodules use HTTPS without credentials, or point to a private project the token cannot access.
How to diagnose
- Print .gitmodules and verify URLs.
- Check that submodule projects grant access to CI_JOB_TOKEN if using relative paths.
Safe fix example: use CI_JOB_TOKEN for HTTPS
variables:
GIT_SUBMODULE_STRATEGY: 'recursive'
GIT_SUBMODULE_UPDATE_FLAGS: '--jobs 4'
before_script:
- git config --global url."https://gitlab-ci-token:${CI_JOB_TOKEN}@gitlab.com/".insteadOf "https://gitlab.com/"
submodule_job:
stage: test
image: alpine:3.20
script:
- git submodule sync --recursive
- git submodule update --init --recursive
- echo 'Submodules ready'
Tips
- Prefer relative submodule URLs within the same GitLab instance to allow CI_JOB_TOKEN auth.
8) Protected variables missing in non-protected branches
Symptoms
- docker login or cloud CLI fails with 403 or auth errors only on feature branches.
Why it happens
- Variables are marked protected and only exposed on protected refs.
How to diagnose
- Compare variable protection flags with the branch protection.
Safe fix example: restrict the job to protected refs when needed
docker_push:
stage: deploy
rules:
- if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
- if: '$CI_COMMIT_TAG'
- when: never
script:
- echo "$REGISTRY_PASSWORD" | docker login -u "$REGISTRY_USER" --password-stdin "$CI_REGISTRY"
- docker push $CI_REGISTRY_IMAGE:release
9) Kubernetes: kubectl or Helm errors
Symptoms
- kubectl: "unable to recognize: no matches for kind ..."
- Helm: "UPGRADE FAILED: another operation is in progress" or timeouts.
Why it happens
- Wrong apiVersion or missing CRDs; missing permissions; stale Helm locks; kubeconfig not loaded.
How to diagnose
- Print kubectl version --client and server versions.
- kubectl auth can-i ... to check permissions.
- helm list and helm history to inspect release state.
Safe fix example: pass kubeconfig via a variable and use atomic Helm upgrades
deploy:
stage: deploy
image: alpine/helm:3.14.4
variables:
KUBECONFIG: '/builds/$CI_PROJECT_PATH/.kube/config'
before_script:
- mkdir -p $(dirname "$KUBECONFIG")
- echo "$KUBECONFIG_DATA" | base64 -d > "$KUBECONFIG"
- kubectl version --client
- kubectl config get-contexts
script:
- kubectl apply -f k8s/ns.yaml
- helm upgrade --install app charts/app \
--namespace app \
--create-namespace \
--atomic --wait --timeout 5m
Tips
- Keep kubectl/Helm client versions compatible with your cluster.
- Use --atomic and --wait to fail fast and roll back on errors.
10) Job exceeded time limit
Symptoms
- "script exceeded time limit" or job canceled after a fixed duration.
Why it happens
- Default timeout too low; slow image pulls; cache misses; long test suites.
How to diagnose
- Check job duration breakdown in logs.
- Note image pull times and network-related waits.
Safe fix example: set a per-job timeout and optimize pulls
long_tests:
stage: test
image: python:3.12-slim
timeout: 45m
script:
- pip install -r requirements.txt
- pytest -m 'not slow' -n auto --dist loadscope
Tips
- Use smaller base images and enable registry-side caching where available.
- Split long jobs into parallel shards and merge reports as artifacts.
Local Pilot Plan
Goal: validate your fixes and patterns with the smallest useful pipeline you can inspect locally before rolling out to all projects.
Scope
- One repo, one shared or project-specific runner.
- Stages: validate, test, build, package.
- Docker-in-Docker build, basic caching, and one artifact.
Success criteria
- Pipeline passes in under 10 minutes on a clean runner.
- Cache hit on second run for dependencies.
- No pending jobs; no artifact upload warnings.
Reference pilot pipeline
stages: [validate, test, build, package]
default:
image: docker:24
services: ['docker:24-dind']
variables:
DOCKER_HOST: 'tcp://docker:2375'
DOCKER_TLS_CERTDIR: ''
validate: yaml:
stage: validate
image: alpine:3.20
script:
- echo 'Validating YAML layout'
- grep -q 'stages:' .gitlab-ci.yml
unit_tests:
stage: test
image: node:20-alpine
cache:
key:
files: [package-lock.json]
paths: [node_modules/]
policy: pull-push
script:
- npm ci
- npm test -- --reporter=junit --reporter-options output=reports/junit.xml
artifacts:
when: always
paths: [reports/junit.xml]
expire_in: '7 days'
build_image:
stage: build
tags: ['docker'] # ensure this matches your runner tag
script:
- echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" --password-stdin "$CI_REGISTRY"
- docker build -t $CI_REGISTRY_IMAGE:ci-$CI_COMMIT_SHORT_SHA .
- docker image ls | head -n 5
package_sbom:
stage: package
image: alpine:3.20
script:
- mkdir -p out && echo '{"sbom":"demo"}' > out/sbom.json
artifacts:
paths: [out/sbom.json]
expire_in: '7 days'
How to run and inspect
- Push a feature branch and open a small MR.
- Confirm runner tags match and that the cache hits on the second run.
- Intentionally break a path (e.g., artifacts) to see the error, then revert.
Rollout
- Copy the patterns into other repos, adjusting images, tags, and cache keys.
- Keep the pilot jobs as templates you can include across projects.
Conclusion
You now have a practical set of patterns to recognize and fix the most common GitLab CI/CD errors:
- Validate YAML early and keep scripts simple.
- Ensure runner availability and tag alignment.
- Configure Docker-in-Docker predictably.
- Produce artifacts reliably and cache what is expensive to rebuild.
- Authenticate submodules and registries correctly.
- Deploy to Kubernetes with explicit kubeconfig, waits, and atomic rollbacks.
- Control timeouts and parallelize long tasks.
Next steps
- Implement the pilot pipeline and record baseline metrics.
- Fix the most painful error in your environment first, then expand.
- Keep changes small, observable, and easy to revert.