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. 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.
- Branche de production identifiée, généralement main.
- Structure flat ou public/ identifiée.
- DocumentRoot actuel ou futur vérifié dans cPanel.
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.
- Le dépôt existe.
- main contient au moins un commit.
- Aucun secret réel n'est versionné.
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" - La clé privée existe seulement sur le serveur.
- La sortie copiée vers GitHub commence par ssh-ed25519.
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.
- La deploy key apparaît dans le bon dépôt.
- Write access est désactivé pour le déploiement normal.
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 yeschmod 600 "$HOME/.ssh/config" - L'alias pointe sur github.com.
- IdentityFile vise la clé dédiée au dépôt.
- ~/.ssh/config est en 600.
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-sitegit ls-remote git@github-mon-site:OWNER/REPO.git HEAD - SSH reconnaît le compte GitHub.
- git ls-remote renvoie un SHA pour le dépôt cible.
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 - origin pointe vers git@github-mon-site:OWNER/REPO.git.
- Le clone est propre.
- HEAD correspond au commit attendu sur main.
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 - Le dossier live est propre à ce site.
- Les applications ou données voisines sont identifiées.
- Le marqueur existe.
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" - 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. 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}" - REPO, LIVE, SITE_URL et EXCLUDES relus.
- Le backup est hors du DocumentRoot.
- Le script contient bien --dry-run par défaut.
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" - Dry-run relu.
- Aucun secret/runtime menacé.
- Le SHA récupéré est celui attendu.
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" --deploynamei -l "$HOME/apps/REPO/public""$HOME/scripts/deploy-REPO.sh" --deploy
curl -I https://example.org/ - DocumentRoot correct.
- Accueil et assets répondent.
- Secrets non accessibles.
- Fonctions sensibles et applications voisines testées.
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 - 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. 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" - 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. 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 200git init -b main
git add -n .
git status --shortgit 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 - 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
L'historique détaillé reste aussi disponible dans Git.