TutosLe Potager du Web
← Retour aux tutos
Git & déploiement Intermédiaire v3.0 Vérifié le 21/09/2026

Créer un site via GitHub puis le mettre en ligne sur o2switch

Guide Potager unique de bout en bout : créer le dépôt GitHub, relier o2switch au dépôt avec une clé SSH dédiée et un remote vérifié, cloner hors du site public, injecter le commit en live avec dry-run et sauvegarde, puis automatiser les mises à jour avec cron.

Avant de commencer

  • Un accès au dépôt GitHub et la possibilité d'ouvrir Settings → Deploy keys.
  • Un accès cPanel et Terminal/SSH au compte o2switch.
  • Le domaine ou sous-domaine destiné au site.
  • Une sauvegarde récente si un site existe déjà à l'emplacement live.
1

1. Fixer l'architecture avant de toucher au live

Le circuit de référence est : GitHub = source de vérité → clé SSH dédiée au dépôt → clone de déploiement privé sur o2switch → dry-run → copie contrôlée vers un dossier live distinct → DocumentRoot vers la partie réellement publique.

Distingue toujours trois états : le dépôt distant GitHub, le clone de déploiement sur o2switch et les fichiers réellement servis au public. Un commit GitHub n'est pas encore un changement live.

Repère aussi la structure du projet. Pour une application qui contient public/, le DocumentRoot doit viser LIVE/public. Pour un site plat dont index.php ou index.html est à la racine, il vise LIVE.

AttentionNe clone jamais le dépôt directement dans le DocumentRoot.
AttentionNe déduis jamais le chemin live à partir d'une ancienne doc : vérifie-le dans cPanel → Domaines.
À vérifier
  • Branche de production identifiée, généralement main.
  • Structure flat ou public/ identifiée.
  • DocumentRoot actuel ou futur vérifié dans cPanel.
2

2. Créer le dépôt GitHub et y mettre la première version du site

Crée le dépôt dans GitHub. Pour un site client ou interne contenant de la logique non destinée à être publique, pars en privé. La branche de production utilisée dans ce guide est main.

Ajoute au minimum la première version du site et un commit sur main avant de demander au serveur de cloner cette branche. Le dépôt peut être rempli depuis l'interface GitHub, une copie locale ou un autre outil : l'important est que main contienne le commit à mettre en ligne.

Ne mets jamais dans Git .env, clés privées, mots de passe, dumps SQL, sauvegardes ou données runtime. Fournis plutôt .env.example quand le projet a besoin de documenter ses variables.

AttentionUn dépôt privé reste un dépôt Git : un secret commité y entre dans l'historique et doit être considéré comme exposé aux personnes et outils autorisés sur ce dépôt.
À vérifier
  • Le dépôt existe.
  • main contient au moins un commit.
  • Aucun secret réel n'est versionné.
3

3. Générer sur o2switch une clé SSH dédiée à CE dépôt

La clé privée reste sur o2switch. Seule la clé publique sera copiée dans GitHub. Une clé par dépôt évite qu'une seule clé serveur donne accès à tous les projets.

Pour un déploiement automatisé par cron, la clé dédiée doit fonctionner sans interaction : le modèle ci-dessous la crée sans phrase secrète, puis restreint les droits du dossier et de la clé privée.

REPO_SLUG="mon-site"
KEY="$HOME/.ssh/id_ed25519_${REPO_SLUG}"
mkdir -p "$HOME/.ssh"
chmod 700 "$HOME/.ssh"
ssh-keygen -t ed25519 -N '' -C "deploy-${REPO_SLUG}@lepotager" -f "$KEY"
chmod 600 "$KEY"
chmod 644 "$KEY.pub"
cat "$KEY.pub"
AttentionNe copie jamais le contenu de la clé privée id_ed25519_* dans GitHub, un ticket, un chat ou un dépôt.
À vérifier
  • La clé privée existe seulement sur le serveur.
  • La sortie copiée vers GitHub commence par ssh-ed25519.
4

4. Autoriser cette clé dans GitHub

Dans le dépôt GitHub : Settings → Deploy keys → Add deploy key. Donne un nom parlant, par exemple « o2switch production », puis colle la clé publique affichée à l'étape précédente.

