Comprehensive guide to GitLab's integrated continuous integration and deployment platform.
GitLab CI/CD
Comprehensive guide to GitLab's integrated continuous integration and deployment platform.
Overview
GitLab CI/CD is a built-in tool for automating software development workflows through pipelines defined in .gitlab-ci.yml. It provides runners for executing jobs, advanced caching mechanisms, environment management, and integrated security scanning. GitLab CI/CD supports complex deployment strategies including blue-green, canary, and progressive delivery patterns.
flowchart LR
A[Code Push] --> B[Pipeline Triggered]
B --> C[Build Stage]
C --> D[Test Stage]
D --> E[Security Scan]
E --> F{Quality Gates}
F -->|Pass| G[Deploy Stage]
F -->|Fail| H[Pipeline Failed]
G --> I[Production]
# Include external configurationsinclude:-project:'my-group/my-project'ref:mainfile:'/templates/.gitlab-ci.yml'-remote:'https://gitlab.com/example/template.yml'-template:Security/SAST.gitlab-ci.yml# Anchors and extends for DRY configuration.deploy-template:&deploy-templateimage:alpine:latestbefore_script:-apk add --no-cache curlretry:max:2when:runner_system_failuredeploy-staging:<<:*deploy-templatestage:deployenvironment:stagingscript:-curl -X POST $WEBHOOK_URL# Using extends (modern approach).base-deploy:image:alpine:latestbefore_script:-apk add --no-cache curldeploy-prod:extends:.base-deployscript:-./deploy.sh production
workflow:rules:# Don't create pipelines for branch pushes if MR exists-if:'$CI_PIPELINE_SOURCE=="merge_request_event"'-if:'$CI_COMMIT_BRANCH&&$CI_OPEN_MERGE_REQUESTS'when:never-if:'$CI_COMMIT_BRANCH'# Always run for tags-if:'$CI_COMMIT_TAG'# Pipeline-wide variables based on conditionsvariables:DEPLOY_ENV:value:"staging"description:"Deploymentenvironment"# Per-stage variablesdeploy:variables:KUBERNETES_NAMESPACE:productionscript:kubectl apply -f manifest.yaml
Runners and Executors
Runner Architecture
graph TB
subgraph "GitLab Instance"
A[GitLab Server]
B[Job Queue]
end
subgraph "Runner Manager"
C[GitLab Runner]
D[Executor]
end
subgraph "Execution Environments"
E[Shell Executor]
F[Docker Executor]
G[Kubernetes Executor]
H[Docker Machine]
end
A --> B
B --> C
C --> D
D --> E
D --> F
D --> G
D --> H
# Job with specific runner tagsbuild-docker:tags:-docker-linux-high-cpuscript:-docker build -t myapp .# Runner configuration in jobtest:tags:-kubernetesimage:name:python:3.11entrypoint:[""]services:-name:postgres:14alias:postgresscript:-pytest
flowchart TD
A[Job Starts] --> B{Cache Exists?}
B -->|Yes| C[Download Cache]
B -->|No| D[Skip Download]
C --> E[Run Job]
D --> E
E --> F[Job Completes]
F --> G{Cache Key Changed?}
G -->|Yes| H[Upload New Cache]
G -->|No| I[Skip Upload]
H --> J[Job Done]
I --> J
Cache Configuration
# Global cache configurationcache:key:${CI_COMMIT_REF_SLUG}paths:-node_modules/-.npm/policy:pull-push# Job-specific cachebuild:cache:key:files:-package-lock.jsonprefix:npmpaths:-node_modules/policy:pull-pushscript:-npm install-npm run build# Cache only (don't update)test:cache:key:files:-package-lock.jsonprefix:npmpaths:-node_modules/policy:pullscript:-npm test# Multiple cachesbuild-java:cache:-key:maven-${CI_COMMIT_REF_SLUG}paths:-.m2/repository/-key:gradle-${CI_COMMIT_REF_SLUG}paths:-.gradle/script:-mvn package
Artefacts Management
# Basic artefact configurationbuild:script:-make buildartifacts:name:"$CI_JOB_NAME-$CI_COMMIT_REF_SLUG"paths:-build/-dist/*.jarexclude:-build/**/*.logexpire_in:1 weekwhen:on_success# Advanced artefact handlingtest:script:-npm testartifacts:paths:-coverage/reports:coverage_report:coverage_format:coberturapath:coverage/cobertura-coverage.xmljunit:test-results/junit.xmlexpose_as:'TestCoverage'expire_in:30 days# Conditional artefactsdeploy:script:-./deploy.shartifacts:paths:-logs/when:on_failureexpire_in:1 week# Download artefacts from previous jobsintegration-test:script:-./integration-test.shdependencies:-build-test# Or use needs for DAGneeds:-job:buildartifacts:true# Dotenv artefacts for variable passingbuild-vars:script:-echo "BUILD_VERSION=1.2.3" >> build.env-echo "DOCKER_TAG=myapp:1.2.3" >> build.envartifacts:reports:dotenv:build.envdeploy:script:-echo "Deploying $BUILD_VERSION"-docker push $DOCKER_TAGneeds:-job:build-varsartifacts:true
# Protected environment (configured in GitLab UI)# Settings > CI/CD > Environmentsdeploy-production:stage:deployscript:-./deploy.sh productionenvironment:name:productionurl:https://example.comrules:-if:'$CI_COMMIT_BRANCH=="main"'when:manual# Requires:# - Allowed to deploy: Maintainer role# - Deployment approvals: 2 approvers required# Deployment with approvaldeploy:stage:deployscript:-echo "Deploying to production"environment:name:productiondeployment_tier:productionneeds:-job:buildartifacts:truerules:-if:'$CI_COMMIT_BRANCH=="main"'when:manualallow_failure:false
Security Scanning and Compliance
Integrated Security Templates
include:# Static Application Security Testing-template:Security/SAST.gitlab-ci.yml# Dependency Scanning-template:Security/Dependency-Scanning.gitlab-ci.yml# Container Scanning-template:Security/Container-Scanning.gitlab-ci.yml# Dynamic Application Security Testing-template:Security/DAST.gitlab-ci.yml# Secret Detection-template:Security/Secret-Detection.gitlab-ci.yml# Infrastructure as Code Scanning-template:Security/SAST-IaC.gitlab-ci.yml# Note: the standalone Security/License-Scanning.gitlab-ci.yml template was# removed in GitLab 17.0. Licence information is now produced by Dependency# Scanning (CycloneDX SBOM); no separate licence-scanning job is needed.variables:# Configure scannersSAST_EXCLUDED_PATHS:"spec,test,tests,tmp"DS_EXCLUDED_PATHS:"spec,test,tests,tmp"SECURE_ANALYZERS_PREFIX:"registry.gitlab.com/gitlab-org/security-products/analyzers"
# Compliance pipeline configurationinclude:-project:'compliance/pipelines'file:'compliance-jobs.yml'# License compliance# The `license_scanning` artifact report type was removed in GitLab 18.0.# Licence data is now ingested from a CycloneDX SBOM produced by Dependency# Scanning; emit a CycloneDX report and register it with the `cyclonedx` type.license-compliance:stage:testimage:licensefinder/license_finder:latestscript:-license_finder report --format cyclonedx > gl-sbom.cdx.jsonartifacts:reports:cyclonedx:gl-sbom.cdx.json# Code quality scanningcode-quality:stage:testimage:docker:stableservices:-docker:dindvariables:DOCKER_DRIVER:overlay2CODE_QUALITY_IMAGE:"registry.gitlab.com/gitlab-org/ci-cd/codequality:latest"script:-|docker run --env SOURCE_CODE="$PWD" \--volume "$PWD":/code \--volume /var/run/docker.sock:/var/run/docker.sock \"$CODE_QUALITY_IMAGE" /codeartifacts:reports:codequality:gl-code-quality-report.jsonexpire_in:1 week# Policy enforcement with OPApolicy-check:stage:testimage:openpolicyagent/opa:latestscript:-opa test policies/ -v-opa check policies/rules:-changes:-policies/**/*-data/**/*
Security Report Integration
# Vulnerability management workflowsecurity-gate:stage:testscript:-|# Parse security reports and fail on high/critical issuespython3 scripts/security-gate.py \--sast gl-sast-report.json \--dependency gl-dependency-scanning-report.json \--container gl-container-scanning-report.json \--max-high 0 \--max-critical 0needs:-sast-dependency_scanning-container_scanningartifacts:reports:metrics:security-metrics.txt# Merge request security widget# Automatically displays in GitLab MR interfacemerge-request-security:stage:testinherit:default:falsedependencies:-sast-dependency_scanning-secret_detectionscript:-echo "Security reports available in MR widget"rules:-if:'$CI_PIPELINE_SOURCE=="merge_request_event"'
# Directed Acyclic Graph for optimised executionstages:-build-test-deploybuild-frontend:stage:buildscript:npm run buildartifacts:paths:-dist/build-backend:stage:buildscript:mvn packageartifacts:paths:-target/*.jartest-unit:stage:testneeds:[build-backend]script:mvn testtest-integration:stage:testneeds:[build-backend,build-frontend]script:./integration-test.shtest-e2e:stage:testneeds:[build-frontend]script:npm run test:e2edeploy:stage:deployneeds:[test-unit,test-integration,test-e2e]script:./deploy.sh
Service Containers
# Database service for testingtest-with-db:image:node:16services:-name:postgres:14alias:postgres-name:redis:7-alpinealias:redisvariables:POSTGRES_DB:testdbPOSTGRES_USER:testuserPOSTGRES_PASSWORD:testpassPOSTGRES_HOST_AUTH_METHOD:trustDATABASE_URL:"postgresql://testuser:testpass@postgres:5432/testdb"REDIS_URL:"redis://redis:6379"script:-npm install-npm run db:migrate-npm test# Docker-in-Docker for container buildsbuild-docker:image:docker:24services:-docker:24-dindvariables:DOCKER_HOST:tcp://docker:2376DOCKER_TLS_CERTDIR:"/certs"DOCKER_TLS_VERIFY:1DOCKER_CERT_PATH:"$DOCKER_TLS_CERTDIR/client"before_script:-docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRYscript:-docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA .-docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
Quick Reference
Essential Pipeline Keywords
Keyword
Purpose
Example
stages
Define pipeline stages
stages: [build, test, deploy]
script
Commands to execute
script: npm test
image
Docker image for job
image: node:16
services
Service containers
services: [postgres:14]
before_script
Commands before script
before_script: npm install
after_script
Commands after script
after_script: cleanup.sh
variables
Environment variables
variables: NODE_ENV: production
cache
Cache directories
cache: paths: [node_modules/]
artifacts
Job outputs
artifacts: paths: [dist/]
dependencies
Artefact dependencies
dependencies: [build]
needs
DAG dependencies
needs: [build, test]
rules
Conditional execution
rules: if: $CI_COMMIT_BRANCH
environment
Deployment target
environment: production
when
Job execution timing
when: manual
allow_failure
Continue on failure
allow_failure: true
retry
Retry configuration
retry: max: 2
timeout
Job timeout
timeout: 2h
parallel
Parallel execution
parallel: 5
trigger
Trigger child pipeline
trigger: include: child.yml
extends
Inherit configuration
extends: .template
include
Include external config
include: template: SAST.yml
Predefined Variables
Variable
Description
$CI_COMMIT_SHA
Commit hash
$CI_COMMIT_REF_NAME
Branch or tag name
$CI_COMMIT_REF_SLUG
URL-safe ref name
$CI_COMMIT_BRANCH
Branch name (not for tags)
$CI_COMMIT_TAG
Tag name (only for tags)
$CI_PIPELINE_SOURCE
Pipeline trigger source
$CI_PROJECT_DIR
Project directory path
$CI_PROJECT_NAME
Project name
$CI_PROJECT_PATH
Project path with namespace
$CI_REGISTRY
Container registry URL
$CI_REGISTRY_IMAGE
Container registry image path
$CI_JOB_NAME
Current job name
$CI_JOB_STAGE
Current stage name
$CI_ENVIRONMENT_NAME
Environment name
$CI_ENVIRONMENT_URL
Environment URL
$GITLAB_USER_LOGIN
User triggering pipeline
Runner Management Commands
# Install GitLab Runner
curl-Lhttps://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.deb.sh|sudobash
sudoapt-getinstallgitlab-runner
# Register runner
sudogitlab-runnerregister\--urlhttps://gitlab.com/\--registration-token$REGISTRATION_TOKEN\--executordocker\--docker-imagealpine:latest\--description"My Runner"\--tag-list"docker,linux"# Runner operations
sudogitlab-runnerstart
sudogitlab-runnerstop
sudogitlab-runnerrestart
sudogitlab-runnerstatus
sudogitlab-runnerverify
# Unregister runner
sudogitlab-runnerunregister--urlhttps://gitlab.com/--token$RUNNER_TOKEN# Run single job for debugging
sudogitlab-runnerexecdockertest-job
# Cache not being usedbuild:cache:key:files:-package-lock.json# Ensure key file existspaths:-node_modules/policy:pull-pushscript:-npm ci# Use ci instead of install for reproducible builds# Artefacts not availabletest:needs:-job:buildartifacts:true# Explicitly enable artefact downloadscript:-ls -la dist/# Verify artefacts exist# Cache pollution fixclean-cache:script:-rm -rf node_modules/cache:key:npm-${CI_COMMIT_REF_SLUG}paths:-node_modules/policy:push# Only upload, don't downloadwhen:manual
Runner Configuration Issues
# Runner not picking up jobs# 1. Check runner status
sudogitlab-runnerverify
# 2. Check runner logs
sudojournalctl-ugitlab-runner-f
# 3. Ensure runner is not paused (GitLab UI)# Settings > CI/CD > Runners# 4. Check runner tags match job tags
gitlab-runnerlist
# Docker executor permission issues# Add gitlab-runner to docker group
sudousermod-aGdockergitlab-runner
sudosystemctlrestartgitlab-runner
# Disk space issues# Clean up old containers and images
dockersystemprune-a--volumes
Security Scan Failures
# SAST analyzer failingsast:variables:SAST_EXCLUDED_ANALYZERS:"eslint"# Exclude problematic analyzerSAST_ANALYZER_IMAGE_TAG:"3"# Pin to specific versionallow_failure:true# Don't block pipeline during rollout# Container scanning with custom policiescontainer_scanning:variables:CS_SEVERITY_THRESHOLD:"high"# Only fail on high/criticalCS_IMAGE:$CI_REGISTRY_IMAGE:$CI_COMMIT_SHACS_DOCKERFILE_PATH:Dockerfilebefore_script:-echo "Scanning $CS_IMAGE"# Dependency scanning exclusionsdependency_scanning:variables:DS_EXCLUDED_PATHS:"spec,test,tests,tmp,node_modules"DS_JAVA_VERSION:"11"DS_REMEDIATE:"false"# Don't auto-fix vulnerabilities
Environment and Deployment Issues
# Environment not showing in deploymentsdeploy:environment:name:productionurl:https://example.com# URL required for environment to showdeployment_tier:production# Helps with environment organisation# Stuck deploymentsstop-environment:script:-echo "Cleaning up environment"environment:name:review/$CI_COMMIT_REF_SLUGaction:stopwhen:manualrules:-if:'$CI_COMMIT_REF_NAME!="main"'when:manual# Protected environment access issues# Solution: Grant deployment permissions in GitLab UI# Settings > CI/CD > Protected Environments# Add allowed roles/users for deployment
Network and Connectivity
# Docker registry authentication.docker-login:&docker-loginbefore_script:-echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" --password-stdin "$CI_REGISTRY"-docker info# Proxy configurationvariables:HTTP_PROXY:"http://proxy.example.com:8080"HTTPS_PROXY:"http://proxy.example.com:8080"NO_PROXY:"localhost,127.0.0.1,.example.com"http_proxy:"http://proxy.example.com:8080"https_proxy:"http://proxy.example.com:8080"no_proxy:"localhost,127.0.0.1,.example.com"# DNS resolution issuestest:script:-cat /etc/resolv.conf-nslookup gitlab.com-curl -v https://gitlab.com# Configure runner with custom DNS# In config.toml: dns = ["8.8.8.8", "8.8.4.4"]
Performance Optimisation
# Reduce pipeline duration with needsbuild-frontend:stage:buildscript:npm run buildartifacts:paths:[dist/]build-backend:stage:buildscript:mvn packageartifacts:paths:[target/]# Both tests run in parallel, not sequentiallytest-frontend:stage:testneeds:[build-frontend]# Start as soon as frontend buildsscript:npm testtest-backend:stage:testneeds:[build-backend]# Start as soon as backend buildsscript:mvn test# Optimise Docker builds with BuildKitdocker-build:image:docker:24services:-docker:24-dindvariables:DOCKER_BUILDKIT:1DOCKER_DRIVER:overlay2script:-docker build --cache-from $CI_REGISTRY_IMAGE:latest --build-arg BUILDKIT_INLINE_CACHE=1 -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA .# Shallow clone for faster checkoutsvariables:GIT_DEPTH:10# Only fetch last 10 commitsGIT_STRATEGY:fetch# Or 'clone' for clean state