GitLab CI/CD Cheatsheet
Installation
GitLab Runner Installation
| Platform | Command |
|---|
| Ubuntu/Debian | `curl -L “https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.deb.sh” \ |
| CentOS/RHEL | `curl -L “https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.rpm.sh” \ |
| macOS | sudo curl --output /usr/local/bin/gitlab-runner "https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-darwin-amd64" && sudo chmod +x /usr/local/bin/gitlab-runner |
| Windows | Invoke-WebRequest -Uri "https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-windows-amd64.exe" -OutFile "C:\GitLab-Runner\gitlab-runner.exe" |
| Docker | docker run -d --name gitlab-runner --restart always -v /srv/gitlab-runner/config:/etc/gitlab-runner -v /var/run/docker.sock:/var/run/docker.sock gitlab/gitlab-runner:latest |
| Kubernetes (Helm) | helm repo add gitlab https://charts.gitlab.io && helm install gitlab-runner gitlab/gitlab-runner --set gitlabUrl=https://gitlab.com/ |
Verification
| Command | Description |
|---|
gitlab-runner --version | Check installed GitLab Runner version |
gitlab-runner verify | Verify runner can connect to GitLab |
Basic Commands
Runner Management
| Command | Description |
|---|
gitlab-runner register | Register a new runner interactively |
gitlab-runner list | List all registered runners |
gitlab-runner start | Start the runner service |
gitlab-runner stop | Stop the runner service |
gitlab-runner restart | Restart the runner service |
gitlab-runner status | Check current runner status |
gitlab-runner unregister --url <URL> --token <TOKEN> | Unregister a specific runner |
gitlab-runner unregister --all-runners | Unregister all runners |
gitlab-runner run | Run runner in foreground for debugging |
gitlab-runner exec docker <job_name> | Execute a single job locally for testing |
Pipeline Operations (GitLab CLI)
| Command | Description |
|---|
glab ci lint | Validate .gitlab-ci.yml syntax locally |
glab ci view | View CI/CD configuration for current project |
glab ci trace <job-id> | View real-time logs for a specific job |
glab ci list | List all pipelines for current project |
glab ci retry <pipeline-id> | Retry a failed pipeline |
glab ci cancel <pipeline-id> | Cancel a running pipeline |
API-Based Pipeline Triggers
| Command | Description |
|---|
curl -X POST -F token=<TOKEN> -F ref=main https://gitlab.com/api/v4/projects/<ID>/trigger/pipeline | Trigger pipeline via API |
curl -X POST -F token=<TOKEN> -F ref=main -F "variables[KEY]=value" <URL> | Trigger pipeline with variables |
curl --header "PRIVATE-TOKEN: <token>" "https://gitlab.com/api/v4/projects/<ID>/pipelines/<PID>" | Get pipeline status via API |
Advanced Usage
Advanced Runner Registration
| Command | Description |
|---|
gitlab-runner register --non-interactive --url <URL> --registration-token <TOKEN> --executor docker --docker-image alpine:latest | Register runner with inline parameters |
gitlab-runner register --executor docker --docker-privileged --docker-volumes /var/run/docker.sock:/var/run/docker.sock | Register Docker-in-Docker (DinD) runner |
gitlab-runner register --cache-type s3 --cache-s3-bucket-name <bucket> --cache-s3-bucket-location us-east-1 | Configure runner with S3 cache |
gitlab-runner register --executor kubernetes --kubernetes-namespace gitlab-runner | Register Kubernetes executor |
gitlab-runner register --docker-network-mode <network> | Configure runner with specific Docker network |
gitlab-runner register --tag-list "docker,aws" --run-untagged=true | Register runner with specific tags |
Advanced Pipeline Operations
| Command | Description |
|---|
curl --header "PRIVATE-TOKEN: <token>" "https://gitlab.com/api/v4/projects/<ID>/jobs/<JID>/artifacts" -o artifacts.zip | Download job artifacts via API |
curl --request POST --header "PRIVATE-TOKEN: <token>" --form "description=Nightly" --form "ref=main" --form "cron=0 2 * * *" <URL> | Create pipeline schedule via API |
curl --request POST --form "token=$CI_JOB_TOKEN" --form "ref=main" <DOWNSTREAM_URL> | Trigger multi-project pipeline |
gitlab-runner exec docker --docker-image alpine:latest <job_name> | Test runner executor locally |
Monitoring and Debugging
| Command | Description |
|---|
sudo journalctl -u gitlab-runner -f | View runner logs in real-time (Linux) |
gitlab-runner --debug run | Run runner with verbose debug logging |
curl http://localhost:9252/metrics | Access runner Prometheus metrics |
docker logs -f gitlab-runner | View Docker runner logs |
kubectl logs -f -n gitlab-runner <pod-name> | View Kubernetes runner logs |
Configuration Management
| Command | Description |
|---|
sudo nano /etc/gitlab-runner/config.toml | Edit runner configuration file (Linux) |
gitlab-runner verify --delete | Remove invalid runners from config |
gitlab-runner run-single --url <URL> --token <TOKEN> | Run a single job without registration |
Configuration
Basic .gitlab-ci.yml Structure
# Define pipeline stages
stages:
- build
- test
- deploy
# Global variables
variables:
DOCKER_DRIVER: overlay2
DATABASE_URL: "postgres://localhost/db"
# Global scripts executed before each job
before_script:
- echo "Pipeline started at $(date)"
- export PATH=$PATH:/custom/bin
# Global scripts executed after each job
after_script:
- echo "Cleaning up..."
# Basic job definition
build_app:
stage: build
image: node:16-alpine
script:
- npm install
- npm run build
artifacts:
paths:
- dist/
expire_in: 1 week
cache:
key: ${CI_COMMIT_REF_SLUG}
paths:
- node_modules/
only:
- main
- merge_requests
tags:
- docker
Advanced Pipeline Configuration
# Include external configurations
include:
- project: 'my-group/ci-templates'
ref: main
file: '/templates/.gitlab-ci-template.yml'
- remote: 'https://example.com/ci-template.yml'
- local: '/templates/security-scan.yml'
- template: Security/SAST.gitlab-ci.yml
# Workflow rules for pipeline execution
workflow:
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
- if: '$CI_COMMIT_BRANCH == "main"'
- if: '$CI_COMMIT_TAG'
- when: never
# Job with complex rules
deploy_production:
stage: deploy
script:
- ./deploy.sh production
rules:
- if: '$CI_COMMIT_BRANCH == "main"'
when: manual
- if: '$CI_COMMIT_TAG =~ /^v[0-9]+\.[0-9]+\.[0-9]+$/'
when: on_success
environment:
name: production
url: https://prod.example.com
on_stop: stop_production
Parallel and Matrix Jobs
# Parallel execution
test:
stage: test
parallel: 5
script:
- bundle exec rspec
# Matrix builds
test_matrix:
parallel:
matrix:
- NODE_VERSION: ['14', '16', '18']
OS: ['linux', 'windows']
image: node:${NODE_VERSION}
script:
- npm test
Artifacts and Cache Configuration
build:
stage: build
script:
- make build
artifacts:
name: "$CI_JOB_NAME-$CI_COMMIT_REF_NAME"
paths:
- binaries/
- build/
exclude:
- binaries/**/*.tmp
reports:
junit: test-results.xml
coverage_report:
coverage_format: cobertura
path: coverage/cobertura-coverage.xml
expire_in: 30 days
when: on_success
cache:
key:
files:
- package-lock.json
paths:
- node_modules/
policy: pull-push
Docker and Services Configuration
build_docker:
stage: build
image: docker:latest
services:
- docker:dind
variables:
DOCKER_TLS_CERTDIR: "/certs"
before_script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
script:
- docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA .
- docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
Dynamic Child Pipelines
generate_config:
stage: build
script:
- ./generate-ci-config.sh > generated-config.yml
artifacts:
paths:
- generated-config.yml
trigger_child:
stage: deploy
trigger:
include:
- artifact: generated-config.yml
job: generate_config
strategy: depend
Runner Configuration File (config.toml)
concurrent = 10
check_interval = 0
[session_server]
session_timeout = 1800
[[runners]]
name = "docker-runner"
url = "https://gitlab.com/"
token = "TOKEN"
executor = "docker"
[runners.custom_build_dir]
[runners.cache]
Type = "s3"
Path = "cache"
Shared = true
[runners.cache.s3]
ServerAddress = "s3.amazonaws.com"
BucketName = "runner-cache"
BucketLocation = "us-east-1"
[runners.docker]
tls_verify = false
image = "alpine:latest"
privileged = true
disable_entrypoint_overwrite = false
oom_kill_disable = false
disable_cache = false
volumes = ["/cache", "/var/run/docker.sock:/var/run/docker.sock"]
shm_size = 0
Common Use Cases
Use Case 1: Build and Test Node.js Application
stages:
- build
- test
- deploy
variables:
NODE_ENV: production
build:
stage: build
image: node:16-alpine
script:
- npm ci
- npm run build
artifacts:
paths:
- dist/
- node_modules/
expire_in: 1 hour
cache:
key: ${CI_COMMIT_REF_SLUG}
paths:
- .npm/
test:unit:
stage: test
image: node:16-alpine
dependencies:
- build
script:
- npm run test:unit
coverage: '/Lines\s*:\s*(\d+\.\d+)%/'
artifacts:
reports:
junit: junit.xml
coverage_report:
coverage_format: cobertura
path: coverage/cobertura-coverage.xml
test:integration:
stage: test
image: node:16-alpine
services:
- postgres:13
variables:
POSTGRES_DB: test_db
POSTGRES_USER: test_user
POSTGRES_PASSWORD: test_password
script:
- npm run test:integration
Use Case 2: Docker Build and Push to Registry
stages:
- build
- scan
- deploy
variables:
DOCKER_DRIVER: overlay2
IMAGE_TAG: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
build_image:
stage: build
image: docker:latest
services:
- docker:dind
before_script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
script:
- docker build -t $IMAGE_TAG .
- docker tag $IMAGE_TAG $CI_REGISTRY_IMAGE:latest
- docker push $IMAGE_TAG
- docker push $CI_REGISTRY_IMAGE:latest
only:
- main
- tags
scan_image:
stage: scan
image: aquasec/trivy:latest
script:
- trivy image --severity HIGH,CRITICAL $IMAGE_TAG
allow_failure: true
deploy_k8s:
stage: deploy
image: bitnami/kubectl:latest
script:
- kubectl config use-context $KUBE_CONTEXT
- kubectl set image deployment/myapp myapp=$IMAGE_TAG
- kubectl rollout status deployment/myapp
environment:
name: production
url: https://myapp.example.com
only:
- main
Use Case 3: Multi-Environment Deployment with Manual Approval
stages:
- build
- deploy_staging
- deploy_production
build:
stage: build
script:
- ./build.sh
artifacts:
paths:
- build/
deploy_staging:
stage: deploy_staging
script:
- ./deploy.sh staging
environment:
name: staging
url: https://staging.example.com
on_stop: stop_staging
only:
- main
stop_staging:
stage: deploy_staging
script:
- ./cleanup.sh staging
environment:
name: staging
action: stop
when: manual
deploy_production:
stage: deploy_production
script:
- ./deploy.sh production
environment:
name: production
url: https://www.example.com
when: manual
only:
- main
needs:
- deploy_staging
stages:
- validate
- plan
- apply
variables:
TF_ROOT: ${CI_PROJECT_DIR}/terraform
TF_STATE_NAME: default
.terraform:
image: hashicorp/terraform:latest
before_script:
- cd ${TF_ROOT}
- terraform init -backend-config="address=${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/terraform/state/${TF_STATE_NAME}"
validate:
extends: .terraform
stage: validate
script:
- terraform validate
- terraform fmt -check
plan:
extends: .terraform
stage: plan
script:
- terraform plan -out=plan.tfplan
artifacts:
paths:
- ${TF_ROOT}/plan.tfplan
expire_in: 1 day
apply:
extends: .terraform
stage: apply
script:
- terraform apply -auto-approve plan.tfplan
dependencies:
- plan
when: manual
only:
- main
environment:
name: production
Use Case 5: Monorepo with Selective Job Execution
workflow:
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
- if: '$CI_COMMIT_BRANCH == "main"'
variables:
FRONTEND_PATH: "apps/frontend"
BACKEND_PATH: "apps/backend"
.changes_frontend: &changes_frontend
changes:
- "${FRONTEND_PATH}/**/*"
- "package.json"
- ".gitlab-ci.yml"
.changes_backend: &changes_backend
changes:
- "${BACKEND_PATH}/**/*"
- "requirements.txt"
- ".gitlab-ci.yml"
build_frontend:
stage: build
image: node:16
script:
- cd $FRONTEND_PATH
- npm ci
- npm run build
rules:
- <<: *changes_frontend
build_backend:
stage: build
image: python:3.9
script:
- cd $BACKEND_PATH
- pip install -r requirements.txt
- python -m pytest
rules:
- <<: *changes_backend
deploy_all:
stage: deploy
script:
- ./deploy-all.sh
rules:
- if: '$CI_COMMIT_BRANCH == "main"'
changes:
- "${FRONTEND_PATH}/**/*"
- "${BACKEND_PATH}/**/*"
Best Practices
-
Use cache for dependencies and artifacts for build outputs: Cache speeds up subsequent pipeline runs by storing dependencies like node_modules/, while artifacts pass build outputs between stages. Never cache build artifacts.
-
Implement proper workflow rules to avoid unnecessary pipeline runs: Use workflow:rules to control when pipelines execute, preventing waste on draft MRs or documentation-only changes. This saves runner resources and reduces costs.
-
Tag runners and jobs appropriately: Use specific tags (docker, kubernetes, gpu) to route jobs to appropriate runners. This ensures jobs run on infrastructure with required capabilities and prevents resource contention.
-
Use needs keyword for DAG pipelines: Instead of sequential stages, use needs: to create directed acyclic graphs (DAGs) that run jobs as soon as dependencies complete, significantly reducing total pipeline time.
-
Store sensitive data in CI/CD variables, never in code: Use protected and masked variables for secrets like API keys, passwords, and tokens. Enable protection to restrict access to protected branches/tags only.
-
Implement security scanning early in the pipeline: Include SAST, dependency scanning, and container scanning in early stages. Use allow_failure: true initially to avoid blocking development while teams address findings.
-
Use only:changes or rules:changes for monorepos: Trigger jobs only when relevant files change, preventing unnecessary builds and tests. This is critical for large monorepos with multiple applications.
-
Set appropriate artifact expiration times: Default artifacts to short expiration (1-7 days) to save storage costs. Use expire_in: never only for release artifacts that need permanent retention.
-
Leverage includes and templates for DRY configuration: Create reusable templates in separate repositories and include them using include:project or include:remote. This ensures consistency across projects.
-
Monitor runner capacity and scale appropriately: Track runner queue times and job wait times. Configure concurrent setting in config.toml based on available resources, and scale runners horizontally during peak times.
Troubleshooting
| Issue | Solution |
|---|
| Runner not picking up jobs | Verify runner is active: gitlab-runner verify. Check runner tags match job tags. Ensure runner isn’t paused in GitLab UI. Check concurrent setting in /etc/gitlab-runner/config.toml. |
| ”This job is stuck” error | No runner available with matching tags. Either add tags to job, register runner with those tags, or enable run_untagged: true in runner config. |
| Docker-in-Docker (DinD) permission errors | Add privileged: true to runner config or use Docker socket binding: --docker-volumes /var/run/docker.sock:/var/run/docker.sock. Ensure runner has proper permissions. |
| Cache not working between jobs | Verify cache key is consistent: use ${CI_COMMIT_REF_SLUG} or file-based keys. Check cache storage configuration (S3, GCS, etc.). Ensure cache:policy is set correctly (pull-push for read/write). |
| Pipeline fails with “yaml invalid” | Validate syntax with glab ci lint or GitLab’s CI Lint tool (CI/CD > Pipelines > CI Lint). Check indentation (use spaces, not tabs). Verify all required fields are present. |
| Artifacts not available in downstream jobs | Use dependencies: or needs: to explicitly declare artifact dependencies. Check artifact paths are correct. Verify artifacts haven’t expired (expire_in). |
| Jobs timing out | Increase timeout in job definition: timeout: 3h. Check for hanging processes. Review runner’s concurrent setting if system resources are exhausted. |
| ”Cannot connect to Docker daemon” error | Ensure Docker service is running on runner host. For Docker executor, add -v /var/run/docker.sock:/var/run/docker.sock. For DinD, use docker:dind service. |
| Kubernetes runner pods failing to start | Check namespace exists and runner has permissions. Verify resource requests/limits. Review pod logs: kubectl logs -n gitlab-runner <pod-name>. Check image pull secrets for private registries. |
| Variables not being passed to jobs | Check variable scope (project, group, instance). Ensure variables aren’t masked when trying to print them. For protected variables, job must run on protected branch/tag. Use $ prefix: $VARIABLE_NAME. |
| Runner registration token invalid | Token may have expired or been revoked. Get new token from GitLab UI: Settings > CI/CD > Runners. For project runners, use project-specific token. For group/instance runners, use appropriate token. |
| High runner CPU/memory usage | Reduce concurrent value in config.toml. Implement job resource limits. Use cache to reduce redundant downloads. Consider distributing load across multiple runners. |
| SSL certificate verification failures | Add tls_verify = false to runner config (not recommended for production). Install proper CA certificates. Use CI_SERVER_TLS_CA_FILE variable to specify CA bundle. |
Important CI/CD Variables
| Variable | Description |
|---|
CI_COMMIT_SHA | Full commit SHA that triggered the pipeline |
CI_COMMIT_SHORT_SHA | First 8 characters of commit SHA |
CI_COMMIT_REF_NAME | Branch or tag name |
CI_COMMIT_REF_SLUG | Lowercase branch/tag name, suitable for URLs |
CI_PROJECT_ID | Unique project ID in GitLab |
CI_PROJECT_NAME | Project name |
CI_PROJECT_PATH | Project namespace with project name |
CI_PIPELINE_ID | Unique pipeline ID |
CI_JOB_ID | Unique job ID |
CI_JOB_TOKEN | Token for authenticating with GitLab API |
CI_REGISTRY | GitLab Container Registry address |
CI_REGISTRY_IMAGE | Full image path for project’s container registry |
CI_REGISTRY_USER | Username for container registry authentication |
CI_REGISTRY_PASSWORD | Password for container registry authentication |