Pour le flux normal GitHub → live, laisse la clé en lecture seule : le serveur n'a besoin que de fetch/clone. N'active l'écriture que pour un cas explicite où le serveur doit pousser vers GitHub, par exemple l'import initial d'un ancien site décrit à la fin de ce guide.

AttentionN'utilise pas une clé de compte générale quand une deploy key limitée au dépôt suffit.
À vérifier
  • La deploy key apparaît dans le bon dépôt.
  • Write access est désactivé pour le déploiement normal.
5

5. Créer l'alias SSH qui relie la clé au bon dépôt

Quand plusieurs dépôts privés vivent sur le même hébergement, un alias Host évite que SSH présente la mauvaise clé à GitHub. Le remote Git utilisera cet alias au lieu de github.com directement.

Ajoute un bloc distinct dans ~/.ssh/config. Remplace mon-site par un nom court propre au dépôt et adapte IdentityFile au fichier créé à l'étape 3.

nano "$HOME/.ssh/config"
Host github-mon-site
  HostName github.com
  User git
  IdentityFile ~/.ssh/id_ed25519_mon-site
  IdentitiesOnly yes
chmod 600 "$HOME/.ssh/config"
AttentionÉvite un wildcard de type Host github-* qui réutiliserait involontairement la même clé pour tous les dépôts.
À vérifier
  • L'alias pointe sur github.com.
  • IdentityFile vise la clé dédiée au dépôt.
  • ~/.ssh/config est en 600.
6

6. Tester la connexion ET le dépôt distant avant de cloner

Teste d'abord l'authentification SSH, puis le repository précis. La première connexion peut demander de confirmer l'empreinte d'hôte : compare-la aux empreintes publiées par GitHub avant d'accepter.

Le test ssh -T de GitHub peut afficher un message d'authentification réussie tout en terminant avec le code de sortie 1, parce que GitHub ne fournit pas de shell interactif. Le message de succès est donc le signal à lire.

git ls-remote valide la partie que les tutos oublient souvent : non seulement la clé parle à GitHub, mais l'alias a réellement accès au bon OWNER/REPO.

ssh -T git@github-mon-site
git ls-remote git@github-mon-site:OWNER/REPO.git HEAD
AttentionSi git ls-remote affiche Repository not found, ne poursuis pas vers le live : vérifie OWNER/REPO et la deploy key du dépôt.
À vérifier
  • SSH reconnaît le compte GitHub.
  • git ls-remote renvoie un SHA pour le dépôt cible.
7

7. Cloner le dépôt dans une zone privée et vérifier origin

Crée un clone de déploiement séparé du site public. Ce clone est pull-only : on n'y développe pas et on n'y corrige pas des fichiers à la main.

Après le clone, vérifie explicitement le remote origin. Il doit utiliser l'alias SSH créé plus haut, pas une URL HTTPS ni une autre clé.

mkdir -p "$HOME/repositories"
git clone --branch main --single-branch git@github-mon-site:OWNER/REPO.git "$HOME/repositories/REPO-deploy"
git -C "$HOME/repositories/REPO-deploy" remote -v
git -C "$HOME/repositories/REPO-deploy" status -sb
git -C "$HOME/repositories/REPO-deploy" rev-parse HEAD
AttentionLe dossier ~/repositories/REPO-deploy ne doit pas être le DocumentRoot du domaine.
À vérifier
  • origin pointe vers git@github-mon-site:OWNER/REPO.git.
  • Le clone est propre.
  • HEAD correspond au commit attendu sur main.
8

8. Préparer et vérifier la destination live

Crée une destination dédiée à ce site, par exemple ~/apps/REPO. Si un ancien site existe déjà, inventorie son contenu avant toute commande comportant --delete : dossiers d'uploads, API, boutique, back-office, .htaccess cPanel, .htpasswd, .well-known et données runtime.

Le marqueur .potager-deploy-root sert de garde-fou : le script refusera de copier vers une destination qui ne l'a pas. Il évite qu'une variable vide ou un mauvais chemin transforme un rsync en catastrophe.

Dans cPanel → Domaines, note le DocumentRoot réel. Ne le change qu'après le premier déploiement si tu prépares une nouvelle application hors ligne.

