---
# === IDENTITY ===
id: software/devops/gitlab-ci-pipeline/2026
canonical_question: "GitLab CI reference: basic pipeline"
aliases:
  - "GitLab CI/CD pipeline configuration"
  - ".gitlab-ci.yml reference guide"
  - "GitLab CI basic pipeline setup"
  - "GitLab CI stages jobs YAML"
entity_type: software_reference
domain: software > devops > GitLab CI Pipeline
region: global
jurisdiction: global
temporal_scope: 2024-2026

# === VERIFICATION ===
last_verified: 2026-02-28
confidence: 0.93
version: 1.0
first_published: 2026-02-28

# === TEMPORAL VALIDITY ===
temporal_validity:
  status: stable
  last_breaking_change: "GitLab 17.0 removed support for only/except rules in favor of rules: keyword (2024); CI/CD components GA in 16.x"
  next_review: 2026-08-27
  change_sensitivity: medium

# === CONSTRAINTS ===
constraints:
  - "Pipeline configuration MUST be in .gitlab-ci.yml at the repository root (or custom path set in CI/CD settings)"
  - "Use rules: instead of only:/except: -- only/except is deprecated since GitLab 14.x and removed in 17.0"
  - "Never store secrets in .gitlab-ci.yml -- use CI/CD Variables (Settings → CI/CD → Variables) with masking and protection"
  - "Jobs in the same stage run in parallel by default -- use needs: for DAG-based execution if sequential execution is required within a stage"
  - "GitLab shared runners have a 3-hour job timeout by default -- configure timeout: for long-running jobs"

# === SKIP CONDITIONS ===
skip_this_unit_if:
  - condition: "Need GitHub Actions instead of GitLab CI"
    use_instead: "software/devops/github-actions-nodejs/2026"
  - condition: "Need Jenkins pipeline configuration"
    use_instead: "software/devops/jenkins-pipeline/2026"
  - condition: "Migrating from Jenkins to GitHub Actions"
    use_instead: "software/migrations/jenkins-to-github-actions/2026"

# === AGENT HINTS ===
inputs_needed:
  - key: "language"
    question: "What programming language/framework does your project use?"
    type: choice
    options: ["Node.js", "Python", "Java/Kotlin", "Go", "Ruby", "PHP", ".NET"]
  - key: "deployment_target"
    question: "Where do you deploy?"
    type: choice
    options: ["Kubernetes", "AWS", "GCP", "Azure", "Docker registry", "GitLab Pages", "On-premises"]

# === DISTRIBUTION ===
canonical_source: "https://knowledgelib.io/software/devops/gitlab-ci-pipeline/2026"
suggested_citation: "Source: knowledgelib.io -- AI Knowledge Library (verified 2026-02-28)"

# === RELATED UNITS ===
related_kos:
  depends_on: []
  related_to:
    - id: "software/devops/github-actions-nodejs/2026"
      label: "GitHub Actions Node.js CI/CD Pipeline"
    - id: "software/devops/dockerfile-best-practices/2026"
      label: "Dockerfile Best Practices"
  solves: []
  alternative_to:
    - id: "software/devops/github-actions-nodejs/2026"
      label: "GitHub Actions Node.js CI/CD Pipeline"
    - id: "software/devops/github-actions-python/2026"
      label: "GitHub Actions Python CI/CD Pipeline"
  often_confused_with:
    - id: "software/devops/github-actions-nodejs/2026"
      label: "GitHub Actions Node.js CI/CD Pipeline"

# === SOURCES ===
sources:
  - id: src1
    title: "CI/CD YAML syntax reference"
    author: GitLab
    url: https://docs.gitlab.com/ci/yaml/
    type: official_docs
    published: 2024-01-15
    reliability: authoritative
  - id: src2
    title: "Tutorial: Create and run your first GitLab CI/CD pipeline"
    author: GitLab
    url: https://docs.gitlab.com/ci/quick_start/
    type: official_docs
    published: 2024-01-15
    reliability: authoritative
  - id: src3
    title: "CI/CD pipelines"
    author: GitLab
    url: https://docs.gitlab.com/ci/pipelines/
    type: official_docs
    published: 2024-01-15
    reliability: authoritative
  - id: src4
    title: "Pipeline architecture"
    author: GitLab
    url: https://docs.gitlab.com/ee/ci/pipelines/pipeline_architectures.html
    type: official_docs
    published: 2024-01-15
    reliability: authoritative
  - id: src5
    title: "Get started with GitLab CI/CD"
    author: GitLab
    url: https://docs.gitlab.com/ci/
    type: official_docs
    published: 2024-01-15
    reliability: authoritative
  - id: src6
    title: "Writing .gitlab-ci.yml File with Examples"
    author: Spacelift
    url: https://spacelift.io/blog/gitlab-ci-yml
    type: technical_blog
    published: 2024-08-15
    reliability: moderate_high
  - id: src7
    title: "Pipeline editor"
    author: GitLab
    url: https://docs.gitlab.com/ci/pipeline_editor/
    type: official_docs
    published: 2024-01-15
    reliability: authoritative
