GitLab CI/CD
Pipeline overview
push / merge request
│
├── stage: test
│ ├── test:unit (Jest tests)
│ ├── test:quality:sonarqube (SonarQube quality analysis)
│ └── test:build:docker (partial build on MR)
│
├── stage: containerize
│ └── package:image (build + push Docker image)
│
└── stage: deploy
├── deploy:int:ansible (staging, automatic)
├── deploy:dev:ansible (dev, automatic)
└── deploy:prod:manual (production, ⚠️ MANUAL)
Trigger by branch
| Branch | Tests | Build image | Deploy |
|---|---|---|---|
main |
✅ | ✅ (tag main) |
⚠️ manual → prod |
tag v1.x.x |
✅ | ✅ (tag v1.x.x) |
⚠️ manual → prod |
develop |
✅ | ✅ (tag develop) |
Auto → staging |
test-ci |
✅ | ✅ (tag test-ci) |
Auto → staging |
qa |
✅ | ✅ (tag qa) |
— |
dev |
— | ✅ (tag dev) |
Auto → dev |
| Merge Request | ✅ | ✅ (partial build) | — |
| Other branch | — | — | — |
Stages
Stage: test
test:unit
test:unit:
image: node:20-alpine
stage: test
cache:
key:
files: [package-lock.json]
paths: [.npm/]
before_script:
- npm ci --cache .npm --prefer-offline
script:
- npm test
rules:
- if: main/develop/qa/test-ci or tag
- if: merge_request_event
Runs npm test (Jest). The npm cache speeds up subsequent runs.
test:quality:sonarqube
test:quality:sonarqube:
image: sonarsource/sonar-scanner-cli:11
stage: test
script:
- sonar-scanner -Dsonar.host.url="${SONAR_HOST_URL}"
allow_failure: true # Does not block the pipeline on failure
rules:
- if: main/develop/qa/test-ci or tag
Static code analysis. allow_failure: true: a SonarQube failure does not block deployment.
test:build:docker
test:build:docker:
stage: test
script:
- docker build --target builder .
rules:
- if: merge_request_event # MRs only
Partial build (stage builder) to validate that the Dockerfile is correct. Does not produce a final image.
Stage: containerize
package:image
package:image:
stage: containerize
needs: [] # Parallel with tests
environment:
name: $DEPLOY_ENV
script:
- docker build --pull --tag ${TARGET_IMAGE} .
- docker push ${TARGET_IMAGE}
rules:
- if: develop/qa/test-ci → DEPLOY_ENV=staging
- if: dev → DEPLOY_ENV=development
- if: main or tag → DEPLOY_ENV=production
Image tag construction:
IMAGE_TAG = CI_COMMIT_TAG (if git tag) OR CI_COMMIT_REF_SLUG (branch name)
TARGET_IMAGE = ${DOCKER_REG_URL}/${CI_PROJECT_PATH}:${IMAGE_TAG}
Examples:
registry.id2real.net/racines/racines-app:develop
registry.id2real.net/racines/racines-app:main
registry.id2real.net/racines/racines-app:v1.2.3
No --build-arg AUDIO_CDN_URL: the CDN URL is injected at runtime via Docker environment variable. A single image serves all environments.
Stage: deploy
deploy:int:ansible (staging)
deploy:int:ansible:
stage: deploy
environment:
name: staging
needs:
- package:image
script:
- chmod 400 $SSHKEY_CI
- ansible-playbook -i ansible/inventory.yaml -l integration \
--private-key $SSHKEY_CI ansible/deployment_playbook.yaml
rules:
- if: develop or test-ci → automatic
deploy:dev:ansible (dev)
deploy:dev:ansible:
stage: deploy
environment:
name: development
needs:
- package:image
script:
- chmod 400 $SSHKEY_DEV
- ansible-playbook -i ansible/inventory.yaml -l dev \
--private-key $SSHKEY_DEV ansible/deployment_playbook.yaml
rules:
- if: dev → automatic
deploy:prod:manual (production)
deploy:prod:manual:
stage: deploy
environment:
name: production
when: manual # ⚠️ Does not trigger automatically
needs:
- package:image
script:
- chmod 400 $DEPLOY_SSHKEY_FILE
- ansible-playbook -i ansible/inventory.yaml -l prod \
--private-key $DEPLOY_SSHKEY_FILE ansible/deployment_playbook.yaml
rules:
- if: main or tag → available manually
Trigger:
GitLab → CI/CD → Pipelines → [pipeline on main] → deploy:prod:manual → ▶
Ansible playbook
The ansible/deployment_playbook.yaml playbook executes on the target server:
# 1. Create the deployment directory
- file: path=/docker-apps/racines state=directory
# 2. Copy the environment's docker-compose file
- copy: src=ansible/docker-compose.{env}.yaml dest=/docker-apps/racines/docker-compose.yml
# 3. Stop the current application
- shell: docker compose -f /docker-apps/racines/docker-compose.yml down
# 4. Pull the new image and restart
- shell: |
docker compose -f /docker-apps/racines/docker-compose.yml pull
docker compose -f /docker-apps/racines/docker-compose.yml up -d --force-recreate
The inventory (ansible/inventory.yaml) defines the hosts:
- integration → staging server (SSH port 9257, user ci_gitlab_deploy)
- dev → development server
- prod → production server
CI/CD variables
To define in GitLab → Settings → CI/CD → Variables:
| Variable | Type | Description | Environments |
|---|---|---|---|
DOCKER_REG_URL |
Variable | Docker registry URL | All |
DOCKER_REG_LOGIN |
Variable | Registry login | All |
DOCKER_REG_PASS |
Masked | Registry password | All |
SSHKEY_CI |
File | SSH key → staging | CI |
SSHKEY_DEV |
File | SSH key → dev | Dev |
DEPLOY_SSHKEY_FILE |
File | SSH key → production | Prod |
SONAR_HOST_URL |
Variable | SonarQube URL | All |
No
AUDIO_CDN_URLvariable in CI/CD variables — this variable is in thedocker-compose.ymlof each environment on the server, not in the pipeline.
Configuration files per environment
| File | Environment | Content |
|---|---|---|
ansible/docker-compose.integration.yaml |
Staging | Compose with staging URLs |
ansible/docker-compose.dev.yaml |
Dev | Compose with dev URLs |
docker-compose.yml |
Local dev | Compose for local development |
Each file contains the environment-specific variables (notably AUDIO_CDN_URL).
Rollback
Via GitLab pipeline
GitLab → CI/CD → Pipelines → [previous pipeline on main] → deploy:prod:manual → ▶
The previous pipeline is still available and its image is in the registry.
Manual on the server
cd /docker-apps/racines
# Identify the previous version
docker images | grep racines-app
# Force a previous image
docker compose pull app
# Modify docker-compose.yml to point to the old tag
docker compose up -d --force-recreate app
Typical durations
| Stage | Estimated duration |
|---|---|
test:unit |
1–2 min |
test:quality:sonarqube |
2–4 min |
package:image |
4–8 min (npm cache) |
deploy:int:ansible |
1–3 min |
| Total push → staging | ~8–15 min |
Diagnostics
test:unit job fails
# Reproduce locally
npm test
# or
npm test -- --verbose
package:image job fails on registry connection
# Check DOCKER_REG_* variables in GitLab CI/CD Settings
# Test manually:
docker login registry.id2real.net -u USER -p PASS
Ansible deployment fails on SSH
# Verify the SSH key is correctly configured in the File variables
# Test SSH access from the GitLab runner:
ssh -i $SSHKEY_CI ci_gitlab_deploy@staging-server -p 9257 "echo ok"
Deployed image is not the right version
# On the server:
docker inspect racines-app | grep Image
# Compare with the expected tag in the registry