Back to Blog
GitLab
CI/CD
DevOps

GitLab CI/CD in Depth: Pipelines, Runners, and Real-World Patterns

OpsQuiz TeamJuly 29, 20263 min read29 views

If you already know the general idea of CI/CD, GitLab's implementation adds its own specific vocabulary and mechanics on top - runners, stages, DAG pipelines, environments - that don't map one-to-one onto other platforms. This post assumes you know what CI/CD is broadly for and focuses specifically on how GitLab does it.

The pipeline file

Every GitLab pipeline is defined in a single .gitlab-ci.yml file at the root of your repo:

stages:
  - build
  - test
  - deploy

build-job:
  stage: build
  script:
    - npm ci
    - npm run build

test-job:
  stage: test
  script:
    - npm test

deploy-job:
  stage: deploy
  script:
    - ./deploy.sh
  only:
    - main

By default, jobs in the same stage run in parallel, and stages run sequentially - everything in build finishes before anything in test starts. That's the simple model. GitLab also supports a more flexible one, covered below.

Runners: what actually executes your jobs

A runner is the agent that picks up jobs and runs them. GitLab.com provides shared runners for free (with usage limits), but many teams register their own:

  • Shared runners - managed by GitLab, used across many projects. Convenient, but you're sharing capacity and can't customize the underlying environment much.
  • Specific runners - registered to your own project or group, running on infrastructure you control (a VM, a Kubernetes cluster, even a laptop for testing). Necessary once you need specific hardware, network access to internal systems, or predictable performance.

Runners use an executor to decide how a job actually runs - docker (each job in a fresh container, the most common choice), shell (directly on the runner's host), kubernetes (spins up a pod per job), among others. You target a specific runner using :

deploy-job:
  stage: deploy
  tags:
    - internal-network
  script:
    - ./deploy.sh

Caching vs artifacts - a common point of confusion

  • cache - speeds up future pipeline runs by reusing things like node_modules between jobs/pipelines. Not guaranteed to be there (it's best-effort), so nothing in your pipeline should depend on the cache existing.
  • artifacts - files a job explicitly produces and passes forward to later stages in the same pipeline run (a build output, a test report). Guaranteed to be available to downstream jobs that need them.
build-job:
  stage: build
  script:
    - npm run build
  artifacts:
    paths:
      - dist/
  cache:
    paths:
      - node_modules/

Mixing these up is a common source of "why did my deploy job not find the build output" bugs - that's what artifacts is for, not cache.

Stages vs DAG pipelines

The stage model above is simple but has a real cost: every job in test waits for every job in build to finish, even if it only actually depends on one of them. GitLab's needs keyword lets you express direct dependencies instead, forming a directed acyclic graph (DAG) where jobs start as soon as their specific dependencies are done, regardless of stage:

test-frontend:
  stage: test
  needs: [build-frontend]
  script:
    - npm test

test-backend:
  stage: test
  needs: [build-backend]
  script:
    - pytest

If build-backend finishes first, test-backend can start immediately rather than waiting on build-frontend too - for pipelines with many independent components, this can meaningfully cut total pipeline time.

Merge request pipelines and environments

By default, pipelines run on every push to a branch. GitLab can also run merge request pipelines specifically in the context of a merge request (useful for running checks that only make sense pre-merge, or showing pipeline status directly on the MR). Combined with environments, you can track exactly what's deployed where:

deploy-staging:
  stage: deploy
  script:
    - ./deploy.sh staging
  environment:
    name: staging
    url: https://staging.example.com

GitLab then shows a deployment history per environment, and can auto-create review apps - a temporary, isolated deployment per merge request - which is genuinely useful for reviewing frontend changes without needing to run the branch locally.

Where GitLab CI/CD fits with GitOps

Everything above is about building and testing code and deciding when to trigger a deploy - GitLab pipelines can absolutely run kubectl apply or helm upgrade directly in a deploy-job. Some teams take a different approach for the deployment step specifically: instead of the pipeline pushing changes directly to a cluster, it just updates a Git repository, and a separate GitOps controller running inside the cluster pulls and applies those changes. That's a meaningfully different model worth understanding on its own - see our GitOps with Flux guide for what that looks like in practice.

Ready to test what you've learned? Try the CI/CD quiz on OpsQuiz.

    Welcome to OpsQuiz!

    Real scenario-based DevOps questions, hands-on practice, and clear explanations for every answer.