GitLab CI
GitLab CI is GitLab's integrated CI/CD system defined in a .gitlab-ci.yml file at the repository root. It runs jobs on registered runners, tracks deployments to named environments, and integrates tightly with GitLab's merge request workflow for inline pipeline status.
Understanding GitLab CI
What Is GitLab CI in Simple Terms
GitLab CI is the CI/CD system built into GitLab. Like GitHub Actions for GitHub, it requires no external tools — define your pipeline in a YAML file, push it, and GitLab runs it. It is particularly strong at environment tracking (showing which version is deployed where across all your environments) and merge request integration (pipeline status shown inline on every MR).
Organisations running self-hosted GitLab for compliance — common in Indian financial services and regulated industries — use GitLab CI because it stays entirely within their infrastructure.
How It Works
+------------------------------------------+| .gitlab-ci.yml in repository root |+------------------------------------------+ | push or MR event | v+------------------------------------------+| GitLab pipeline created || Jobs assigned to stages || Dependency graph (DAG) resolved |+------------------------------------------+ | v+------------------------------------------+| Runner picks up job from queue || || Shared runner (GitLab SaaS) || OR || Specific runner (your infrastructure) |+------------------------------------------+ | v+------------------------------------------+| Job executes in Docker container || Results: artifacts, reports, logs || Environment deployment tracked |+------------------------------------------+Complete .gitlab-ci.yml:
## .gitlab-ci.yml ## Define the order of stagesstages: - build - test - scan - deploy ## Variables available to all jobsvariables: IMAGE_NAME: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA DOCKER_DRIVER: overlay2 ## Reusable configuration with YAML anchors.node-base: &node-base image: node:20-alpine cache: key: $CI_COMMIT_REF_SLUG paths: - node_modules/ 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 unit-tests: stage: test <<: *node-base ## merge node-base config script: - npm ci - npm test -- --coverage --reporter=junit ## Publish JUnit test results to GitLab artifacts: reports: junit: junit.xml paths: - coverage/ expire_in: 1 week security-scan: stage: scan image: name: aquasec/trivy:latest entrypoint: [""] script: - trivy image --exit-code 1 --severity HIGH,CRITICAL $IMAGE_NAME 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 control when this job runs rules: - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH 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 ## Manual trigger only on main branch rules: - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH when: manual needs: - deploy-staging ## must deploy to staging firstPractical Commands
## GitLab CLI -- view pipeline statusglab ci viewglab ci status ## View specific job logsglab ci trace JOB_ID ## Retry a failed jobglab ci retry JOB_ID ## Cancel a pipelineglab ci cancel PIPELINE_ID ## Trigger a pipeline via APIcurl -X POST \ -H "PRIVATE-TOKEN: $GITLAB_TOKEN" \ "https://gitlab.com/api/v4/projects/PROJECT_ID/pipeline" \ -d "ref=main"Troubleshooting
| Symptom | Check | What to Look For |
|---|---|---|
| Job stuck pending | Runner availability | No runners with matching tags |
| YAML syntax error | CI Lint tool in GitLab UI | Indentation and key names |
| Rules not matching | rules:if conditions | Variable names and values |
| Cache not working | Cache key and paths | Key must be consistent across runs |
TipUse GitLab's built-in CI Lint tool (Settings > CI/CD > CI Lint) to validate your .gitlab-ci.yml before pushing. It catches YAML syntax errors, invalid keywords, and rule logic errors — much faster than push-and-see.
RememberGitLab CI predefined variables are your friends.
$CI_COMMIT_SHAis the full commit hash.$CI_COMMIT_SHORT_SHAis the short version.$CI_REGISTRY_IMAGEis the full path to your project's container registry.$CI_ENVIRONMENT_NAMEis the name of the environment being deployed to. Use these instead of hardcoding values.
SecurityProtect your production environment in GitLab (Settings > CI/CD > Environments > Edit > Protected). Protected environments restrict which branches can deploy and which users can manually trigger deployments — preventing a developer branch from accidentally deploying to production.
Common MistakeUsing
when: alwayson deployment jobs. This deploys on every pipeline run regardless of branch, test results, or manual conditions. Userules:blocks to explicitly define when deployment jobs should run — they should only run on specific branches (main, release/*) and after required stages pass.
Frequently Asked Questions
What's the practical benefit of GitLab CI's environments feature tying directly into merge requests?
Because GitLab owns both the source control and CI system, a merge request can show live deployment status, environment URLs, and even a 'Review App' link for a temporary environment spun up just for that branch — all inline without leaving the MR page. This tight coupling is harder to replicate with an external CI tool bolted onto a separately-hosted Git provider, where environment state has to be surfaced via API integration rather than being native.
What's a common mistake when structuring .gitlab-ci.yml stages and jobs?
Not using `rules:` or `only/except` conditions to scope jobs correctly causes every job to run on every pipeline trigger, including expensive deploy jobs firing on feature-branch pushes that should never deploy anywhere. A cleaner pattern is defining reusable job templates with `extends` or `!reference` tags and gating environment-specific jobs explicitly by branch or trigger type, keeping feature-branch pipelines fast and deploy-only jobs isolated to protected branches.