LIVE="$HOME/apps/REPO"
mkdir -p "$LIVE"
touch "$LIVE/.potager-deploy-root"
find "$LIVE" -maxdepth 2 -mindepth 1 -print | sort | head -n 200
AttentionNe choisis jamais /, $HOME ou le public_html global comme destination d'un script de déploiement spécialisé.
À vérifier
  • Le dossier live est propre à ce site.
  • Les applications ou données voisines sont identifiées.
  • Le marqueur existe.
9

9. Définir ce que rsync n'a pas le droit de supprimer

Crée un fichier d'exclusions propre au projet. Les exemples ci-dessous ne sont pas une liste universelle : garde uniquement les dossiers qui existent réellement et qui doivent survivre au déploiement.

.htaccess mérite une décision explicite : s'il appartient au projet, versionne-le et déploie-le ; s'il est créé côté cPanel pour une protection locale, exclue-le. Même logique pour les autres fichiers serveur.

Le marqueur de sécurité, les secrets et les données runtime doivent toujours rester hors de la gestion destructive du dépôt.

mkdir -p "$HOME/deploy"
cat > "$HOME/deploy/REPO-rsync-excludes.txt" <<'EOF'
.git/
.github/
.env
.htpasswd
.well-known/
.potager-deploy-root
uploads/
storage/
files/
logs/
EOF
cat "$HOME/deploy/REPO-rsync-excludes.txt"
AttentionN'utilise pas cette liste aveuglément : une exclusion incorrecte peut soit supprimer une donnée live, soit empêcher un fichier de code légitime d'être mis à jour.
À vérifier
  • Secrets exclus.
  • Runtime/uploads exclus selon le projet.
  • Protections cPanel identifiées.
  • Aucun nom singulier/pluriel n'a été supposé sans vérifier le serveur.
10

10. Installer le script de déploiement sûr

Le script ci-dessous met à jour uniquement en fast-forward, refuse un clone modifié localement, utilise flock contre les exécutions simultanées, simule par défaut, sauvegarde les fichiers remplacés lors du vrai déploiement et n'enregistre le SHA comme déployé qu'après succès.

Il utilise rsync -rlt plutôt que -a : on copie contenu, arborescence et dates sans imposer aveuglément au live les permissions du clone. Les droits du serveur restent ainsi une décision de déploiement, pas un effet de bord de Git.

mkdir -p "$HOME/scripts" "$HOME/logs" "$HOME/.cache" "$HOME/backups/REPO"
nano "$HOME/scripts/deploy-REPO.sh"
#!/usr/bin/env bash
set -Eeuo pipefail

REPO="$HOME/repositories/REPO-deploy"
LIVE="$HOME/apps/REPO"
BRANCH="main"
SITE_URL="https://example.org"
EXCLUDES="$HOME/deploy/REPO-rsync-excludes.txt"
LOG="$HOME/logs/deploy-REPO.log"
LOCK="$HOME/.cache/deploy-REPO.lock"
STATE="$HOME/.cache/deploy-REPO.state"
BACKUP_ROOT="$HOME/backups/REPO"
MODE="${1:---dry-run}"
SKIP_HTTP_CHECK="${SKIP_HTTP_CHECK:-0}"

check_http() {
  [ "$SKIP_HTTP_CHECK" = "1" ] && return 0
  command -v curl >/dev/null || return 0
  CODE="$(curl -LsS --max-time 20 -o /dev/null -w '%{http_code}' "$SITE_URL/" || true)"
  [ "$CODE" = "200" ] || { echo "Contrôle HTTP en échec : $CODE"; return 1; }
}

mkdir -p "$(dirname "$LOG")" "$(dirname "$LOCK")" "$(dirname "$STATE")" "$BACKUP_ROOT"
touch "$LOG"
chmod 600 "$LOG"
exec >>"$LOG" 2>&1

case "$LIVE" in
  ""|/|"$HOME"|"$HOME/public_html") echo "Destination live dangereuse : $LIVE"; exit 2 ;;
esac

