Skip to main content

GitLab CI/CD Pipelines — Configuration, Runners, and Environment Deployments

Configure GitLab CI/CD pipelines with .gitlab-ci.yml — covering runners, environments, artifacts, cache, rules, and deployment to Kubernetes clusters.

22 Terms

Overview and What You Will Learn

GitLab CI defines pipelines in a .gitlab-ci.yml file at the repository root. No external tools, no separate dashboard -- the pipeline lives where the code lives and is tightly integrated with merge requests. GitLab CI is common at organisations running self-hosted GitLab for compliance, and at teams that prefer having their entire DevOps toolchain in one platform.

By the end of this lab you will:

  • Write a complete .gitlab-ci.yml with stages, rules, and environments
  • Configure GitLab runners with correct executor types and tags
  • Use artifacts and cache correctly to pass data between jobs
  • Deploy to Kubernetes with GitLab CI environment tracking
  • Use rules to control exactly when each job runs
  • Understand GitLab CI vs GitHub Actions and when to use each

Why This Matters in Production

GitLab CI is the default choice for self-hosted DevOps platforms. Organisations that cannot use GitHub.com -- due to data residency, compliance, or cost -- often run GitLab on-premise. Engineers who know GitLab CI are immediately productive in these environments without needing to set up external CI tools.

Core Principles

GitLab CI pipeline execution model:

◈ DIAGRAM
+------------------------------------------+
| .gitlab-ci.yml defines: |
| stages: [build, test, scan, deploy] |
+------------------------------------------+
|
push or MR event
|
v
+------------------------------------------+
| GitLab creates pipeline |
| Resolves job-to-stage assignments |
| Builds dependency DAG |
+------------------------------------------+
|
v
+------------------------------------------+
| Jobs in same stage run in parallel |
| Next stage starts when all jobs pass |
| Failed job blocks subsequent stages |
+------------------------------------------+
|
v
+------------------------------------------+
| Runner picks up job from queue |
| Executes in Docker container |
| Reports result back to GitLab |
+------------------------------------------+

Key GitLab CI concepts vs GitHub Actions:

TEXT
GitLab CI GitHub Actions
----------- --------------
stages: jobs with needs:
.gitlab-ci.yml .github/workflows/*.yaml
runners runners
artifacts artifacts
cache cache
environments environments
rules: if: conditions
$CI_COMMIT_SHA ${{ github.sha }}
$CI_REGISTRY_IMAGE ${{ env.ECR_REGISTRY }}/image

Detailed Step-by-Step Practical Lab

Milestone 1 -- Basic .gitlab-ci.yml structure

YAML
## .gitlab-ci.yml
## Define stage order
stages:
- build
- test
- scan
- deploy
## Variables available to all jobs
variables:
## GitLab built-in: full image path in project registry
IMAGE_NAME: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
DOCKER_DRIVER: overlay2
DOCKER_TLS_CERTDIR: ''
## YAML anchor -- reusable configuration block
.node-base: &node-base
image: node:20-alpine
cache:
key: $CI_COMMIT_REF_SLUG
paths:
- node_modules/
before_script:
- npm ci
build-image:
stage: build
image: docker:24
services:
- docker:24-dind
script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
- docker build -t $IMAGE_NAME .
- docker push $IMAGE_NAME

Milestone 2 -- Tests, artifacts, and cache

YAML
unit-tests:
stage: test
<<: *node-base ## merge the node-base anchor
script:
- npm test -- --coverage
## Artifacts: files passed to downstream jobs or kept for inspection
artifacts:
## JUnit format: GitLab parses and shows test results on MR
reports:
junit: junit.xml
## Coverage report: available to download
paths:
- coverage/
## How long to keep artifacts
expire_in: 1 week
## Upload even when job fails -- critical for test result inspection
when: always
lint:
stage: test
<<: *node-base
script:
- npm run lint
- npm run format:check
## Coverage gate: fail if coverage drops below threshold
coverage-gate:
stage: test
<<: *node-base
needs: [unit-tests] ## wait for unit-tests specifically
script:
- COVERAGE=$(cat coverage/coverage-summary.json | jq '.total.lines.pct')
- echo "Coverage is $COVERAGE%"
- |
python3 -c "
import sys
coverage = float('$COVERAGE')
if coverage < 80:
print(f'Coverage {coverage}% below 80% threshold')
sys.exit(1)
print('Coverage gate passed')
"

Milestone 3 -- Security scanning

YAML
trivy-scan:
stage: scan
image:
name: aquasec/trivy:latest
entrypoint: ['']
variables:
GIT_STRATEGY: none ## skip git checkout -- not needed for image scan
script:
## Scan for HIGH and CRITICAL vulnerabilities
- trivy image
--exit-code 1
--severity HIGH,CRITICAL
--no-progress
--format json
--output trivy-report.json
$IMAGE_NAME
artifacts:
paths:
- trivy-report.json
expire_in: 1 week
when: always ## keep report even on failure
## Only run on main branch and MRs
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
- if: $CI_PIPELINE_SOURCE == 'merge_request_event'

Milestone 4 -- Rules for conditional job execution

YAML
## Rules control WHEN a job runs -- replacing the older 'only/except'
deploy-staging:
stage: deploy
script:
- kubectl set image deployment/payment-api payment-api=$IMAGE_NAME -n staging
environment:
name: staging
url: https://staging.payment.internal
rules:
## Run automatically on default branch
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
when: on_success
## Never run on MRs
- when: never
deploy-production:
stage: deploy
script:
- kubectl set image deployment/payment-api payment-api=$IMAGE_NAME -n production
environment:
name: production
url: https://api.payment.razorpay.com
rules:
## Manual trigger only on default branch
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
when: manual
- when: never
needs:
- deploy-staging ## must succeed before production can be triggered
## Skip expensive jobs for documentation-only changes
unit-tests:
rules:
## Skip if only .md files changed
- changes:
- '**/*.md'
- 'docs/**'
when: never
- when: on_success

