Pipeline CI/CD pour une architecture microservices : API Flask -passerelle Node.js-
Déployer une application à la main, c’est lent, répétitif et truffé d’occasions de se tromper. Un oubli, une mauvaise version copiée, et le…
Pipeline CI/CD pour une architecture microservices : API Flask -passerelle Node.js- Reverse proxy Nginx

Architecture complète
Déployer une application à la main, c’est lent, répétitif et truffé d’occasions de se tromper. Un oubli, une mauvaise version copiée, et le serveur tombe. J’ai voulu supprimer ce risque en automatisant tout : à chaque git push, le code est construit, testé, publié puis déployé, sans que je touche au serveur.
Dans cet article, je détaille l’architecture que j’ai choisie, le pipeline GitLab CI qui se charge d’automatiser, et surtout les trois bugs bien réels sur lesquels je me suis cogné — parce que c’est en les résolvant que j’ai le plus appris.
L’architecture en un coup d’œil

Architecture
L’application est volontairement simple (une mini-API de citations), car l’intérêt du projet n’est pas l’appli en elle-même mais la chaîne qui la déploie. Elle est composée de trois services :
- Flask — l’API métier en Python, qui sert les données.
- Node.js (Express) — une passerelle qui sert la page et appelle Flask.
- Nginx — un reverse proxy, point d’entrée unique de tout le système.
Pourquoi un reverse proxy ?
Sans Nginx, il faudrait exposer les ports de Node et de Flask vers l’extérieur : deux portes d’entrée à surveiller. Avec Nginx devant, un seul port est ouvert sur la machine : le 80. Node et Flask deviennent invisibles depuis Internet ; ils ne sont joignables que par Nginx, sur le réseau interne de Docker.
Conteneuriser chaque service avec Docker
Chaque service a son propre Dockerfile. Pour Flask, on copie les dépendances avant le code, afin de profiter du cache de couches Docker (les dépendances ne sont réinstallées que si requirements.txt change) :
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app.py .
EXPOSE 5000
CMD ["gunicorn", "--bind", "0.0.0.0:5000", "--workers", "2", "app:app"]
Le Dockerfile de Node ensuite. On copie d’abord lepackage*.json, puis le code, en utilisant la version d’image alpine, beaucoup plus légère.
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install
# Code + tests
COPY src ./src
COPY tests ./tests
EXPOSE 3000
CMD ["npm", "start"]
Pour Nginx, pas besoin de créer son Dockerfile car on n’a pas à reconstruire son image, on va juste la télécharger depuis DockerHub durant chaque tache où on en aura besoin.
Orchestrer avec Docker-compose
Le docker-compose.yml décrit les trois conteneurs et le réseau qui les relie.
services:
nginx:
image: nginx:alpine
ports:
- "80:80" # Le seul port qui sera expose dans l'hote EC2
volumes:
- /etc/nginx/:/etc/nginx/
depends_on: [node, flask]
networks: [app-net]
restart: unless-stopped
node:
image: manuelkevin4real/node-api:latest
environment:
- FLASK_URL=http://flask:5000
expose: ["3000"] # interne seulement
depends_on: [flask]
networks: [app-net]
restart: unless-stopped
flask:
image: manuelkevin4real/api-flask:latest
expose: ["5000"] # interne seulement
networks: [app-net]
restart: unless-stopped
networks:
app-net:
driver: bridge
Les services Flask et Node ne sont exposés qu’en interne, contrairement au service Nginx qui sera exposé dans l’hôte EC2 (mapping 80:80).
Les 3 services communiquent entre eux grâce au type de réseau Bridge dans Docker (communication entre conteneurs).
Côté Nginx, le routage par URL tient en quelques lignes. Un aperçu du fichier nginx.conf :
events {}
http {
# Cible les services Flask et Node
upstream node_gateway { server node:3000; }
upstream flask_api { server flask:5000; }
server {
listen 80; # Nginx ecoute sur le port 80
# Tout ce qui commence par /api/ passe part Flask
location /api/ {
proxy_pass http://flask_api/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
# tout le reste va vers Node
location / {
proxy_pass http://node_gateway;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
}
La pipeline CI/CD : le cœur du projet

Tout se joue dans le .gitlab-ci.yml, organisé en trois stages ( grandes étapes ) qui s'enchaînent comme suit :
- Build-test : Ce stage est constitué de 2 jobs, qui sont en faite des blocs de taches qui s’exécuteront en parallèle tout au long du stage. Ici, chaque job va construire l’image de chaque service ( Flask et Node ), lancer chacun un conteneur à partir de ces images afin d’y effectuer à l’intérieur leurs tests respectifs.
- Push : Etant donné que les stages sont exécutés de manière séquentielle, si les tests de l’étape Build-test sont bons, alors nous allons charger les images construites déjà, les tagguer, puis les pousser vers DockerHub. Ces étapes seront également divisées en 2 jobs.
- Deploy : Ici, nous utiliserons un seul job. Contrairement aux autres étapes, on commence avec le before-script qui est en fait un script qui s’exécute avant même de passer au script du stage. On va installer un client ssh (Openssh en occurrence) dans le runner gitlab (la machine qui effectue le job), on charge la clé SSH dans le runner et on se connecte à l’instance EC2 grâce à elle. Dans le script lui même, on exécute les taches ci-après : — Copier les fichiers nginx.conf et docker-compose.yml dans EC2 grâce au protocole SCP ; — Déplacer ces fichiers dans les répertoires appropriés et y configurant les permissions nécessaires pour les exécuter ; — Se connecter à Dockerhub pour télécharger les images des services Flask et Node ; —Lancer simultanément tous les services avec les commandes de Docker compose.
Un aperçu du fichier .gitlab-ci.yml :
stages:
- build-test
- push
- deploy
variables:
IMAGE_NODE: "image-node:$CI_COMMIT_SHORT_SHA"
IMAGE_FLK: "image-flk:$CI_COMMIT_SHORT_SHA"
build-flask:
stage: build-test
image: docker:27
services:
- docker:27-dind
variables:
DOCKER_TLS_CERTDIR: "/certs"
script:
- docker build -t "$IMAGE_FLK" ./flask-api
- docker run --rm "$IMAGE_FLK" pytest -q
- docker save "$IMAGE_FLK" -o image-flask.tar
artifacts:
paths:
- image-flask.tar
when: on_success
expire_in: 30 days
build-node:
stage: build-test
image: docker:27
services:
- docker:27-dind
variables:
DOCKER_TLS_CERTDIR: "/certs"
script:
- docker build -t "$IMAGE_NODE" ./node-gateway
- docker run --rm "$IMAGE_NODE" npm test
- docker save "$IMAGE_NODE" -o image-node.tar
artifacts:
paths:
- image-node.tar
when: on_success
expire_in: 30 days
push-flask:
stage: push
image: docker:27
services:
- docker:27-dind
variables:
DOCKER_TLS_CERTDIR: "/certs"
script:
- docker load -i image-flask.tar
- docker login -u "$DEPLOY_TOKEN_USER" -p "$DEPLOY_TOKEN_PASSWORD"
- docker tag "$IMAGE_FLK" "manuelkevin4real/api-flask:latest"
- docker push manuelkevin4real/api-flask:latest
only:
- main
push-node:
stage: push
image: docker:27
services:
- docker:27-dind
variables:
DOCKER_TLS_CERTDIR: "/certs"
script:
- docker load -i image-node.tar
- docker login -u "$DEPLOY_TOKEN_USER" -p "$DEPLOY_TOKEN_PASSWORD"
- docker tag "$IMAGE_NODE" "manuelkevin4real/node-api:latest"
- docker push manuelkevin4real/node-api:latest
only:
- main
deploy-ec2:
stage: deploy
image: alpine:3.19
when: manual
before_script:
- apk add --no-cache openssh-client # installer ssh
- eval $(ssh-agent -s) #demarrer l'agent ssh
- echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add - #charge la cle privee dans l'agent
- mkdir -p ~/.ssh && chmod 700 ~/.ssh #cree le dossier .ssh et restreint les permissions
# Ajouter l'empreinte de l'hôte pour éviter le prompt de confirmation
- ssh-keyscan "$EC2_HOST" >> ~/.ssh/known_hosts 2>/dev/null # Ajouter l'empreinte de l'hôte
script:
- |
ssh "ubuntu@$EC2_HOST" "
set -e
"
- scp nginx/nginx.conf "ubuntu@$EC2_HOST:/home/ubuntu/"
- scp docker-compose.yml "ubuntu@$EC2_HOST:/home/ubuntu/"
- |
ssh "ubuntu@$EC2_HOST" "
set -e
sudo mv /home/ubuntu/nginx.conf /etc/nginx/
sudo mv /home/ubuntu/docker-compose.yml /etc/
sudo chown root:root /etc/nginx/nginx.conf
sudo chown root:root /etc/docker-compose.yml
cd /etc/
echo pwd
docker login -u '$DEPLOY_TOKEN_USER' -p '$DEPLOY_TOKEN_PASSWORD'
docker pull manuelkevin4real/node-api:latest
docker pull manuelkevin4real/api-flask:latest
docker compose down || true
docker compose pull && docker compose up -d
"
only:
- main
NB: Les variables environnement sensibles c’est-à-dire les secrets comme les clés SSH, les crédentials, les tokens, clés API sont à configurer au préalable dans GitLab CI > Settings > CI/CD > variables. Cela permet de masquer et protéger ces secrets dans les logs.
L’environnement de destination : AWS EC2
C’est ici que le projet sera déployé au final. Pour cela, il est nécessaire de le configurer en accordance avec notre application.
Pour ce projet, j’ai choisi une image Ubuntu, dans laquelle on installera Docker et Docker-compose. Deux solutions se présentent : soit les installer manuellement ( apt-get update && apt-get install Docker Docker-compose), soit utiliser un script bash user-data ( un script que vous même configurez, qui va s’exécuter lors de la création de votre instance EC2 dans AWS).
Les 2 erreurs qui m’ont le plus appris
Voici la partie que je voulais vraiment écrire. Aucun tutoriel ne montre ça, parce que tout le monde affiche un chemin parfait. Le mien ne l’était pas.
Erreur 1 — Le port 80 était déjà utilisé dans l’instance.
En testant le fonctionnement des services, je me suis rendu compte que le port 80 était déjà utilisé dans l’instance car pour des raisons de tests, j’ai installé une première fois Nginx dans cette instance. Or il faut savoir que Docker-compose up lance des conteneurs embarqués avec leur propre configuration et leurs ports. Par conséquent, je me suis retrouvé avec Nginx dans mon instance connecté au port 80 et le service Nginx de mon docker-compose qui exposait également son service au port 80 de l’instance EC2. D'où la collision des services. En ce qui vous concerne, vous ne le rencontrerez pas en faisant ce tutoriel mais juste pour vous tenir avisé du fait de se rassurer de n’avoir qu’un seul service exposé vers le port 80 de votre instance.
Erreur 2 — Le Docker compose down nettoyait la mauvaise stack
Dans l’optique d’arrêter et remplacer les images modifiées lors de chaque commit dans l’instance EC2, il s’avère que j’avais un problème de répertoire, c’est-a-dire, la commande Docker-compose up /down ne s’ exécute que dans le répertoire dans lequel est présent le fichier Docker-compose.yml or dans mon cas, j’avais ce fichier présent dans deux répertoires ( encore pour des raisons de tests mais important). Ce qui démarrait et stoppait le mauvais service. Alors ce qu’il faut faire c’est créer un répertoire unique ou sera copié le fichier Docker-compose.yml .
Dans ce cas, la solution de déploiement est plus complexe car Docker-compose ne prend pas en compte les autres types de déploiement (Canary, rolling update,…) comme on en a avec d’autres solutions plus adéquates comme Kubernetes.
Le résultat
A la fin, on obtient une architecture complète automatisée du Commit au cloud, en passant par le pipeline CI/CD. Ce projet m’a permis d’asseoir mes connaissances sur GitLab CI, les pipelines CI/CD et leur fonctionnement, le réseau grâce au reverse proxy sur Nginx, les API REST avec Node.js et Python Flask.

Interface principale

Interface Pipeline CI/CD dans GitLab CI

Le repo dans GitLab CI
Lien du dépôt GitLab : https://gitlab.com/ManuelKevin4real/projet-node-flask
Merci pour votre attention !
메타데이터
- post_id
- 9fb9f65d2cff
- slug
- pipeline-ci-cd-pour-une-architecture-microservices-api-flask-passerelle-node-js-9fb9f65d2cff
- url
- https://medium.com/@manuelkevin4real/pipeline-ci-cd-pour-une-architecture-microservices-api-flask-passerelle-node-js-9fb9f65d2cff
- canonical_url
- https://medium.com/@manuelkevin4real/pipeline-ci-cd-pour-une-architecture-microservices-api-flask-passerelle-node-js-9fb9f65d2cff
- author_url
- https://medium.com/@manuelkevin4real
- status
- ok
- fetched_at
- 2026-06-15 20:49:13