Setting up a CI/CD Pipeline with GitLab CI

devops cicd gitlab

Jenkins dominated CI/CD for years, but managing Jenkins servers is a full-time job. GitLab CI bundles pipelines directly into your Git workflow—no separate infrastructure to maintain.

Why GitLab CI?

Basic Pipeline Structure

Create .gitlab-ci.yml in your repository root:

stages:
  - build
  - test
  - deploy

build:
  stage: build
  script:
    - npm install
    - npm run build
  artifacts:
    paths:
      - dist/

test:
  stage: test
  script:
    - npm test

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

Push to GitLab, and the pipeline runs automatically.

Understanding Stages and Jobs

Stages run sequentially. Jobs within a stage run in parallel.

stages:
  - build
  - test
  - deploy

# These run in parallel
unit-tests:
  stage: test
  script: npm run test:unit

integration-tests:
  stage: test
  script: npm run test:integration

e2e-tests:
  stage: test
  script: npm run test:e2e

Docker Integration

Most pipelines use Docker images:

image: python:3.9

stages:
  - test
  - build

test:
  stage: test
  script:
    - pip install -r requirements.txt
    - pytest

build:
  stage: build
  image: docker:20
  services:
    - docker:dind
  script:
    - docker build -t myapp:$CI_COMMIT_SHA .
    - docker push registry.example.com/myapp:$CI_COMMIT_SHA

Variables and Secrets

variables:
  DATABASE_URL: "postgres://localhost/test"

test:
  script:
    - echo $DATABASE_URL
    - echo $SECRET_KEY  # Set in GitLab UI

Set sensitive variables in Settings → CI/CD → Variables. Mark them as “protected” and “masked.”

Caching Dependencies

Speed up builds by caching:

cache:
  key: ${CI_COMMIT_REF_SLUG}
  paths:
    - node_modules/
    - .npm/

install:
  script:
    - npm ci --cache .npm

# Or per-job cache
test:
  cache:
    key: pip-cache
    paths:
      - .pip-cache/
  script:
    - pip install --cache-dir .pip-cache -r requirements.txt

Artifacts

Pass files between stages:

build:
  stage: build
  script:
    - npm run build
  artifacts:
    paths:
      - dist/
    expire_in: 1 week

deploy:
  stage: deploy
  script:
    - ls dist/  # Available from build stage
    - ./deploy.sh dist/

Branch-Specific Pipelines

# Only run on main
deploy-production:
  stage: deploy
  script: ./deploy-prod.sh
  only:
    - main

# Only on merge requests
test-mr:
  stage: test
  script: npm test
  only:
    - merge_requests

# Except specific branches
build:
  script: npm run build
  except:
    - schedules

Manual Approvals

deploy-production:
  stage: deploy
  script: ./deploy-prod.sh
  when: manual
  only:
    - main

A play button appears in the pipeline UI. Click to trigger.

Environment Tracking

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

deploy-production:
  stage: deploy
  script: ./deploy.sh production
  environment:
    name: production
    url: https://example.com
  when: manual

GitLab tracks deployments per environment and enables rollbacks.

Complete Django Example

image: python:3.9

variables:
  POSTGRES_DB: test_db
  POSTGRES_USER: postgres
  POSTGRES_PASSWORD: postgres
  DATABASE_URL: postgres://postgres:postgres@postgres:5432/test_db

stages:
  - test
  - build
  - deploy

services:
  - postgres:13

before_script:
  - pip install -r requirements.txt

test:
  stage: test
  script:
    - python manage.py migrate
    - python manage.py test
  coverage: '/TOTAL.*\s+(\d+%)$/'

lint:
  stage: test
  script:
    - pip install flake8
    - flake8 .

build-docker:
  stage: build
  image: docker:20
  services:
    - docker:dind
  script:
    - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
    - docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA .
    - docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
  only:
    - main

deploy:
  stage: deploy
  script:
    - apt-get update && apt-get install -y ssh
    - ssh deploy@server "docker pull $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA && docker-compose up -d"
  only:
    - main
  when: manual

Self-Hosted Runners

For more control or private infrastructure:

# Install runner
curl -L https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.deb.sh | sudo bash
sudo apt-get install gitlab-runner

# Register runner
sudo gitlab-runner register

Tag jobs to use specific runners:

build:
  tags:
    - docker
    - linux

Tips for Success

  1. Keep pipelines fast: Cache aggressively, parallelize tests
  2. Fail fast: Run quick checks early
  3. Use includes: Split large pipelines into files
  4. Review merge request pipelines: Catch issues before merge
  5. Monitor pipeline duration: Slow pipelines reduce productivity

Final Thoughts

GitLab CI removes the ops burden of Jenkins while providing powerful, integrated pipelines. The YAML configuration lives with your code, making pipelines reproducible and reviewable.

Start simple. Add complexity as needed. Your deployment confidence will improve dramatically.


Automate everything. Deploy with confidence.

All posts