What GitLab CI/CD is and when it fits
GitLab CI/CD is the pipeline engine inside GitLab. You commit a .gitlab-ci.yml file, and GitLab turns it into a pipeline whose jobs run on GitLab Runners. Around that sits a container registry, environments with deployment history, artifacts, merge request widgets and security scanning, all tied to the same project and the same permissions.
That integration is the reason teams choose it. If GitLab already holds your repositories and reviews, adding delivery costs little extra tooling. Our GitLab CI consulting work usually starts when a team has outgrown its first pipeline: jobs are slow, runners are a shared bottleneck, includes have become tangled, and nobody is sure which variables hold which credentials.
How we design pipelines for production
Graph pipelines with needs and rules
The classic model runs every job in a stage before moving on. For most projects a directed graph is faster: tests start as soon as the build they depend on finishes. We combine needs for ordering with rules for deciding whether a job exists at all, so merge request pipelines stay light and release pipelines do the heavy work.
stages: [build, test, scan, deploy]
include:
- project: "platform/ci-templates"
ref: v2
file: "/templates/container-build.yml"
workflow:
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
unit-tests:
stage: test
needs: []
image: node:22
cache:
key:
files: [package-lock.json]
paths: [node_modules/]
script:
- npm ci
- npm test
deploy-production:
stage: deploy
environment: production
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
when: manual
script:
- ./deploy.sh
The cache key derives from the lock file, so it changes only when dependencies change. needs: [] lets tests begin immediately rather than waiting for earlier stages.
Shared templates and components
We centralise build, scan and deploy logic in a templates project and consume it with include. Pinning the ref to a tag lets the platform team release changes deliberately. Where your GitLab version supports CI/CD components, they give a more structured, versioned way to do the same thing. The wider approach is described in our work on reusable workflows and pipeline as code.
Environments and promotion
Environments give you deployment history and rollback targets. We make production a protected environment with named approvers, deploy the same image digest through staging first, and keep environment-scoped variables so a staging job cannot read a production secret. See environment promotion gates and approvals.
Runner architecture
Runner design decides whether GitLab CI feels fast or frustrating.
- Ephemeral over long-lived. A runner that handles one job and disappears cannot accumulate state, leaked credentials or disk bloat.
- Kubernetes executor. Each job runs as a pod, so scaling follows your cluster and you can set CPU and memory requests per job. Our Kubernetes consulting team often places the build pool on its own node group.
- Separate trust levels. Runners for protected branches and production deploys are kept apart from runners that build merge requests from less trusted contributors.
- Avoid privileged builds where possible. Docker-in-Docker needs elevated privileges. Daemonless builders reduce that exposure.
Security and access
- ID tokens for cloud access. Jobs request a signed token, and AWS or Azure trusts it for short-lived credentials, scoped by project and ref.
- Protected branches, tags and variables. Sensitive variables are available only to protected refs, and masked so they do not appear in logs.
- Least-privilege deploy tokens. Registry and repository access use scoped tokens, not personal ones.
- A secrets manager for the sensitive set. Vault or a cloud secrets service holds production credentials, retrieved at job time. See CI/CD secrets management with Vault.
- Scanning in the pipeline. Dependency, container and infrastructure checks run on merge requests, with results visible in review.
Cost and capacity
On the hosted service, compute minutes are the metered resource. On self-managed GitLab, runner infrastructure and the instance itself are. In both cases the same habits help: avoid running pipelines for documentation-only changes, cancel redundant pipelines when a branch is updated, keep images small, and share cache across jobs that need it. Autoscaled runners should scale to zero outside working hours.
Day-2 operations
Upgrade GitLab and runners on a schedule, and keep the runner version compatible with the server. For self-managed instances, test backup restoration regularly, because an untested backup is a hope, not a plan. Watch job queue time, failed-job rate and cache hit ratio, and treat a rising queue as a capacity signal rather than a developer problem.
Common failure modes
- Cache poisoning or staleness: one global cache key shared by everything. We key by lock file and scope by branch where needed.
- Include sprawl: templates that include templates that include templates. We keep the depth shallow and version each template.
- Secrets in plain variables: unprotected variables readable by any branch. We scope them to protected refs and environments.
- Shared runner starvation: every project on the same pool. We separate pools by workload.
When to choose something else
If your code is on GitHub, GitHub Actions is the lower-friction option, and moving repositories just to adopt GitLab CI is rarely justified. For a long-lived, plugin-heavy estate that you cannot easily rewrite, Jenkins may remain pragmatic. For Kubernetes delivery, we generally let GitLab CI build and test, then hand deployment to ArgoCD or Flux.
To discuss your pipelines, see our CI/CD pipeline service or get in touch.
Official documentation: docs.gitlab.com/ee/ci