---

# GitLab CI: Basic Pipeline

## TL;DR

- **Bottom line**: GitLab CI/CD pipelines are configured in a single `.gitlab-ci.yml` file at the repo root, defining stages (build, test, deploy) with jobs that run in parallel within each stage and sequentially across stages.
- **Key tool/command**: `.gitlab-ci.yml` with `stages:`, `script:`, `rules:`, and `image:` keywords.
- **Watch out for**: Using deprecated `only:/except:` instead of `rules:` -- the former was removed in GitLab 17.0 and produces less predictable behavior.
- **Works with**: Any language/framework, Docker-based runners (shared or self-hosted), Kubernetes executors.

## Constraints
<!-- Agents: read this section before recommending any action from this unit.
     These are hard constraints that must not be violated. -->

- Pipeline configuration MUST be in `.gitlab-ci.yml` at the repository root (or custom path set in CI/CD settings).
- Use `rules:` instead of `only:/except:` -- the latter is deprecated since GitLab 14.x and removed in 17.0.
- Never store secrets in `.gitlab-ci.yml` -- use CI/CD Variables (Settings -> CI/CD -> Variables) with masking and protection.
- Jobs in the same stage run in parallel by default -- use `needs:` for DAG-based execution if you need specific ordering.
- GitLab shared runners have a 3-hour job timeout by default -- configure `timeout:` for long-running jobs.

## Quick Reference

| Keyword | Purpose | Example |
|---|---|---|
| `stages` | Define pipeline stages (order matters) | `stages: [build, test, deploy]` |
| `image` | Docker image for job | `image: node:20-alpine` |
| `script` | Commands to execute | `script: [npm ci, npm test]` |
| `stage` | Assign job to a stage | `stage: test` |
| `rules` | Conditional job execution | `rules: - if: $CI_COMMIT_BRANCH == "main"` |
| `needs` | DAG dependencies (skip stage order) | `needs: [build_job]` |
| `artifacts` | Files to pass between jobs | `artifacts: paths: [dist/]` |
| `cache` | Persist files between pipeline runs | `cache: paths: [node_modules/]` |
| `variables` | Define environment variables | `variables: NODE_ENV: production` |
| `before_script` | Commands before every job | `before_script: [npm ci]` |
| `default` | Default settings for all jobs | `default: image: node:20` |
| `include` | Import external YAML configs | `include: - template: Auto-DevOps.gitlab-ci.yml` |
| `extends` | Inherit from another job | `extends: .base_test` |
| `environment` | Define deployment environment | `environment: name: production` |
| `when` | Job execution condition | `when: manual` or `when: on_failure` |
| `parallel` | Run job N times in parallel | `parallel: 5` |
| `services` | Linked Docker services | `services: [postgres:16]` |
| `tags` | Select specific runners | `tags: [docker, linux]` |

## Decision Tree

```
START
├── Simple app (single language)?
│   ├── YES → Three stages: build → test → deploy with shared image
│   └── NO ↓
├── Multiple services/microservices?
│   ├── YES → Use include: to split config, needs: for DAG
│   └── NO ↓
├── Need deployment approval?
│   ├── YES → Add when: manual to deploy job + environment: protection
│   └── NO ↓
├── Need matrix testing (multiple versions)?
│   ├── YES → Use parallel:matrix: with version variables
│   └── NO ↓
├── Long-running tests?
│   ├── YES → Split into parallel jobs with artifacts, use needs: for DAG
│   └── NO ↓
└── DEFAULT → Basic: stages [build, test, deploy] with rules: for branch control
```

## Step-by-Step Guide

### 1. Create the pipeline configuration file

Create `.gitlab-ci.yml` at the repository root. GitLab automatically detects and runs it. [src2]

```yaml
stages:
  - build
  - test
  - deploy

default:
  image: node:20-alpine

build:
  stage: build
  script:
    - npm ci
    - npm run build
  artifacts:
    paths:
      - dist/
    expire_in: 1 hour

test:
  stage: test
  script:
    - npm ci
    - npm test

deploy:
  stage: deploy
  script:
    - echo "Deploy to production"
  rules:
    - if: $CI_COMMIT_BRANCH == "main"
  environment:
    name: production
```

