CI/CD avec GitLab : déployer un backend et un frontend en architecture microservices
Prérequis
Avant de commencer, tu as besoin de :
- Un compte GitLab
- Un serveur Linux (Ubuntu/Debian) avec Docker installé
- Un projet à déployer
- Un nom de domaine (optionnel mais recommandé)
1. L'architecture
Dans ce guide, on déploie deux microservices indépendants :
- Un backend Python/FastAPI qui expose une API sur le port 8000
- Un frontend Next.js qui consomme cette API sur le port 3000
Les deux communiquent via Nginx qui joue le rôle de reverse proxy. Chaque microservice a son propre pipeline CI/CD indépendant dans GitLab.
2. Installer le GitLab Runner
Sur ton serveur :
curl -L https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.deb.sh | sudo bash
sudo apt-get install gitlab-runner
Si l'installation via apt échoue, télécharge directement le binaire :
sudo curl -L --output /usr/local/bin/gitlab-runner \
"https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-linux-amd64"
sudo chmod +x /usr/local/bin/gitlab-runner
sudo useradd --comment 'GitLab Runner' --create-home gitlab-runner --shell /bin/bash
sudo gitlab-runner install --user=gitlab-runner --working-directory=/home/gitlab-runner
sudo gitlab-runner start
3. Enregistrer le Runner sur GitLab
Dans GitLab, va dans Project → Settings → CI/CD → Runners → Create project runner. Donne un tag à ton runner, puis sur le serveur :
sudo gitlab-runner register
Réponds aux questions :
| Question | Réponse |
|---|---|
| GitLab instance URL | https://gitlab.com/ |
| Registration token | glrt-xxxx (depuis GitLab) |
| Executor | docker |
| Default image | docker:latest |
Choisis toujours docker comme executor. Avec shell, les images spécifiées dans le yml sont ignorées et les jobs s'exécutent directement sur la machine hôte sans isolation.
4. Configurer le Runner pour Docker
Après l'enregistrement, modifie /etc/gitlab-runner/config.toml :
sudo nano /etc/gitlab-runner/config.toml
Dans la section [runners.docker] :
privileged = true
volumes = ["/var/run/docker.sock:/var/run/docker.sock", "/cache"]
privileged = true permet au container d'exécuter des commandes Docker. Le volume docker.sock monte le socket Docker de l'hôte dans le container pour éviter de démarrer un daemon supplémentaire.
sudo gitlab-runner restart
5. Configurer les variables CI/CD
Dans GitLab → Settings → CI/CD → Variables, ajoute les mêmes variables dans chaque projet :
| Variable | Description | Options |
|---|---|---|
SSH_PRIVATE_KEY | Clé privée SSH encodée en base64 | Protected |
SERVER_HOST | IP du serveur | Protected + Masked |
SERVER_USER | Utilisateur SSH | Protected |
Génère et encode la clé SSH sur le serveur :
ssh-keygen -t ed25519 -C "gitlab-ci" -f ~/.ssh/gitlab-ci
cat ~/.ssh/gitlab-ci.pub >> ~/.ssh/authorized_keys
cat ~/.ssh/gitlab-ci | base64 -w 0
L'encodage base64 est indispensable car une clé SSH contient des sauts de ligne qui cassent la variable GitLab si elle est copiée directement.
6. Le pipeline CI/CD du backend Python/FastAPI
variables:
IMAGE_NAME: $CI_REGISTRY_IMAGE/backend
IMAGE_TAG: $CI_COMMIT_REF_SLUG
stages:
- test
- build
- deploy
test:
stage: test
tags:
- mon-runner
image: python:3.11-slim
script:
- pip install --no-cache-dir -r requirements.txt
- python -m py_compile app/main.py
- echo "Tests OK"
rules:
- if: '$CI_PIPELINE_SOURCE == "push"'
build:
stage: build
tags:
- mon-runner
image: docker:24
variables:
DOCKER_HOST: unix:///var/run/docker.sock
DOCKER_TLS_CERTDIR: ""
script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
- docker build -t $IMAGE_NAME:$IMAGE_TAG .
- |
if [ "$CI_COMMIT_REF_NAME" == "main" ]; then
docker tag $IMAGE_NAME:$IMAGE_TAG $IMAGE_NAME:latest
docker push $IMAGE_NAME:latest
fi
- docker push $IMAGE_NAME:$IMAGE_TAG
needs: ["test"]
rules:
- if: '$CI_PIPELINE_SOURCE == "push"'
deploy:
stage: deploy
tags:
- mon-runner
image: alpine:latest
script:
- apk add --no-cache openssh-client
- mkdir -p ~/.ssh
- echo "$SSH_PRIVATE_KEY" | base64 -d > ~/.ssh/id_rsa
- chmod 600 ~/.ssh/id_rsa
- ssh-keyscan -H $SERVER_HOST >> ~/.ssh/known_hosts
- |
ssh $SERVER_USER@$SERVER_HOST "
docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY &&
docker pull $IMAGE_NAME:latest &&
docker stop mon-backend || true ;
docker rm mon-backend || true ;
docker run -d \
--name mon-backend \
--env-file /home/app/.env \
-p 8000:8000 \
--restart always \
$IMAGE_NAME:latest &&
sleep 5 &&
curl -f http://localhost:8000/
"
needs: ["build"]
rules:
- if: '$CI_COMMIT_REF_NAME == "main"'
7. Le Dockerfile du backend
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8000
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
8. Le pipeline CI/CD du frontend Next.js
variables:
IMAGE_NAME: $CI_REGISTRY_IMAGE/frontend
IMAGE_TAG: $CI_COMMIT_REF_SLUG
stages:
- test
- build
- deploy
test:
stage: test
tags:
- mon-runner
image: node:20-alpine
script:
- npm install
- npm run lint
rules:
- if: '$CI_PIPELINE_SOURCE == "push"'
build:
stage: build
tags:
- mon-runner
image: docker:24
variables:
DOCKER_HOST: unix:///var/run/docker.sock
DOCKER_TLS_CERTDIR: ""
script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
- docker build --build-arg NEXT_PUBLIC_API_URL=https://mondomaine.com/api -t $IMAGE_NAME:$IMAGE_TAG .
- |
if [ "$CI_COMMIT_REF_NAME" == "main" ]; then
docker tag $IMAGE_NAME:$IMAGE_TAG $IMAGE_NAME:latest
docker push $IMAGE_NAME:latest
fi
- docker push $IMAGE_NAME:$IMAGE_TAG
needs: ["test"]
rules:
- if: '$CI_PIPELINE_SOURCE == "push"'
deploy:
stage: deploy
tags:
- mon-runner
image: alpine:latest
script:
- apk add --no-cache openssh-client
- mkdir -p ~/.ssh
- echo "$SSH_PRIVATE_KEY" | base64 -d > ~/.ssh/id_rsa
- chmod 600 ~/.ssh/id_rsa
- ssh-keyscan -H $SERVER_HOST >> ~/.ssh/known_hosts
- |
ssh $SERVER_USER@$SERVER_HOST "
docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY &&
docker pull $IMAGE_NAME:latest &&
docker stop mon-frontend || true ;
docker rm mon-frontend || true ;
docker run -d \
--name mon-frontend \
-p 3000:3000 \
--restart always \
$IMAGE_NAME:latest
"
needs: ["build"]
rules:
- if: '$CI_COMMIT_REF_NAME == "main"'
9. Le Dockerfile du frontend Next.js
FROM node:20-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN npm install
FROM node:20-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
ARG NEXT_PUBLIC_API_URL
ENV NEXT_PUBLIC_API_URL=$NEXT_PUBLIC_API_URL
RUN npm run build
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/public ./public
EXPOSE 3000
CMD ["node", "server.js"]
Et dans next.config.ts :
const nextConfig: NextConfig = {
output: "standalone",
};
10. Communication entre les deux microservices via Nginx
Le frontend et le backend tournent dans deux containers séparés sur le même serveur. Ils communiquent via Nginx qui joue le rôle de reverse proxy.
server {
listen 80 default_server;
server_name _;
return 444;
}
server {
listen 80;
server_name mondomaine.com www.mondomaine.com;
location / {
proxy_pass http://localhost:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
}
location /api/ {
proxy_pass http://localhost:8000/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
Toute requête vers mondomaine.com/ est redirigée vers le frontend. Toute requête vers mondomaine.com/api/ est redirigée vers le backend.
Du côté du frontend, la variable NEXT_PUBLIC_API_URL est définie à la compilation avec --build-arg. Les variables NEXT_PUBLIC_* dans Next.js sont intégrées au moment du build et non à l'exécution. Sans cette variable, le frontend continuerait à appeler localhost:8000 au lieu du vrai domaine en production.
11. Résultat
Une fois tout configuré, le workflow devient :
- Tu modifies le code d'un microservice et tu fais
git push origin main - GitLab déclenche le pipeline de ce microservice uniquement
- Le Runner exécute les tests
- Si les tests passent, l'image Docker est construite et poussée vers le Registry
- Le serveur télécharge la nouvelle image et redémarre le container
- Le microservice est mis à jour en production sans toucher à l'autre
Conclusion
Cette architecture permet de faire évoluer chaque microservice indépendamment. Le backend peut être mis à jour sans redéployer le frontend et vice versa. C'est l'un des principaux avantages de l'architecture microservices par rapport à une application monolithique.