intermediate By Mathias Paulenko

Setup CI with GitLab Pipelines

How to configure GitLab CI/CD pipelines for testing, building, and deploying applications using .gitlab-ci.yml with stages, jobs, caching, and runners.

Topics: devops

Note: This guide follows English-language naming conventions and terminology standards common in international development teams. Examples use English identifiers and comments to maximize compatibility across codebases and tooling.

Overview

GitLab CI/CD is a built-in continuous integration and deployment platform that uses a .gitlab-ci.yml file to define pipelines. Jobs run in isolated Docker containers on shared or self-hosted runners, making it easy to automate testing, building, and releasing software.

Before CI/CD pipelines, teams ran tests and deployments manually from local machines. This led to “works on my laptop” bugs, inconsistent environments, and no audit trail of what was deployed when. GitLab CI/CD solves this by codifying every step of the delivery process in version-controlled YAML.

When to Use

Use this recipe when:

  • Setting up automated testing for a GitLab-hosted project on every push or merge request.
  • Building and pushing Docker images to a registry as part of the release process.
  • Deploying to staging or production with environment-specific variables and manual approvals.
  • Running scheduled pipelines for nightly backups, dependency audits, or periodic cleanup tasks.
  • Using self-hosted runners for private infrastructure or specialized build environments.

What Works

  • Use npm ci instead of npm install in CI for reproducible builds that strictly respect package-lock.json.
  • Cache dependencies between jobs using the cache keyword to dramatically reduce build times.
  • Pin Docker image versions instead of using latest tags to ensure reproducible builds.
  • Use artifacts to pass files between stages (e.g., compiled bundles from build to deploy).
  • Set only or rules carefully to avoid running expensive deploy jobs on feature branches.
  • Use environment blocks for deployment jobs to track what is deployed and enable rollbacks.

Common Mistakes

  • Not caching node_modules causes every job to reinstall dependencies from scratch, wasting minutes per run.
  • Using only instead of rulesrules is the modern, more flexible way to control job execution.
  • Running DIND without TLS can expose the Docker socket to other jobs on the same runner.
  • Storing secrets in .gitlab-ci.yml — always use CI/CD variables from the project settings.
  • Forgetting tags for self-hosted runners causes jobs to queue indefinitely on shared runners.

Performance Tips

  1. Use needs for DAG execution. Reduce wall-clock time by starting jobs as soon as their dependencies finish:
test:
  needs: [build]  # Starts immediately after build
  1. Cache per-branch. Avoid cache collisions between branches:
cache:
  key: ${CI_COMMIT_REF_SLUG}
  paths:
    - node_modules/
  1. Use interruptible: true for MR jobs. Save runner time by canceling superseded pipelines:
test:
  interruptible: true
  1. Use small images. Reduce pull time:
# Bad: large image
image: node:20

# Good: slim image
image: node:20-slim

# Best: alpine if compatible
image: node:20-alpine
  1. Use before_script and after_script wisely. They run for every job in the file:
default:
  before_script:
    - npm ci --silent
  after_script:
    - echo "Job completed with exit code $?"

Frequently Asked Questions

What is a GitLab Runner?
A GitLab Runner is the agent that executes pipeline jobs. It can be shared, group-specific, or project-specific, and runs on Linux, Windows, macOS, or Kubernetes.
How do I cache dependencies in GitLab CI?
Use the cache keyword to persist directories like node_modules, .m2, or .pip between pipelines. Use key to scope caches per branch or lockfile.
What is the difference between stages and jobs?
Stages define execution phases (build, test, deploy) that run sequentially. Jobs are the individual tasks within a stage, which can run in parallel if they share a stage.