18.6 GitLab CI/CD Pipeline: A Detailed Implementation Guide
GitLab is a DevOps platform where Git repository and CI/CD capabilities live in one place. A single .gitlab-ci.yml file lets you declaratively define a "test → build → deploy" pipeline.
1. GitLab CI/CD vs GitHub Actions
| Aspect | GitLab CI/CD | GitHub Actions |
|---|---|---|
| Config File | .gitlab-ci.yml (repo root) | .github/workflows/*.yml |
| Runner | GitLab Runner (self/shared) | GitHub-hosted Runner |
| Build unit | Stage → Job | Job → Step |
| Built-in Container Registry | ✅ Included | ❌ Requires integration |
| Secrets storage | Settings → CI/CD → Variables | Settings → Secrets |
2. Pipeline Overview
GitLab CI/CD defines order via an ordered Stages array; each Job declares which stage it belongs to.
# .gitlab-ci.yml
stages:
- test # Stage 1: Run tests
- build # Stage 2: Build Docker image
- deploy # Stage 3: Deploy to server
3. Stage 1: Test Job
variables:
MYSQL_ROOT_PASSWORD: testpass
MYSQL_DATABASE: testdb
test:
stage: test # One of the names declared in the stages array
image: eclipse-temurin:21-jdk # Runner executes inside this image
services:
- mysql:8.0 # Sidecar container for integration tests
variables: # Job-scoped env vars (override global variables)
SPRING_DATASOURCE_URL: "jdbc:mysql://mysql:3306/testdb" # service name = hostname
SPRING_DATASOURCE_USERNAME: root
SPRING_DATASOURCE_PASSWORD: testpass
script:
- chmod +x ./gradlew
- ./gradlew test --no-daemon
artifacts:
when: always # on_success(default) | on_failure | always
# on_success: save artifacts only on success
# on_failure: save artifacts only on failure (useful for debugging)
# always : always save
reports:
junit: build/test-results/test/*.xml # Integrates with GitLab test report UI
expire_in: 30 days # Retention period (e.g. '1 week', '30 days', never)
cache:
key: "$CI_COMMIT_REF_SLUG" # Cache key (separate cache per branch)
policy: pull-push # pull-push(default) | pull | push
# pull-push: download then upload (full cycle)
# pull : download only — read-only, useful for parallel jobs
# push : upload only — use in a dedicated cache-warmup job
paths:
- .gradle/
- build/
only: # Execution condition (prefer the more flexible `rules:` keyword)
- merge_requests # Triggered on MR create/update
- main # Triggered on push to main
- develop # Triggered on push to develop
allow_failure: false # true | false(default)
# true: pipeline won't be marked failed if this job fails
timeout: 10 minutes # Max execution time for this job (default: Runner config value)
# e.g. '1 hour', '30 minutes', '1 hour 30 minutes'
4. Stage 2: Build Image and Push to GitLab Container Registry
GitLab provides a free Container Registry per project — no Docker Hub required.
build-image:
stage: build
image: docker:26-dind
services:
- docker:26-dind
variables:
DOCKER_TLS_CERTDIR: "/certs" # TLS cert directory (/certs | "" to choose)
# "": disables TLS — insecure, not recommended
IMAGE_TAG: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
before_script: # Commands that run before every script block
# Auto-login to GitLab's built-in registry — no credentials to configure!
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
script:
- docker build -t $IMAGE_TAG .
- docker push $IMAGE_TAG
- docker tag $IMAGE_TAG $CI_REGISTRY_IMAGE:latest
- docker push $CI_REGISTRY_IMAGE:latest
needs: # Declare upstream job dependencies (DAG style)
- test # Runs only after `test` job succeeds
# Without needs:, waits for all jobs in same stage
only:
- main
retry: # Optional: automatically retry on failure
max: 2 # Max retry attempts (0–2)
when: # Conditions to retry (multiple allowed)
- runner_system_failure # Retry on Runner infrastructure issues
- stuck_or_timeout_failure # Retry on timeout or stuck job
# Other values: script_failure, api_failure, always
Auto-injected CI/CD variables (no setup required)
Variable Description $CI_REGISTRYGitLab Container Registry host $CI_REGISTRY_USERRegistry login user $CI_REGISTRY_PASSWORDRegistry login password $CI_REGISTRY_IMAGEFull path for current project's registry $CI_COMMIT_SHORT_SHAShort commit hash (8 chars) $CI_COMMIT_REF_NAMECurrent branch or tag name
5. Stage 3: Automated EC2 Deployment
deploy-production:
stage: deploy
image: alpine:3.19
before_script:
- apk add --no-cache openssh-client
- eval $(ssh-agent -s)
- echo "$EC2_PRIVATE_KEY" | tr -d '\r' | ssh-add -
- mkdir -p ~/.ssh
- chmod 700 ~/.ssh
- ssh-keyscan $EC2_HOST >> ~/.ssh/known_hosts
script:
- |
ssh ubuntu@$EC2_HOST << 'ENDSSH'
docker login -u $CI_DEPLOY_USER -p $CI_DEPLOY_PASSWORD $CI_REGISTRY
docker pull $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
docker stop todo-app || true
docker rm todo-app || true
docker run -d \
--name todo-app \
--restart=unless-stopped \
-p 8080:8080 \
-e SPRING_DATASOURCE_URL=$DB_URL \
-e SPRING_DATASOURCE_PASSWORD=$DB_PASSWORD \
$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
ENDSSH
environment:
name: production # Name shown in GitLab Environments tab (free-form string)
# e.g. production | staging | review/$CI_COMMIT_REF_SLUG
url: https://myapp.com
action: start # start(default) | stop | prepare | access | verify
# stop: marks environment as stopped (use for review cleanup)
needs:
- build-image
only:
- main
when: manual # on_success(default) | on_failure | always | manual | delayed | never
# on_success: auto-run when all previous stages succeed
# on_failure: run only when a previous stage fails (e.g. notify jobs)
# always : always run regardless of prior stage results
# manual : requires a human to click Run in the GitLab UI
# delayed : auto-runs after `start_in` duration (e.g. start_in: 1 hour)
# never : excludes job from the pipeline entirely
when: manual means a human must click the ▶ button in the GitLab pipeline dashboard to trigger deployment. Combining when: on_success for staging and when: manual for production is a safe and recommended pattern.
6. Registering CI/CD Variables
Go to Settings → CI/CD → Variables → Add variable:
| Variable | Type | Purpose |
|---|---|---|
EC2_HOST | Variable | EC2 public IP or hostname |
EC2_PRIVATE_KEY | File | EC2 SSH private key (masked) |
DB_URL | Variable (Masked) | Database connection URL |
DB_PASSWORD | Variable (Masked) | Database password |
Variables of type File are mounted as actual temp files during the job — use them like ssh-add $EC2_PRIVATE_KEY.
7. Full Pipeline Flow Summary
git push origin main
│
▼
┌──────────────┐
│ test │ ← MySQL service container → Gradle tests
│ │ JUnit XML report integrated in GitLab UI
└──────┬───────┘
│ (on success)
▼
┌──────────────┐
│ build-image │ ← Docker build → Push to GitLab Container Registry
│ (DinD) │
└──────┬───────┘
│ (on success)
▼
┌──────────────┐
│ deploy │ ← (Manual approval required in UI)
│ (manual) │ SSH → EC2: pull new image & restart container
└──────────────┘