test -d "$REPO/.git" || { echo "Clone Git absent : $REPO"; exit 1; }
test -d "$LIVE" || { echo "Dossier live absent : $LIVE"; exit 1; }
test -f "$LIVE/.potager-deploy-root" || { echo "Marqueur de sécurité absent"; exit 1; }
test -f "$EXCLUDES" || { echo "Fichier d'exclusions absent : $EXCLUDES"; exit 1; }

exec 9>"$LOCK"
flock -n 9 || { echo "Un déploiement est déjà en cours"; exit 0; }

if [ -n "$(git -C "$REPO" status --porcelain --untracked-files=no)" ]; then
  echo "Le clone contient des modifications locales suivies"; exit 1
fi

git -C "$REPO" fetch --prune origin "$BRANCH"
LOCAL="$(git -C "$REPO" rev-parse HEAD)"
REMOTE="$(git -C "$REPO" rev-parse "origin/$BRANCH")"
if [ "$LOCAL" != "$REMOTE" ]; then
  git -C "$REPO" merge-base --is-ancestor "$LOCAL" "$REMOTE" || { echo "Historique divergent : arrêt"; exit 1; }
  git -C "$REPO" merge --ff-only "origin/$BRANCH"
fi
COMMIT="$(git -C "$REPO" rev-parse HEAD)"

if [ "$MODE" = "--deploy" ] && [ -f "$STATE" ] && grep -Fqx "$COMMIT" "$STATE"; then
  check_http
  echo "Aucun nouveau commit : ${COMMIT:0:12}"
  exit 0
fi

if [ -f "$REPO/composer.json" ]; then
  command -v composer >/dev/null || { echo "Composer requis mais absent"; exit 1; }
  (cd "$REPO" && composer install --no-dev --optimize-autoloader --no-interaction --prefer-dist)
fi

RSYNC=(-rlt --delete-delay --itemize-changes --exclude-from="$EXCLUDES")
if [ "$MODE" = "--dry-run" ]; then
  rsync "${RSYNC[@]}" --dry-run "$REPO/" "$LIVE/"
  echo "Simulation terminée : aucun fichier live modifié"
  exit 0
fi

[ "$MODE" = "--deploy" ] || { echo "Usage: $0 [--dry-run|--deploy]"; exit 2; }
STAMP="$(date +%Y%m%d-%H%M%S)"
BACKUP="$BACKUP_ROOT/$STAMP"
mkdir -p "$BACKUP"
rsync "${RSYNC[@]}" --backup --backup-dir="$BACKUP" "$REPO/" "$LIVE/"

check_http

printf '%s\n' "$COMMIT" > "$STATE.tmp"
mv "$STATE.tmp" "$STATE"
chmod 600 "$STATE"
echo "Déploiement terminé : ${COMMIT:0:12}"
AttentionRemplace REPO et https://example.org avant toute exécution.
AttentionLe script ne doit pas être rendu générique au point de pouvoir cibler n'importe quel dossier arbitraire.
À vérifier
  • REPO, LIVE, SITE_URL et EXCLUDES relus.
  • Le backup est hors du DocumentRoot.
  • Le script contient bien --dry-run par défaut.
11

11. Simuler le déploiement avant d'écrire

Le premier passage est toujours un dry-run. Lis toute la sortie, surtout les lignes qui indiquent une suppression. Si quelque chose de serveur doit survivre, ajoute son chemin exact au fichier d'exclusions puis recommence.

Valide aussi la syntaxe du script avant de l'utiliser.

chmod 700 "$HOME/scripts/deploy-REPO.sh"
bash -n "$HOME/scripts/deploy-REPO.sh" && echo "Syntaxe correcte"
"$HOME/scripts/deploy-REPO.sh" --dry-run
tail -n 150 "$HOME/logs/deploy-REPO.log"
AttentionNe lance pas --deploy tant que la simulation montre une suppression ou un écrasement non compris.
À vérifier
  • Dry-run relu.
  • Aucun secret/runtime menacé.
  • Le SHA récupéré est celui attendu.
12

12. Faire le premier déploiement puis brancher le domaine sur le live

Si le domaine ne pointe pas encore vers le nouveau dossier, exécute le premier déploiement avec SKIP_HTTP_CHECK=1. Ensuite, dans cPanel → Domaines, règle le DocumentRoot vers LIVE/public pour une application structurée ou LIVE pour un site plat.