**Verify**: Push to GitLab → CI/CD → Pipelines → see pipeline with 3 stages.

### 2. Add caching for faster builds

Cache dependencies to avoid downloading them on every pipeline run. [src1]

```yaml
default:
  image: node:20-alpine
  cache:
    key:
      files:
        - package-lock.json
    paths:
      - node_modules/
```

**Verify**: Second pipeline run shows "Restoring cache" and runs faster.

### 3. Add rules for conditional execution

Use `rules:` to control when jobs run based on branch, tag, or merge request. [src1]

```yaml
test:
  stage: test
  script:
    - npm test
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
    - if: $CI_COMMIT_BRANCH == "main"
    - if: $CI_COMMIT_TAG
```

**Verify**: Create a merge request → test job runs. Push to feature branch → test does not run (unless MR exists).

### 4. Add parallel matrix testing

Run tests across multiple versions using `parallel:matrix:`. [src1]

```yaml
test:
  stage: test
  parallel:
    matrix:
      - NODE_VERSION: ['18', '20', '22']
  image: node:${NODE_VERSION}-alpine
  script:
    - npm ci
    - npm test
```

**Verify**: Pipeline shows 3 parallel test jobs, one for each Node.js version.

### 5. Add deployment with environments

Create a deploy job with environment tracking and optional manual approval. [src5]

```yaml
deploy_staging:
  stage: deploy
  script:
    - echo "Deploying to staging..."
  environment:
    name: staging
    url: https://staging.example.com
  rules:
    - if: $CI_COMMIT_BRANCH == "main"

deploy_production:
  stage: deploy
  script:
    - echo "Deploying to production..."
  environment:
    name: production
    url: https://example.com
  rules:
    - if: $CI_COMMIT_BRANCH == "main"
      when: manual
```

**Verify**: Merge to main → staging deploys automatically → production shows "play" button for manual trigger.

### 6. Use artifacts to pass data between jobs

Share build output between stages using artifacts. [src1]

```yaml
build:
  stage: build
  script:
    - npm ci
    - npm run build
  artifacts:
    paths:
      - dist/
    expire_in: 1 day

deploy:
  stage: deploy
  needs: [build]
  script:
    - ls dist/
    - echo "Deploy dist/ to server"
```

**Verify**: Deploy job can access `dist/` directory produced by build job.

## Code Examples

### Complete pipeline for Node.js project

> Full script: [complete-pipeline-for-node-js-project.yml](scripts/complete-pipeline-for-node-js-project.yml) (53 lines)

```yaml
# .gitlab-ci.yml
# Input:  Node.js project with package.json
# Output: Linted, tested, built, and deployed application
stages:
  - lint
# ... (see full script)
```

### Python pipeline with pytest and tox

> Full script: [python-pipeline-with-pytest-and-tox.yml](scripts/python-pipeline-with-pytest-and-tox.yml) (38 lines)

```yaml
# .gitlab-ci.yml
# Input:  Python project with pyproject.toml
# Output: Linted, tested, and published Python package
stages:
  - lint
# ... (see full script)
```

### Docker build and push pipeline

```yaml
# .gitlab-ci.yml
# Input:  Dockerfile at repo root
# Output: Container image in GitLab Container Registry

stages:
  - build
  - deploy

build_image:
  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
  rules:
    - if: $CI_COMMIT_BRANCH == "main"
```

## Anti-Patterns

### Wrong: Using only/except instead of rules

```yaml
# ❌ BAD — only/except is deprecated and removed in GitLab 17.0
deploy:
  script: echo "deploy"
  only:
    - main
  except:
    - tags
```

### Correct: Using rules for conditional execution

```yaml
# ✅ GOOD — rules is the current standard, more flexible and predictable
deploy:
  script: echo "deploy"
  rules:
    - if: $CI_COMMIT_BRANCH == "main"
      when: always
    - if: $CI_COMMIT_TAG
      when: never
```

### Wrong: Caching node_modules without lock file key

```yaml
# ❌ BAD — cache never invalidates when dependencies change
cache:
  paths:
    - node_modules/
```

### Correct: Keying cache to lock file hash

```yaml
# ✅ GOOD — cache invalidates when package-lock.json changes
cache:
  key:
    files:
      - package-lock.json
  paths:
    - node_modules/
```

### Wrong: Hardcoding secrets in pipeline config

