Skip to content

GitLab CI — .gitlab-ci.yml Complete Guide

DodaTech Updated 2026-06-24 5 min read

In this tutorial, you'll learn about GitLab CI. We cover key concepts, practical examples, and best practices to help you understand and apply this topic effectively.

GitLab CI is a built-in continuous integration and delivery platform that uses a .gitlab-ci.yml file to define pipelines — stages, jobs, dependencies, and deployment targets — all version-controlled alongside your code in every GitLab Repository.

What You'll Learn

Why It Matters

GitLab CI eliminates the need for external CI servers by integrating pipelines directly into the GitLab platform. Every push, merge request, or schedule triggers a pipeline automatically. DodaTech runs 500+ pipelines daily across 60 repositories using GitLab CI, with parallel test execution cutting feedback time from 45 minutes to under 8 minutes.

Real-World Use

The Durga Antivirus Pro team uses GitLab CI to build Windows, macOS, and Linux installers in parallel, run malware signature tests across 10,000+ samples, publish packages to a GitLab Package Registry, and deploy to staging environments — all defined in a single .gitlab-ci.yml file.

flowchart LR
    A[Git Push] --> B[.gitlab-ci.yml]
    B --> C[Stage: build]
    B --> D[Stage: test]
    B --> E[Stage: package]
    C --> F[Job: compile]
    D --> G[Job: unit]
    D --> H[Job: integration]
    E --> I[Job: docker-build]
    F --> G
    F --> H
    G --> I
    H --> I
    style B fill:#FC6D26,color:#fff
â„šī¸ Info

Prerequisites: A GitLab account and Repository. GitLab runners configured (shared or self-hosted).

Basic Pipeline Structure

# .gitlab-ci.yml
stages:
  - build
  - test
  - package
  - deploy

variables:
  DOCKER_DRIVER: overlay2
  APP_NAME: user-service
  REGISTRY: registry.gitlab.com/dodatech

cache:
  key: ${CI_COMMIT_REF_SLUG}
  paths:
    - node_modules/
    - .npm/

before_script:
  - node --version
  - npm --version

build:
  stage: build
  script:
    - npm ci
    - npm run build
  artifacts:
    paths:
      - dist/
    expire_in: 1 hour

unit-test:
  stage: test
  script:
    - npm run test:unit
  dependencies:
    - build

integration-test:
  stage: test
  script:
    - npm run test:integration
  dependencies:
    - build
  services:
    - postgres:16-alpine
    - redis:7-alpine

package:
  stage: package
  script:
    - docker build -t ${REGISTRY}/${APP_NAME}:${CI_COMMIT_SHORT_SHA} .
    - docker push ${REGISTRY}/${APP_NAME}:${CI_COMMIT_SHORT_SHA}
  only:
    - main

deploy-staging:
  stage: deploy
  script:
    - kubectl set image deployment/${APP_NAME} ${APP_NAME}=${REGISTRY}/${APP_NAME}:${CI_COMMIT_SHORT_SHA} -n staging
  environment:
    name: staging
    url: https://staging.dodatech.com
  only:
    - main
  needs:
    - package

deploy-production:
  stage: deploy
  script:
    - kubectl set image deployment/${APP_NAME} ${APP_NAME}=${REGISTRY}/${APP_NAME}:${CI_COMMIT_SHORT_SHA} -n production
  environment:
    name: production
    url: https://app.dodatech.com
  when: manual
  only:
    - main
  needs:
    - package

DAG Pipelines with needs

The needs keyword defines a directed acyclic graph (DAG), allowing jobs to start as soon as their dependencies complete:

stages:
  - build
  - test
  - deploy

build:
  stage: build
  script:
    - npm ci && npm run build
  artifacts:
    paths:
      - dist/

lint:
  stage: test
  script:
    - npm run lint
  needs: []

unit-test-backend:
  stage: test
  script:
    - npm run test:backend
  needs:
    - build

unit-test-frontend:
  stage: test
  script:
    - npm run test:frontend
  needs:
    - build

e2e-test:
  stage: test
  script:
    - npm run test:e2e
  needs:
    - build