Une fois le domaine basculé, relance le script sans SKIP_HTTP_CHECK pour valider aussi la réponse HTTP. Contrôle les pages, assets, formulaires, API, BDD et applications voisines.

En cas de 403, vérifie toute la chaîne de répertoires avec namei -l. Corrige seulement les droits nécessaires ; n'applique pas chmod -R 755 au projet entier.

SKIP_HTTP_CHECK=1 "$HOME/scripts/deploy-REPO.sh" --deploy
namei -l "$HOME/apps/REPO/public"
"$HOME/scripts/deploy-REPO.sh" --deploy
curl -I https://example.org/
AttentionNe pointe jamais le domaine vers ~/repositories/REPO-deploy.
À vérifier
  • DocumentRoot correct.
  • Accueil et assets répondent.
  • Secrets non accessibles.
  • Fonctions sensibles et applications voisines testées.
13

13. Automatiser les futures mises à jour avec cron

N'automatise qu'après un déploiement manuel réussi. Dans cPanel → Tâches cron, programme le script. Le cron ne contient pas de logique Git : il appelle seulement le script testé.

Un passage toutes les dix minutes suffit généralement pour un site. Tu peux décaler les minutes entre projets afin qu'ils ne fassent pas tous fetch en même temps.

/home/USER/scripts/deploy-REPO.sh --deploy >> /home/USER/logs/deploy-REPO-cron.log 2>&1
AttentionNe crée pas une deuxième tâche identique si un cron existe déjà.
À vérifier
  • Minute, heure, jour, mois et jour de semaine renseignés dans l'interface cPanel.
  • Commande absolue.
  • Un nouveau passage apparaît dans le log.
14

14. Vérifier le circuit complet GitHub → SSH → clone → live

Fais une petite modification réversible dans GitHub et fusionne-la sur main. Sans lancer le script à la main, laisse le cron récupérer le nouveau commit.

Vérifie ensuite les trois états séparément : SHA de origin/main, SHA du clone et contenu live. Ce test prouve que la connexion distante, le fetch, le déploiement et le cron fonctionnent tous ensemble.

git -C "$HOME/repositories/REPO-deploy" fetch origin main
git -C "$HOME/repositories/REPO-deploy" rev-parse origin/main
git -C "$HOME/repositories/REPO-deploy" rev-parse HEAD
cat "$HOME/.cache/deploy-REPO.state"
tail -n 100 "$HOME/logs/deploy-REPO.log"
AttentionPour le premier test automatique, évite une migration de base de données, un paiement ou une autre modification difficile à annuler.
À vérifier
  • origin/main = HEAD après le passage cron.
  • Le fichier d'état contient le SHA déployé.
  • La modification est réellement visible sur le site.
15

15. Cas particulier : partir d'un site déjà live et l'envoyer d'abord sur GitHub

Si le site existe avant le dépôt, ne fais pas git init dans le DocumentRoot. Copie d'abord le code vers ~/exports/REPO, retire secrets, dumps, sauvegardes, caches et données personnelles, puis initialise Git dans cette copie privée.

Pour ce premier push serveur → GitHub, utilise une clé dédiée qui a temporairement le droit d'écriture sur le dépôt ou une autre méthode d'authentification de push explicitement autorisée. Une fois l'import terminé, reviens à une deploy key de lecture seule pour la production.

Pour WordPress ou SPIP, le dépôt doit surtout contenir le code spécifique ; la BDD, les uploads et les données runtime se sauvegardent et se migrent séparément.

mkdir -p "$HOME/exports/REPO"
rsync -a --dry-run --exclude='.git/' --exclude='.env' --exclude='*.sql' --exclude='*.zip' CHEMIN-LIVE/ "$HOME/exports/REPO/"
cd "$HOME/exports/REPO"
find . -type f \( -name '.env' -o -name '*.sql' -o -name '*.pem' -o -name '*.key' \) -print
grep -RInE --exclude-dir=.git '(password|passwd|secret|api[_-]?key|token|DB_PASSWORD|DB_PASS)' . | head -n 200
git init -b main
git add -n .
git status --short
git add .
git commit -m "Import initial depuis le site live"
git remote add origin git@github-mon-site:OWNER/REPO.git
git remote -v
git push -u origin main
AttentionSi un secret réel a déjà été poussé, le supprimer dans un nouveau commit ne suffit pas : révoque-le et nettoie l'historique.
À vérifier
  • Import fait depuis une copie hors live.
  • git add -n relu avant le commit.
  • Remote utilise l'alias SSH du bon dépôt.
  • Après import, la clé de production est revenue en lecture seule.

