Chapitre 13.2 — Déployer par git
⏱️ TL;DR — Avant d’automatiser, il faut fiabiliser le geste manuel. On transforme la checklist mentale de déploiement en un script unique,
deploy.sh, versionné avec le projet : il enchaînegit pull,npm ci,npm run build, les migrations de base, puissystemctl reload/restartdu service. On le blinde avecset -euo pipefailpour qu’il s’arrête au premier échec au lieu de continuer sur une prod à moitié cassée, et on le veut idempotent (le relancer deux fois ne casse rien). On le lance en SSH d’une seule commande. On survole ensuite les git hooks (post-receivesur un dépôt bare) qui permettent qu’un simplegit pushdéclenche le déploiement. Enfin, on nomme les limites de cette approche — build sur le serveur, pas de tests — que GitHub Actions (13.3) viendra lever.
🎯 Objectifs
- Écrire un script de déploiement
deploy.shclair, commenté et versionné. - Comprendre et appliquer
set -euo pipefailet le principe d’idempotence. - Lancer un déploiement d’une seule commande SSH, sans se connecter interactivement.
- Comprendre le principe des git hooks (
post-receivesur un dépôt bare) pour ungit pushqui déploie. - Nommer les limites du build-sur-serveur et savoir pourquoi on passera à une CI.
Du geste manuel au script
Au chapitre précédent, on a listé les sept étapes du déploiement à la main. Le problème n’était pas les étapes elles-mêmes, mais le fait qu’un humain les enchaîne de mémoire. La solution est directe : on les écrit une fois pour toutes, dans un fichier, et c’est ce fichier qu’on exécute.
Ce fichier, deploy.sh, vit dans le dépôt de ton projet. Il est versionné : il évolue avec le code, tout le monde voit ce qu’il fait, et son historique est traçable comme le reste.
#!/usr/bin/env bash
# deploy.sh — déploiement de FormaCampus sur le VPS.
# À lancer depuis le dossier du projet sur le serveur.
set -euo pipefail
echo "==> Récupération du code"
git fetch --all
git reset --hard origin/main # on force l'état exact de main (pas de merge surprise)
echo "==> Installation des dépendances (reproductible)"
npm ci # ci = install strict depuis package-lock, pas install
echo "==> Migrations de base de données"
npm run migrate # applique les migrations EN ATTENTE (idempotent)
echo "==> Build de production"
npm run build # produit le build figé et optimisé
echo "==> Redémarrage du service"
sudo systemctl reload formacampus-web || sudo systemctl restart formacampus-web
echo "==> Vérification"
sleep 2
curl -fsS -o /dev/null https://formacampus.fr && echo "OK, le site répond"Décortiquons les choix qui comptent :
git reset --hard origin/mainplutôt quegit pull. Ungit pullpeut échouer sur un conflit ou créer un merge inattendu si le serveur a des modifications locales traînantes.reset --hardforce l’état exact de la branche de prod : le serveur devient une copie fidèle deorigin/main, sans surprise. (On assume que personne ne modifie le code à la main sur le serveur — c’est une règle.)npm ciet pasnpm install.ciinstalle exactement les versions figées danspackage-lock.json, dans unnode_modulespropre. C’est le geste reproductible : deuxnpm ciproduisent le même résultat, contrairement àinstallqui peut faire évoluer le lock.- Migrations avant le build/redémarrage — l’ordre dépend de ta stratégie (on l’affine au 13.4), mais l’idée est que la base doit être dans le bon schéma avant que le nouveau code ne tourne.
reloadavec repli surrestart—reloadrecharge sans couper (quand le service le supporte) ; s’il échoue, onrestartfranchement. On détaillereloadvsrestarten Partie 9.
set -euo pipefail : le script qui s’arrête quand ça casse
Cette ligne, tout en haut, est non négociable dans un script de déploiement. Sans elle, un script bash continue après une erreur — exactement le pire comportement quand on touche à la prod. Décomposons ce qu’active chaque option :
set -e— arrête le script dès qu’une commande échoue (code de sortie non nul). Sans lui, sinpm run buildplante, le script enchaîne quand même sursystemctl restartet tu sers un build cassé. Avec lui, le script s’arrête net sur l’échec.set -u— traite toute variable non définie comme une erreur. Attrape les fautes de frappe ($DEPLOY_DIRvs$DEPLOYDIR) au lieu de les laisser passer comme une chaîne vide, ce qui pourrait donner un désastreuxrm -rf /sur un chemin vide.set -o pipefail— dans un pipe (a | b), fait échouer l’ensemble si n’importe quelle commande du pipe échoue, pas seulement la dernière. Sans lui,commande_qui_plante | tee logrenvoie « succès » parce queteea réussi, masquant l’erreur.
⚠️ Piège — Un
deploy.shsansset -euo pipefailest un piège à retardement. Le jour oùnpm ciéchoue (registre npm indisponible, réseau coupé), le script continue, build à partir d’unnode_modulesincomplet, redémarre le service, et met la prod par terre — en affichant « déploiement terminé ». Le script ne doit jamais mentir sur son succès. Cette ligne garantit qu’il s’arrête à la première casse et remonte le bon code d’erreur.
💡 Réflexe — Rends ton script idempotent : le relancer deux fois de suite doit produire le même état final, sans effet de bord ni erreur.
git reset --hardest idempotent (l’état cible est le même).npm cil’est (il repart d’unnode_modulespropre). Les migrations bien écrites le sont (elles n’appliquent que ce qui est en attente). L’idempotence, c’est ce qui te permet de relancer un déploiement sans peur après un échec partiel.
Lancer le déploiement en une commande
Tu n’as pas besoin de te connecter interactivement pour déployer. SSH exécute une commande à distance et rend la main. Depuis ton poste :
ssh deploy@formacampus.fr 'cd /var/www/formacampus && ./deploy.sh'
# Se connecte, va dans le dossier du projet, lance le script, affiche sa sortie,
# et se déconnecte. Une seule ligne, tout le déploiement.🐚 Au terminal — Pour rendre ça encore plus court, tu peux poser un alias local ou une petite fonction dans ton
~/.bashrc(côté poste de dev) :
alias deploy-formacampus="ssh deploy@formacampus.fr 'cd /var/www/formacampus && ./deploy.sh'"
# Désormais, `deploy-formacampus` déclenche tout le déploiement à distance.C’est déjà un énorme progrès sur les sept étapes manuelles : une commande, toujours la même, qui exécute toujours les mêmes actions dans le même ordre. C’est la brique que GitHub Actions viendra appeler tout seul au chapitre suivant.
Les git hooks : quand git push déclenche le déploiement
Il existe une manière historique de déployer sans SSH manuel ni CI : les git hooks. Un hook est un script que git exécute automatiquement à un moment de son cycle de vie. Celui qui nous intéresse est post-receive, déclenché côté serveur quand un git push arrive.
Le montage classique : on crée sur le serveur un dépôt bare (un dépôt git sans copie de travail, juste l’historique), on y pousse depuis son poste, et le hook post-receive déploie le code reçu vers le dossier de prod.
# --- Sur le serveur : créer le dépôt bare ---
mkdir -p /var/repo/formacampus.git
cd /var/repo/formacampus.git
git init --bare # dépôt sans working tree, il ne stocke que l'historique# --- Sur le serveur : le hook post-receive ---
# Fichier : /var/repo/formacampus.git/hooks/post-receive (rendu exécutable avec chmod +x)
#!/usr/bin/env bash
set -euo pipefail
TARGET="/var/www/formacampus" # dossier de prod (working tree)
GIT_DIR="/var/repo/formacampus.git"
# Déploie le contenu reçu dans le dossier de prod, puis lance le script :
git --work-tree="$TARGET" --git-dir="$GIT_DIR" checkout -f main
cd "$TARGET" && ./deploy.sh# --- Sur ton poste : ajouter le serveur comme remote, puis pousser ---
git remote add prod deploy@formacampus.fr:/var/repo/formacampus.git
git push prod main # ce push déclenche le hook post-receive côté serveurDésormais, git push prod main déploie. C’est élégant et sans dépendance externe — mais c’est un survol ici, car cette approche a les mêmes limites que le script lancé en SSH (voir plus bas), plus une : elle mélange le rôle de « transport du code » et de « déclencheur de déploiement ». En pratique, on lui préfère aujourd’hui une CI dédiée.
📚 La doc — Les git hooks sont documentés dans le Pro Git book et via
man githooks.post-receivereçoit sur son entrée standard les références poussées (ancien SHA, nouveau SHA, nom de la ref) : tu peux t’en servir pour ne déployer que si c’estmainqui a été poussée, et ignorer les autres branches.
Les limites de cette approche
Le script deploy.sh, qu’on le lance en SSH ou via un hook, reste une étape intermédiaire. Ses limites motivent le passage à une vraie CI :
- Le build a lieu sur le serveur de prod. Compiler, c’est gourmand en CPU et en RAM. Sur un petit VPS qui sert déjà le trafic, un
npm run buildpeut saturer la mémoire et faire ramer le site pendant le déploiement — voire faire tomber le service par manque de RAM. - Aucun test. Le script déploie ce qu’on lui donne, sans vérifier que le code est sain. Un commit qui casse les tests part quand même en prod.
- Pas de fenêtre de contrôle. Le build échoue au milieu du déploiement, alors qu’on a déjà commencé à toucher à la prod. Le pipeline idéal (13.1) teste et build avant d’approcher le serveur.
- Ça reste déclenché à la main (ou par un push qui fait aussi office de transport). Pas de tableau de bord, pas d’historique clair des exécutions, pas de gestion fine des secrets.
🔒 Sécurité — Ce script suppose que personne ne modifie le code directement sur le serveur :
git reset --hardécrase toute modification locale. C’est voulu — le serveur est un reflet du dépôt, pas un lieu d’édition. Corollaire : ledeploy.shne doit jamais contenir de secret en dur (mot de passe de base, token). Les secrets vivent dans un.envhors du dépôt, sur le serveur, avec des permissions600— on y consacre tout le 13.5.
🧭 Sur FormaCampus — L’équipe commence par écrire ce
deploy.shet le versionner à la racine du dépôt. Premier bénéfice immédiat : plus personne ne déploie « de mémoire », et un nouveau membre peut déployer dès le premier jour avecssh deploy@formacampus.fr './deploy.sh'. Mais le build de l’API Symfony plus du front Next.js sur leur VPS mutualisé fait grimper la charge et ralentit le site pendant chaque livraison. C’est le déclic : sortir le build du serveur de prod et le confier à la CI. Ledeploy.shne disparaît pas pour autant — il devient la cible que GitHub Actions appellera à distance (13.3), une fois le build et les tests déjà passés ailleurs.
Ce qu’il faut retenir
Un deploy.sh versionné, blindé par set -euo pipefail et idempotent, transforme la checklist manuelle en un geste fiable et répétable, lançable d’une seule commande SSH. Les git hooks (post-receive sur un dépôt bare) permettent qu’un git push déploie, sans outil externe. Mais ces approches buildent sur le serveur et ne testent rien : elles fiabilisent le geste sans le sécuriser complètement. La suite déplace build et tests avant la prod, avec GitHub Actions — qui, in fine, appellera ce même deploy.sh.
✏️ Exercices
Exercice 1 — Blinde ce script. Un collègue te montre son deploy.sh : il commence directement par git pull, n’a pas de set, et finit par systemctl restart sans vérifier que le build a réussi. Cite trois améliorations concrètes et explique le risque évité par chacune.
✅ Solution
(1) Ajouter set -euo pipefail en tête : sans lui, si npm run build échoue, le script continue jusqu’au restart et sert un build cassé en affichant « terminé ». (2) Remplacer git pull par git fetch + git reset --hard origin/main : évite les conflits/merges surprises et garantit que le serveur reflète exactement la branche de prod. (3) Utiliser npm ci au lieu de npm install : installation reproductible depuis le lock, dans un node_modules propre. On accepte aussi : un healthcheck final (curl -fsS) qui échoue bruyamment si le site ne répond pas, et l’ordre migrations → build → redémarrage.
Exercice 2 — Pourquoi -u ? Un script contient rm -rf "$BUILD_DIR/cache". La variable BUILD_DIR n’a jamais été définie (faute de frappe : elle s’appelle en réalité BUILDDIR). Que se passe-t-il sans set -u, et en quoi set -u l’empêche-t-il ?
✅ Solution
Sans set -u, une variable non définie vaut la chaîne vide. La commande devient rm -rf "/cache" — elle tente de supprimer /cache à la racine du système, pas le cache du build voulu. Selon les droits, ça peut détruire des fichiers hors du projet. Avec set -u, toute référence à une variable non définie arrête immédiatement le script avec une erreur (BUILD_DIR: unbound variable), avant qu’une commande destructrice ne s’exécute sur un chemin vide. C’est un garde-fou majeur des scripts qui manipulent des fichiers.
🧠 Quiz de révision
1. Pourquoi mettre deploy.sh dans le dépôt du projet plutôt que seulement sur le serveur ?
deploy.sh dans le dépôt du projet plutôt que seulement sur le serveur ?Parce qu’il est ainsi versionné : il évolue avec le code, son historique est traçable, tout le monde voit et revoit ce qu’il fait, et il est identique sur tous les environnements. Un script qui ne vit que sur le serveur devient une boîte noire non revue, qu’on n’ose plus toucher.
2. Que fait set -euo pipefail et pourquoi est-ce vital dans un script de déploiement ?
set -euo pipefail et pourquoi est-ce vital dans un script de déploiement ?-e arrête le script à la première commande qui échoue ; -u traite les variables non définies comme des erreurs ; -o pipefail fait échouer un pipe si n’importe laquelle de ses commandes échoue. Vital parce qu’un script de déploiement qui continue après une erreur peut redémarrer le service sur un build cassé tout en affichant « succès ». On veut qu’il s’arrête net et remonte l’erreur.
3. Pourquoi npm ci plutôt que npm install dans un déploiement ?
npm ci plutôt que npm install dans un déploiement ?npm ci installe exactement les versions figées dans package-lock.json, dans un node_modules propre (qu’il recrée). C’est reproductible : deux exécutions donnent le même résultat. npm install peut faire évoluer le lock et introduire des versions différentes — inacceptable pour un déploiement où l’on veut l’artefact identique à celui testé.
4. Qu’est-ce qu’un hook post-receive et quand se déclenche-t-il ?
post-receive et quand se déclenche-t-il ?C’est un script git exécuté côté serveur, automatiquement, quand un git push a été reçu. Sur un dépôt bare, on s’en sert pour déployer le code poussé vers le dossier de production (git checkout -f vers le working tree, puis lancer le script). Ainsi, un simple git push prod main déclenche le déploiement.
5. Cite deux limites du déploiement par script/hook qui justifient de passer à une CI.
(1) Le build a lieu sur le serveur de prod, ce qui consomme CPU/RAM et peut faire ramer ou tomber le site pendant le déploiement. (2) Aucun test n’est exécuté : un code cassé part quand même en prod. On accepte aussi : le build peut échouer après avoir commencé à toucher à la prod, et il n’y a ni historique clair des exécutions ni gestion fine des secrets.
Chapitre suivant : 13.3 — GitHub Actions — on sort le build et les tests du serveur de prod : un workflow qui, à chaque push sur main, installe, lint, teste, build, puis appelle ton deploy.sh à distance.