deploy:
  stage: deploy
  script:
    - kubectl apply -f k8s/
  needs:
    - lint
    - unit-test-backend
    - unit-test-frontend
    - e2e-test

Rules and Conditions

variables:
  DOCKER_TLS_CERTDIR: ""

workflow:
  rules:
    - if: $CI_MERGE_REQUEST_IID
      when: always
    - if: $CI_COMMIT_TAG
      when: always
    - if: $CI_COMMIT_BRANCH == "main"
      when: always
    - when: never

deploy-review-app:
  stage: deploy
  script:
    - kubectl apply -f k8s/review/${CI_MERGE_REQUEST_IID}/
  environment:
    name: review/${CI_MERGE_REQUEST_IID}
    url: https://review-${CI_MERGE_REQUEST_IID}.dodatech.com
    on_stop: stop-review-app
  rules:
    - if: $CI_MERGE_REQUEST_IID
      when: manual

stop-review-app:
  stage: deploy
  script:
    - kubectl delete -f k8s/review/${CI_MERGE_REQUEST_IID}/
  environment:
    name: review/${CI_MERGE_REQUEST_IID}
    action: stop
  rules:
    - if: $CI_MERGE_REQUEST_IID
      when: manual

Self-Hosted Runners

# Register a GitLab runner
sudo gitlab-runner register \
  --url https://gitlab.com \
  --registration-token GL-REG-TOKEN \
  --executor docker \
  --description "DodaTech GPU Runner" \
  --docker-image "docker:24" \
  --docker-privileged \
  --tag-list "gpu,production" \
  --run-untagged="false" \
  --locked="false"
# Use tagged runners
build-gpu:
  stage: build
tags:
    - gpu
  script:
    - nvidia-smi
    - npm run build:gpu
  variables:
    NVIDIA_VISIBLE_DEVICES: all

Child Pipelines

Trigger child pipelines for microservice deployments:

# Parent .gitlab-ci.yml
trigger-service-a:
  trigger:
    include: services/service-a/.gitlab-ci.yml
    strategy: depend
  variables:
    DEPLOY_ENV: staging

trigger-service-b:
  trigger:
    include: services/service-b/.gitlab-ci.yml
    strategy: depend
  variables:
    DEPLOY_ENV: staging

Common Configuration Mistakes

  1. Not using needs for dependency ordering: Without needs, jobs wait for all previous stage jobs to complete. needs enables DAG execution where jobs start as soon as their dependencies finish.

  2. Missing cache key for npm/maven dependencies: Without caching, every pipeline downloads all dependencies from scratch. Use cache: key: ${CI_COMMIT_REF_SLUG} to cache per-branch.

  3. Overusing only/except instead of rules: GitLab recommends rules over legacy only/except syntax for more flexible conditional job execution.

  4. Storing CI variables in .gitlab-ci.yml: Secrets should be set in GitLab's CI/CD Settings > Variables, not committed to the Repository.

  5. Not cleaning up review environments: Review apps that aren't stopped when a merge request closes leave stale resources. Use on_stop and action: stop for cleanup.

Practice Questions

  1. What is the difference between stages and jobs in GitLab CI? Answer: stages defines the ordered phases of a pipeline. Each stage contains one or more jobs that run in parallel within that stage.

  2. How does the needs keyword change pipeline execution? Answer: needs creates a DAG where a job starts as soon as its listed dependencies finish, rather than waiting for all jobs in the previous stage.

  3. What is the purpose of artifacts in a job? Answer: artifacts pass files between jobs (e.g., build output to test jobs). They are stored on the GitLab server and available in dependent jobs.

  4. How do you define manual deployment approvals? Answer: Add when: manual to the deploy job. The pipeline pauses at that job until a user clicks the play button in the GitLab UI.

Challenge

Create a complete .gitlab-ci.yml for a microservice that builds and tests in parallel across Node.js 18, 20, and 22, caches dependencies, runs E2E tests against a review environment, deploys to staging on main branch pushes, deploys to production with manual approval after passing integration tests against the staging environment, and cleans up review environments on merge request close.

Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro