GitLab CI/CD — Complete Pipeline Guide for 2026
Advertisement
Introduction
Why This Matters
GitLab CI/CD is one of the most feature-rich pipeline systems available today — and it lives directly inside the same platform as your source code, merge requests, and container registry. Unlike external CI tools, everything from code review to deployment tracking to environment management is in one place.
For teams already on GitLab, leveraging its built-in CI eliminates the overhead of integrating and securing a separate CI service. For teams evaluating options, GitLab CI offers powerful features like review apps, protected environments, and built-in Kubernetes integration that would require multiple tools elsewhere.
Anatomy of .gitlab-ci.yml
Every GitLab pipeline is defined in a .gitlab-ci.yml file at the root of your repository. Here is a production-ready starting point for a Node.js application:
image: node:20
stages:
- install
- lint
- test
- build
- deploy
variables:
NODE_ENV: test
cache:
key:
files:
- package-lock.json
paths:
- node_modules/
install:
stage: install
script:
- npm ci
artifacts:
paths:
- node_modules/
expire_in: 1 hour
lint:
stage: lint
script:
- npm run lint
test:
stage: test
script:
- npm test -- --coverage
coverage: '/Lines\s*:\s*(\d+\.?\d*)%/'
artifacts:
reports:
coverage_report:
coverage_format: cobertura
path: coverage/cobertura-coverage.xml
build:
stage: build
script:
- npm run build
artifacts:
paths:
- dist/
expire_in: 1 week
only:
- main
- tagsThe cache block keyed on package-lock.json ensures the cache is invalidated automatically when dependencies change. Using artifacts to pass node_modules/ between stages is more reliable than relying purely on caching in single-pipeline runs.
Running Services: Databases and Redis
Integration tests often need a real database or cache. GitLab CI launches linked Docker containers as services:
test:integration:
stage: test
image: node:20
services:
- postgres:16
- redis:7
variables:
POSTGRES_DB: testdb
POSTGRES_USER: testuser
POSTGRES_PASSWORD: testpassword
DATABASE_URL: postgresql://testuser:testpassword@postgres/testdb
REDIS_URL: redis://redis:6379
script:
- npm ci
- npm run db:migrate
- npm run test:integrationServices are reachable by their image name as the hostname (postgres, redis). They start before the job script runs and are torn down after.
Parallel Jobs and Matrix Builds
Jobs in the same stage run in parallel by default. To test across multiple Node versions, use the parallel:matrix keyword:
test:
stage: test
parallel:
matrix:
- NODE_VERSION: ['18', '20', '22']
image: node:$NODE_VERSION
script:
- npm ci
- npm testThis spawns three concurrent test jobs, one per Node version, without duplicating YAML. Results from all matrix jobs must pass before the next stage begins.
Environments and Protected Deployments
GitLab tracks deployments per environment, showing history and allowing rollbacks. Protected environments require approvals before deployment proceeds:
deploy:staging:
stage: deploy
environment:
name: staging
url: https://staging.myapp.com
script:
- ./deploy.sh staging
only:
- develop
deploy:production:
stage: deploy
environment:
name: production
url: https://myapp.com
when: manual
script:
- ./deploy.sh production
only:
- mainSetting when: manual on production creates a one-click deployment gate in the GitLab UI. This is the simplest form of deployment approval without external tooling.
Building and Pushing Docker Images
GitLab includes a built-in container registry (registry.gitlab.com). Push images without any external registry credentials:
build:docker:
stage: build
image: docker:24
services:
- docker:24-dind
variables:
DOCKER_TLS_CERTDIR: '/certs'
before_script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
script:
- docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA .
- docker build -t $CI_REGISTRY_IMAGE:latest .
- docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
- docker push $CI_REGISTRY_IMAGE:latest
only:
- mainCI_REGISTRY, CI_REGISTRY_USER, CI_REGISTRY_PASSWORD, and CI_REGISTRY_IMAGE are predefined GitLab CI variables — no manual secret configuration needed.
Common Mistakes
- Not setting
cache.keyproperly — Without a meaningful cache key, jobs share stale caches across branches, causing flaky builds. - Using
only/exceptinstead ofrules—onlyandexceptare legacy syntax. Theruleskeyword is more expressive and should be used in new pipelines. - Running all stages on every branch — Use
rulesor branch filters to skip expensive build and deploy stages on feature branches. - Long-running jobs with no timeout — Set
timeoutper job to prevent stuck runners from consuming quota indefinitely. - Hardcoding secrets in
.gitlab-ci.yml— Store all sensitive values in GitLab CI/CD Variables (masked + protected) and reference them as environment variables.
Best Practices
- Define
default:image and cache once at the top of your file rather than repeating it in every job. - Use
needs:to create directed acyclic graphs (DAGs) — this lets later-stage jobs start as soon as their dependencies finish, not after the entire stage. - Enable
retry: 2on flaky network-dependent jobs to automatically retry transient failures. - Use include files and templates from GitLab's CI template library to avoid reinventing common jobs.
- Always set
expire_inon artifacts to prevent disk usage from growing unboundedly over time.
Key Takeaways
- GitLab CI/CD pipelines are defined entirely in
.gitlab-ci.ymland run natively without external tools. - Services (Postgres, Redis, etc.) are linked Docker containers that launch before job scripts and are reachable by image name as hostname.
parallel.matrixruns jobs across multiple variable combinations (e.g., Node versions) concurrently without duplicating YAML.- GitLab predefined variables (
CI_REGISTRY_IMAGE,CI_COMMIT_SHORT_SHA, etc.) eliminate the need for manual secret setup for GitLab-native operations. when: manualon deploy jobs creates a human-in-the-loop gate visible in the GitLab UI before production deployments.- The
needs:keyword enables DAG-based pipelines where downstream jobs start as soon as their specific dependencies finish, not after the entire stage. - Always set artifact
expire_into control disk usage — artifacts without expiry accumulate indefinitely on self-hosted runners. - Use
rules:instead of legacyonly/exceptfor fine-grained control over when jobs run.
Advertisement