Setting up a CI/CD Pipeline with GitLab CI
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?
- Integrated: Lives in your repository, not a separate system
- YAML-based: Pipeline as code, version controlled
- Runners: Run on GitLab’s infrastructure or your own
- Container-native: First-class Docker support
- Free tier: Generous free minutes for private repos
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
- Keep pipelines fast: Cache aggressively, parallelize tests
- Fail fast: Run quick checks early
- Use includes: Split large pipelines into files
- Review merge request pipelines: Catch issues before merge
- 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.