GitLab CI/CD
GitLab CI/CD is GitLab’s built-in automation platform. Pipelines are defined in a single .gitlab-ci.yml file at the repository root and run on GitLab-hosted shared runners or self-managed runners. Its tight integration with GitLab’s merge requests, container registry, environments, and package registry makes it an all-in-one DevOps platform.
How GitLab CI/CD Works
Section titled “How GitLab CI/CD Works”Event (push, MR, schedule, tag, ...) │ ▼Pipeline (.gitlab-ci.yml) │ ├── Stage: build │ └── Job: compile │ ├── Stage: test │ ├── Job: unit-tests │ └── Job: lint │ └── Stage: deploy └── Job: deploy-staging| Concept | What It Is |
|---|---|
| Pipeline | A collection of jobs organized into stages, triggered by an event |
| Stage | A phase of the pipeline; all jobs in a stage run in parallel; stages run sequentially |
| Job | A set of commands that run on a runner; the basic unit of execution |
| Runner | The machine (shared, group, or project) that executes jobs |
Pipeline YAML Anatomy
Section titled “Pipeline YAML Anatomy”stages: # Define stage order - build - test - deploy
variables: # Pipeline-level variables NODE_ENV: production
default: # Defaults applied to all jobs image: node:20-alpine before_script: - npm ci
compile: # Job name stage: build # Which stage this job belongs to script: # Commands to run - npm run build artifacts: # Files to pass to later stages paths: - dist/ expire_in: 1 hour
unit-tests: stage: test script: - npm test -- --coverage coverage: '/Lines\s*:\s*(\d+\.?\d*)%/' # Parse coverage from output artifacts: reports: junit: junit.xml # GitLab parses JUnit for MR widget coverage_report: coverage_format: cobertura path: coverage/cobertura-coverage.xml
lint: stage: test script: - npm run lint
deploy-staging: stage: deploy script: - echo "Deploying to staging..." - ./deploy.sh staging environment: name: staging url: https://staging.myapp.com only: - mainKey YAML Keywords
Section titled “Key YAML Keywords”| Keyword | Purpose | Example |
|---|---|---|
stages | Define stage execution order | [build, test, deploy] |
image | Docker image to run the job in | node:20, python:3.12 |
script | Commands to execute | - npm test |
before_script | Commands before script (e.g. install deps) | - pip install -r requirements.txt |
after_script | Commands after script (cleanup, always runs) | - echo "Job finished" |
artifacts | Files to save and pass to later stages | paths: [dist/] |
cache | Dependencies to cache across pipeline runs | paths: [node_modules/] |
variables | Environment variables | APP_ENV: staging |
environment | Deploy target with URL and tracking | name: production |
only / except | Simple branch/tag filters (legacy) | only: [main] |
rules | Advanced conditional logic (preferred) | See below |
needs | DAG dependencies (skip stage ordering) | needs: [compile] |
extends | Inherit from another job definition | extends: .deploy-template |
include | Import external YAML files | include: 'templates/deploy.yml' |
Triggers and Rules
Section titled “Triggers and Rules”rules (Preferred)
Section titled “rules (Preferred)”deploy-production: stage: deploy script: - ./deploy.sh production rules: - if: $CI_COMMIT_BRANCH == "main" # Only on main branch when: manual # Require manual click allow_failure: false # Block the pipeline until clicked - if: $CI_PIPELINE_SOURCE == "schedule" # Also run on schedules - when: never # Skip for everything elseCommon rules Conditions
Section titled “Common rules Conditions”| Variable | Meaning | Example |
|---|---|---|
$CI_COMMIT_BRANCH | Branch name | == "main" |
$CI_COMMIT_TAG | Tag name (set only for tag pipelines) | =~ /^v\d+/ |
$CI_PIPELINE_SOURCE | What triggered the pipeline | == "merge_request_event" |
$CI_MERGE_REQUEST_SOURCE_BRANCH_NAME | MR source branch | == "feature/x" |
changes | File path patterns that changed | changes: [src/**] |
when Values
Section titled “when Values”| Value | Behavior |
|---|---|
on_success | Run if previous stages succeeded (default) |
on_failure | Run only if a previous stage failed |
always | Run regardless of previous stage status |
manual | Wait for a user to click “Run” in the UI |
delayed | Run after a delay (start_in: 30 minutes) |
never | Don’t run this job |
Merge Request Pipelines
Section titled “Merge Request Pipelines”test: stage: test script: - npm test rules: - if: $CI_PIPELINE_SOURCE == "merge_request_event" - if: $CI_COMMIT_BRANCH == "main"This runs the job for both merge request pipelines and pushes to main.
Artifacts and Caching
Section titled “Artifacts and Caching”Artifacts
Section titled “Artifacts”build: stage: build script: - npm run build artifacts: paths: - dist/ # Files available to later stages expire_in: 1 week # Auto-delete after 1 week reports: junit: test-results.xml # Parsed by GitLab for MR widgetCaching
Section titled “Caching”default: cache: key: files: - package-lock.json # Cache key changes when lockfile changes paths: - node_modules/ policy: pull-push # pull: restore only, push: save only, pull-push: bothArtifacts vs Cache
Section titled “Artifacts vs Cache”| Artifacts | Cache | |
|---|---|---|
| Purpose | Pass files between stages/jobs in the same pipeline | Speed up jobs across pipeline runs |
| Scope | Single pipeline | Across pipelines (same branch, then default branch) |
| Example | Build output, test reports | node_modules, .pip, .gradle |
| Expiration | Configurable (expire_in) | LRU eviction |
Variables and Secrets
Section titled “Variables and Secrets”Defining Variables
Section titled “Defining Variables”# In .gitlab-ci.ymlvariables: APP_NAME: myapp DEPLOY_TIMEOUT: "300"
# Job-level variables (override pipeline-level)deploy: variables: APP_ENV: production script: - echo "Deploying $APP_NAME to $APP_ENV"CI/CD Variables (UI / API)
Section titled “CI/CD Variables (UI / API)”Set secrets in Settings > CI/CD > Variables:
| Setting | What It Does |
|---|---|
| Masked | Value is hidden in job logs (***) |
| Protected | Only available in protected branches/tags |
| File | Written to a temp file; $VAR contains the file path |
| Environment scope | Only available for jobs targeting a specific environment |
Predefined Variables
Section titled “Predefined Variables”GitLab provides 100+ predefined variables:
| Variable | Value |
|---|---|
CI_COMMIT_SHA | Full commit SHA |
CI_COMMIT_SHORT_SHA | First 8 chars |
CI_COMMIT_BRANCH | Branch name |
CI_COMMIT_TAG | Tag name (if tag pipeline) |
CI_PIPELINE_ID | Pipeline ID |
CI_PROJECT_NAME | Repository name |
CI_REGISTRY_IMAGE | Container registry image path |
CI_JOB_TOKEN | Token for API access within the pipeline |
Environments and Deployment
Section titled “Environments and Deployment”Defining Environments
Section titled “Defining Environments”deploy-staging: stage: deploy script: - kubectl apply -f k8s/staging/ environment: name: staging url: https://staging.myapp.com on_stop: stop-staging # Job to run when environment is stopped
stop-staging: stage: deploy script: - kubectl delete -f k8s/staging/ environment: name: staging action: stop when: manualReview Apps
Section titled “Review Apps”Review apps create a temporary environment for each merge request:
review: stage: deploy script: - helm install review-$CI_MERGE_REQUEST_IID ./chart \ --set image.tag=$CI_COMMIT_SHORT_SHA \ --set ingress.host=$CI_MERGE_REQUEST_IID.review.myapp.com environment: name: review/$CI_MERGE_REQUEST_IID url: https://$CI_MERGE_REQUEST_IID.review.myapp.com on_stop: stop-review auto_stop_in: 1 week rules: - if: $CI_PIPELINE_SOURCE == "merge_request_event"
stop-review: stage: deploy script: - helm uninstall review-$CI_MERGE_REQUEST_IID environment: name: review/$CI_MERGE_REQUEST_IID action: stop when: manual rules: - if: $CI_PIPELINE_SOURCE == "merge_request_event"Each MR gets its own URL — reviewers can test the change live before merging.
Runners
Section titled “Runners”Runner Types
Section titled “Runner Types”| Type | Scope | Who Manages |
|---|---|---|
| Shared | Available to all projects on the instance | GitLab (SaaS) or instance admin |
| Group | Available to all projects in a group | Group owner |
| Project | Available to a single project | Project maintainer |
Runner Executors
Section titled “Runner Executors”The executor determines how the runner runs jobs:
| Executor | How It Runs | Best For |
|---|---|---|
| Docker | Each job runs in a fresh Docker container | Most common — clean, reproducible |
| Kubernetes | Each job runs as a Kubernetes pod | Auto-scaling on K8s clusters |
| Shell | Runs directly on the runner’s OS | Simple, but no isolation |
| Docker Machine | Auto-provisions cloud VMs (Docker Machine) | Auto-scaling on cloud (legacy) |
| Instance | Auto-provisions cloud VMs (newer, replaces Docker Machine) | Auto-scaling on cloud |
Registering a Runner
Section titled “Registering a Runner”# Install GitLab Runnercurl -L https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.deb.sh | sudo bashsudo apt install gitlab-runner
# Register with the GitLab instancesudo gitlab-runner register \ --url https://gitlab.com \ --registration-token <TOKEN> \ --executor docker \ --docker-image node:20-alpine \ --description "Docker runner" \ --tag-list "docker,linux"Tagging
Section titled “Tagging”build: tags: - docker # Only run on runners with the "docker" tag - linux script: - make buildDAG Pipelines (needs)
Section titled “DAG Pipelines (needs)”By default, stages are sequential. The needs keyword creates a Directed Acyclic Graph (DAG) — jobs start as soon as their dependencies finish, regardless of stage:
stages: - build - test - deploy
build-frontend: stage: build script: npm run build:frontend artifacts: paths: [frontend/dist/]
build-backend: stage: build script: go build -o api ./cmd/api
test-frontend: stage: test needs: [build-frontend] # Starts as soon as build-frontend finishes script: npm run test:frontend
test-backend: stage: test needs: [build-backend] # Doesn't wait for build-frontend script: go test ./...
deploy: stage: deploy needs: [test-frontend, test-backend] # Waits for both tests script: ./deploy.shbuild-frontend ──► test-frontend ──┐ ├──► deploybuild-backend ──► test-backend ──┘Without needs, test-frontend would wait for both build-frontend AND build-backend to finish (because they’re in the same stage).
Parent-Child and Multi-Project Pipelines
Section titled “Parent-Child and Multi-Project Pipelines”Parent-Child (Same Project)
Section titled “Parent-Child (Same Project)”Split a large .gitlab-ci.yml into smaller files:
# .gitlab-ci.yml (parent)stages: - triggers
trigger-frontend: stage: triggers trigger: include: frontend/.gitlab-ci.yml strategy: depend # Parent pipeline waits for child
trigger-backend: stage: triggers trigger: include: backend/.gitlab-ci.yml strategy: dependMulti-Project (Cross-Repo)
Section titled “Multi-Project (Cross-Repo)”Trigger a pipeline in another project:
deploy-infra: stage: deploy trigger: project: myorg/infrastructure # Trigger pipeline in another repo branch: main strategy: dependInclude and Extends (Reusable Templates)
Section titled “Include and Extends (Reusable Templates)”include
Section titled “include”Import pipeline definitions from external sources:
include: # From the same project - local: 'templates/deploy.yml'
# From another project - project: 'myorg/ci-templates' ref: main file: '/templates/docker-build.yml'
# From a URL - remote: 'https://example.com/ci-templates/lint.yml'
# From a CI/CD template gallery - template: Security/SAST.gitlab-ci.ymlextends
Section titled “extends”Inherit from a hidden job (prefixed with .):
.deploy-template: # Hidden job (template) image: bitnami/kubectl:latest before_script: - kubectl config use-context $KUBE_CONTEXT script: - kubectl apply -f k8s/$APP_ENV/ - kubectl rollout status deployment/$APP_NAME
deploy-staging: extends: .deploy-template variables: APP_ENV: staging KUBE_CONTEXT: staging-cluster environment: name: staging
deploy-production: extends: .deploy-template variables: APP_ENV: production KUBE_CONTEXT: prod-cluster environment: name: production when: manualAuto DevOps
Section titled “Auto DevOps”Auto DevOps is GitLab’s convention-over-configuration pipeline. Enable it and GitLab automatically:
- Builds your app (auto-detects language via buildpack).
- Tests with built-in test suites.
- Scans for vulnerabilities (SAST, DAST, dependency scanning, container scanning).
- Deploys to Kubernetes (if a cluster is connected).
- Creates review apps for merge requests.
- Sets up monitoring with Prometheus.
# Enable in .gitlab-ci.yml (or via Settings > CI/CD)include: - template: Auto-DevOps.gitlab-ci.ymlAuto DevOps is great for getting started quickly, but most teams customize their pipeline as complexity grows.
OIDC (OpenID Connect)
Section titled “OIDC (OpenID Connect)”GitLab CI supports OIDC for cloud authentication without stored secrets:
deploy-aws: image: amazon/aws-cli:latest id_tokens: AWS_TOKEN: aud: https://gitlab.com # Audience claim script: - > export $(printf "AWS_ACCESS_KEY_ID=%s AWS_SECRET_ACCESS_KEY=%s AWS_SESSION_TOKEN=%s" $(aws sts assume-role-with-web-identity --role-arn arn:aws:iam::123456789012:role/gitlab-deploy --role-session-name "GitLabCI-${CI_JOB_ID}" --web-identity-token "${AWS_TOKEN}" --query "Credentials.[AccessKeyId,SecretAccessKey,SessionToken]" --output text)) - aws s3 lsExample Pipelines
Section titled “Example Pipelines”Node.js CI
Section titled “Node.js CI”stages: - test - build - deploy
default: image: node:20-alpine cache: key: files: [package-lock.json] paths: [node_modules/]
lint: stage: test script: - npm ci - npm run lint
test: stage: test script: - npm ci - npm test -- --coverage coverage: '/Lines\s*:\s*(\d+\.?\d*)%/' artifacts: reports: junit: junit.xml
build: stage: build script: - npm ci - npm run build artifacts: paths: [dist/] expire_in: 1 day rules: - if: $CI_COMMIT_BRANCH == "main"Docker Build and Push
Section titled “Docker Build and Push”build-image: image: docker:24 services: - docker:24-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_SHORT_SHA . - docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA - docker tag $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA $CI_REGISTRY_IMAGE:latest - docker push $CI_REGISTRY_IMAGE:latest rules: - if: $CI_COMMIT_BRANCH == "main"Kubernetes Deploy
Section titled “Kubernetes Deploy”deploy-staging: image: bitnami/kubectl:latest stage: deploy script: - kubectl set image deployment/myapp myapp=$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA - kubectl rollout status deployment/myapp --timeout=120s environment: name: staging url: https://staging.myapp.com rules: - if: $CI_COMMIT_BRANCH == "main"GitLab CI vs GitHub Actions
Section titled “GitLab CI vs GitHub Actions”| Feature | GitLab CI | GitHub Actions |
|---|---|---|
| Config file | .gitlab-ci.yml (single) | .github/workflows/*.yml (multiple) |
| Stages | Explicit stages: block | Implicit via needs |
| Reuse | include, extends, templates | Reusable workflows, composite actions |
| Marketplace | Smaller (include from URLs) | Large marketplace |
| Runners | Shared + group + project, multiple executors | GitHub-hosted + self-hosted |
| Environments | Built-in with review apps | Built-in with protection rules |
| Container registry | Built-in ($CI_REGISTRY) | GitHub Packages (GHCR) |
| OIDC | id_tokens keyword | Built-in provider |
| DAG | needs keyword | needs in jobs |
| Auto DevOps | Yes (convention-over-configuration) | No equivalent |
| Parent-child pipelines | Yes (trigger + include) | No direct equivalent |
| Review apps | Built-in with dynamic environments | Manual setup |
| Best for | All-in-one DevOps platform | GitHub-hosted projects |
Key Takeaways
Section titled “Key Takeaways”- GitLab CI is configured in a single
.gitlab-ci.ymlfile at the repository root. - Stages define execution order; jobs in the same stage run in parallel.
- Use
rules(notonly/except) for conditional job execution. needscreates DAG pipelines for faster execution (skip waiting for unrelated stages).includeandextendsenable reusable pipeline templates across projects.- Review apps create temporary environments for every merge request.
- Runners come in Docker, Kubernetes, and Shell executors — Docker is most common.
- Auto DevOps provides a zero-config pipeline for building, testing, scanning, and deploying.
- GitLab CI shines as an all-in-one platform — code, CI/CD, registry, environments, and monitoring in one tool.