🧯 Erreurs fréquentes

ssh -T affiche une authentification réussie mais la commande semble échouer

Cause probable : GitHub n'ouvre pas de shell interactif et son test SSH peut terminer avec le code 1 malgré une authentification réussie.

Correctif : Lis le message d'authentification. Confirme ensuite le dépôt avec git ls-remote ; c'est ce second test qui valide réellement le remote.

Permission denied (publickey)

Cause probable : Mauvais alias SSH, mauvais IdentityFile, droits de ~/.ssh incorrects ou clé publique absente du bon dépôt.

Correctif : Vérifie ~/.ssh/config, chmod 700 ~/.ssh, chmod 600 sur la clé privée/config, puis la Deploy key dans GitHub.

Repository not found

Cause probable : SSH fonctionne mais la clé n'a pas accès à OWNER/REPO, le nom du dépôt est faux ou origin vise le mauvais alias.

Correctif : Contrôle git ls-remote, git remote -v et la Deploy key du dépôt ciblé avant de continuer.

git remote -v affiche github.com au lieu de l'alias dédié

Cause probable : Le dépôt a été cloné ou relié avec l'URL SSH générique.

Correctif : Corrige avec git remote set-url origin git@github-mon-site:OWNER/REPO.git puis reteste git ls-remote origin.

Le dry-run veut supprimer des fichiers du serveur

Cause probable : Le dossier live contient des secrets, uploads, protections cPanel ou applications voisines absentes du dépôt et non exclues.

Correctif : Arrête le déploiement, inventorie le chemin réel et ajoute les exclusions exactes. Ne suppose pas les noms de dossiers.

Le clone refuse la mise à jour fast-forward

Cause probable : Il contient des commits locaux ou l'historique a divergé.

Correctif : Ne force pas et ne merge pas automatiquement. Sauvegarde ce qui doit l'être puis réaligne ou recrée proprement le clone de déploiement.

Le site renvoie 403 après le changement de DocumentRoot

Cause probable : Apache ne peut pas traverser un répertoire parent ou les droits du dossier public sont incorrects.

Correctif : Utilise namei -l sur le DocumentRoot et corrige uniquement les droits nécessaires ; évite chmod -R 755.

Le commit existe sur GitHub mais le site ne change pas

Cause probable : Le remote, le clone et le live sont trois états distincts ; le cron peut ne pas être passé ou le script peut avoir échoué avant rsync.

Correctif : Compare origin/main, HEAD, le fichier state et les logs de déploiement, puis vérifie le cron cPanel.

Le cron échoue alors que le script marche à la main

Cause probable : Chemins relatifs, environnement PATH différent ou clé SSH non résolue sans interaction.

Correctif : Utilise des chemins absolus, conserve l'alias ~/.ssh/config et vérifie le log exact produit par l'exécution cron.

Notes

  • Cette fiche remplace les anciennes fiches séparées « Clé SSH de déploiement GitHub », « Automatiser GitHub → live avec cron » et « Site live → GitHub ».
  • Le module Grav 05 reste séparé parce qu'il appartient au parcours pédagogique Grav 00→06 et renvoie à ce workflow général au lieu de le dupliquer.
  • Le tuto général o2switch sur les tâches cron reste séparé : il sert aussi à d'autres usages que le déploiement Git.

Sources de vérification

Ces liens servent à recontrôler le tuto quand l'outil ou l'hébergeur évolue.

Historique de la fiche

v3.0Fusion des guides Git/o2switch : création du dépôt, deploy key SSH dédiée, alias et remote distant, clone, rsync sûr, live, cron et cas inverse d'import d'un ancien site.
v2.0Refonte du premier guide GitHub → site live : clone séparé, dry-run et exclusions runtime.

L'historique détaillé reste aussi disponible dans Git.