Milestone 5 -- Environments and deployment tracking

YAML
## GitLab tracks every deployment to named environments
## View in: Deployments > Environments
deploy-staging:
stage: deploy
image: bitnami/kubectl:latest
script:
## Use GitLab CI/CD variable for kubeconfig
- echo "$KUBECONFIG_STAGING" > /tmp/kubeconfig
- export KUBECONFIG=/tmp/kubeconfig
- |
helm upgrade --install payment-api ./charts/payment-api \\
--namespace payment-api-staging \\
--values ./charts/values-staging.yaml \\
--set image.tag=$CI_COMMIT_SHA \\
--atomic --timeout 5m --wait
- rm -f /tmp/kubeconfig
environment:
## Name appears in GitLab Environments page
name: staging
## URL shown as a link in GitLab
url: https://staging.payment.internal
## Auto stop environment after 7 days of inactivity
auto_stop_in: 7 days
## Review environments for merge requests
deploy-review:
stage: deploy
script:
- ./deploy-review.sh $CI_MERGE_REQUEST_IID
environment:
## Dynamic name per MR -- creates separate environment per PR
name: review/$CI_COMMIT_REF_SLUG
url: https://review-$CI_MERGE_REQUEST_IID.payment.internal
## Auto-delete when MR is merged or closed
on_stop: stop-review
rules:
- if: $CI_PIPELINE_SOURCE == 'merge_request_event'
stop-review:
stage: deploy
script:
- ./teardown-review.sh $CI_MERGE_REQUEST_IID
environment:
name: review/$CI_COMMIT_REF_SLUG
action: stop
rules:
- if: $CI_PIPELINE_SOURCE == 'merge_request_event'
when: manual

Milestone 6 -- Self-hosted runner registration

Bash
## Install GitLab Runner on a Linux server
curl -L https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.deb.sh | sudo bash
sudo apt install gitlab-runner
## Register a runner with Docker executor
sudo gitlab-runner register \
--non-interactive \
--url https://gitlab.company.in \
--token REGISTRATION_TOKEN \
--executor docker \
--docker-image alpine:latest \
--description payment-api-runner \
--tag-list docker,payment-vpc \
--docker-volumes /var/run/docker.sock:/var/run/docker.sock
## Verify runner is registered
sudo gitlab-runner status
sudo gitlab-runner list
## Use runner tags in jobs to target specific runners
build-image:
tags:
- docker ## only run on runners tagged 'docker'
- payment-vpc ## only run on runners in payment VPC
## Runner config file: /etc/gitlab-runner/config.toml
## Tune concurrency, resource limits per runner

Production Best Practices and Common Pitfalls

Mistake Problem Fix
Using only/except Deprecated, less powerful Use rules: blocks always
No artifact expiry Storage fills up Set expire_in on all artifacts
Shared runner for prod deploy No network access to cluster Register specific runner in prod VPC
No cache key strategy Cache never invalidates Key cache on lockfile hash
Missing when: always on artifacts Test results lost on failure Upload artifacts when: always

Quick Reference and Troubleshooting Commands

Task Command
View pipeline glab ci view
View job logs glab ci trace JOB_ID
Retry failed job glab ci retry JOB_ID
Cancel pipeline glab ci cancel PIPELINE_ID
Validate YAML GitLab UI > CI/CD > CI Lint
Trigger via API curl -X POST -H 'PRIVATE-TOKEN: $TOKEN' URL/pipeline
List runners sudo gitlab-runner list
Tip

Use GitLab's built-in CI Lint tool (available at /-/ci/lint in your project) to validate .gitlab-ci.yml before pushing. It shows syntax errors, invalid keywords, and can simulate what pipeline a given push would generate -- invaluable when debugging complex rules.

Remember

GitLab CI predefined variables are always available without configuration. $CI_COMMIT_SHA is the full commit hash. $CI_REGISTRY_IMAGE is the full container registry path for the project. $CI_DEFAULT_BRANCH is the name of the default branch (main or master). Use these instead of hardcoding values.

Security

Mark CI/CD variables as Protected and Masked in GitLab settings. Protected variables are only available to pipelines on protected branches -- preventing a feature branch pipeline from accessing production credentials. Masked variables are redacted from job logs even if accidentally printed.

Common Mistake

Forgetting GIT_STRATEGY: none on jobs that do not need source code. By default, every job checks out the entire repository -- even a job that just scans a Docker image. Set GIT_STRATEGY: none on jobs that only need artifacts from previous jobs or just run a command against an external resource. This eliminates unnecessary git operations and speeds up those jobs significantly.

Resources

Explore More in CI/CD Tools and Platforms

All 3 Topics

Frequently Asked Questions

Is GitLab CI/CD Pipelines — Configuration, Runners, and Environment Deployments free to learn on DevOps Network?

Yes - this topic, like everything on DevOps Network, is 100% free with no paywall or sign-up gate.

What does the GitLab CI/CD Pipelines — Configuration, Runners, and Environment Deployments topic cover?

Configure GitLab CI/CD pipelines with .gitlab-ci.yml — covering runners, environments, artifacts, cache, rules, and deployment to Kubernetes clusters.