```yaml
# ❌ BAD — secrets in YAML are visible to anyone with repo access
variables:
  API_KEY: "sk-12345-secret-key"
```

### Correct: Using CI/CD Variables

```yaml
# ✅ GOOD — store secrets in Settings → CI/CD → Variables (masked, protected)
deploy:
  script:
    - curl -H "Authorization: Bearer $API_KEY" https://api.example.com
  # $API_KEY is defined in CI/CD settings, not in the YAML file
```

## Common Pitfalls

- **Pipeline not triggering**: `.gitlab-ci.yml` must be on the default branch for new pipelines. Fix: merge the file to main first, then create feature branches. [src2]
- **Job timeout on shared runners**: Default 3-hour limit. Fix: set `timeout: 6h` on long-running jobs or use dedicated runners. [src1]
- **Artifacts not available in later stages**: Artifacts expire by default. Fix: set `expire_in: 1 week` or use `needs:` to pull artifacts from specific jobs. [src1]
- **Cache inconsistency**: Cache is per-runner, not global. Fix: use `cache:key:files:` for deterministic cache keys, or use distributed cache with S3. [src6]
- **Services not connecting**: Service hostname is the image name (e.g., `postgres` not `localhost`). Fix: use the service name as hostname in connection strings. [src1]
- **YAML syntax errors**: Incorrect indentation breaks the pipeline. Fix: use the GitLab Pipeline Editor (CI/CD → Editor) for real-time validation. [src7]

## Diagnostic Commands

```bash
# Validate .gitlab-ci.yml locally
gitlab-ci-lint .gitlab-ci.yml

# Check pipeline status via API
curl --header "PRIVATE-TOKEN: $GITLAB_TOKEN" \
  "https://gitlab.com/api/v4/projects/$PROJECT_ID/pipelines?per_page=5"

# List available CI/CD variables
# (In pipeline: all $CI_* variables are auto-set by GitLab)
env | grep CI_

# Check runner availability
curl --header "PRIVATE-TOKEN: $GITLAB_TOKEN" \
  "https://gitlab.com/api/v4/projects/$PROJECT_ID/runners"

# View pipeline YAML after includes are resolved
# Use the Pipeline Editor "Full configuration" tab in GitLab UI

# Debug job locally with gitlab-runner
gitlab-runner exec docker test_job
```

## Version History & Compatibility

| Version | Status | Breaking Changes | Migration Notes |
|---|---|---|---|
| GitLab 17.x | Current | Removed only/except, deprecated runner registration tokens | Use rules:, runner authentication tokens |
| GitLab 16.x | Previous | CI/CD Components GA, CI/CD Catalog | Adopt include: component for reuse |
| GitLab 15.x | EOL | Removed CI_BUILD_* variables | Use CI_JOB_* equivalents |
| `.gitlab-ci.yml` syntax | Stable | — | — |
| Shared runners | SaaS | 400 CI/CD minutes/month (Free), 10K (Premium) | Self-hosted for unlimited |

## When to Use / When Not to Use

| Use When | Don't Use When | Use Instead |
|---|---|---|
| Project is hosted on GitLab | Project is on GitHub | GitHub Actions |
| Need built-in container registry | Need complex multi-repo orchestration | Jenkins or Tekton |
| Want integrated DevSecOps (SAST, DAST) | Need 100+ parallel jobs on free tier | Self-hosted runners |
| Need environment dashboards | Simple static site deployment | GitLab Pages (simpler) |
| Self-managed GitLab instance | Vendor-agnostic CI required | Jenkins, Tekton, or Dagger |

## Important Caveats

- GitLab CI/CD minutes on SaaS are limited: 400/month (Free), 10,000/month (Premium), 50,000/month (Ultimate). Self-managed instances have no limits.
- `cache:` is stored per-runner, not globally. If your project uses multiple runners, cache may miss frequently. Use S3 or GCS distributed cache for consistency.
- `artifacts:` are uploaded to the GitLab server and count against storage quotas. Set `expire_in:` to manage storage.
- `services:` (like PostgreSQL, Redis) are only available with Docker or Kubernetes executors, not the shell executor.
- The `needs:` keyword creates a DAG (Directed Acyclic Graph) pipeline, which can run jobs out of stage order. This is powerful but can be confusing -- visualize the pipeline graph in the UI.

## Related Units

- [GitHub Actions: Node.js CI/CD Pipeline](/software/devops/github-actions-nodejs/2026)
- [GitHub Actions: Python CI/CD Pipeline](/software/devops/github-actions-python/2026)
- [Dockerfile Best Practices](/software/devops/dockerfile-best-practices/2026)
