Une agence WordPress qui gère 5 à 30 sites clients ne peut pas continuer à pousser des fichiers en SFTP depuis le poste du dev qui « a le code le plus à jour ». Ça tient trois semaines, puis ça casse : fichier écrasé, patch de sécurité perdu, wp-config.php modifié à la main sur prod que personne n'a répercuté. Le workflow qui suit est un standard de marché, testé chez plusieurs agences clientes de Datacampus. Il n'est pas exotique, il est juste rigoureux.
1. Pourquoi un workflow, et pas juste du SFTP
Le SFTP direct en prod pose quatre problèmes qui coûtent cher à moyen terme.
- Pas d'historique : vous ne savez pas qui a changé quoi, quand, pourquoi.
- Pas de rollback, un déploiement foireux à 17 h le vendredi, vous rejouez de mémoire.
- Pas de pré-vérification — le bug se découvre en prod, devant le client.
- Pas de reproductibilité : un second dev reprend le projet et reconstitue péniblement l'état du code.
Le workflow Git + staging + CI/CD résout les quatre en une fois. Ce n'est pas une lubie de devops, c'est un ROI opérationnel direct : moins d'incidents, moins de stress, facturation plus saine.
2. Les principes, en quatre phrases
- Le code vit dans Git (
gitlab.datacampus.fr, GitHub, Bitbucket — peu importe, mais un seul endroit). - Deux environnements seulement : staging (
preprod.client.fr, protégé par.htpasswd) et prod (www.client.fr). - La base de données et les
uploadssont synchronisés prod → staging ponctuellement. Jamais l'inverse. - Le déploiement est automatique sur push (staging sur
develop, prod surmainavec bouton manuel).
3. Que tracker dans Git (et surtout, que ne pas tracker)
Un dépôt WordPress bien configuré ne versionne que ce qui est spécifique au projet. Le reste vient d'ailleurs.
À versionner
- Le thème custom (
wp-content/themes/votre-theme/). - Les plugins custom développés pour le client (
wp-content/plugins/client-custom/). - Un template de
wp-config.php(variables lues depuis l'environnement ou un fichier.envnon versionné). - Les scripts utilitaires :
pull-prod.sh,deploy.sh,.gitlab-ci.yml. - Un
composer.jsonsi vous passez par Composer (recommandé, voir Bedrock plus bas).
À ne pas versionner
- Le core WordPress (
wp-admin,wp-includes, les fichiers à la racine), géré parwp-cliou Composer. - Les plugins tiers du marché (WooCommerce, Yoast, ACF Pro…) — gérés par Composer ou installés à la main. Exception : un fork custom d'un plugin, qu'on versionne volontairement.
- Les uploads (
wp-content/uploads/) : c'est du contenu, pas du code. - Les caches :
wp-content/cache/,wp-content/w3tc-config/,wp-content/backup-*/. - Les fichiers sensibles :
.env, clés SSH, dumps SQL.
.gitignore type
# WordPress core
/wp-admin/
/wp-includes/
/wp-*.php
!/wp-config-sample.php
/index.php
/license.txt
/readme.html
/xmlrpc.php
# Contenu dynamique
wp-content/uploads/
wp-content/cache/
wp-content/backup-*/
wp-content/upgrade/
wp-content/debug.log
# Plugins tiers (ajouter au cas par cas)
wp-content/plugins/*
!wp-content/plugins/client-custom/
!wp-content/plugins/.gitkeep
# Environnement
.env
.env.*
!.env.example
*.sql
*.sql.gz
# Système
.DS_Store
Thumbs.db
.idea/
.vscode/
4. Bedrock : la structure pro pour aller plus loin
Bedrock (par Roots) est une réorganisation de WordPress autour de Composer. Le core WP devient une dépendance, les plugins aussi, la configuration passe par un fichier .env, les secrets sortent du code.
bedrock/
├── composer.json # WP core + plugins déclarés ici
├── .env # DB, URLs, secrets (non versionné)
├── config/
│ ├── application.php # remplace wp-config.php
│ └── environments/
│ ├── development.php
│ ├── staging.php
│ └── production.php
└── web/
├── app/ # équivalent wp-content/
├── wp/ # core WP (géré par Composer)
└── index.php
Au quotidien, on écrit :
composer require wpackagist-plugin/woocommerce
composer update
composer require roots/wp-password-policy
Et on versionne le composer.lock. L'environnement de staging et de prod reconstruisent exactement le même état avec composer install --no-dev. Recommandé dès qu'on est plus d'une personne sur le projet ou que la durée de vie dépasse six mois.
5. Modèle de branches
Trois branches suffisent pour 95 % des projets.
main: l'état qui tourne en prod. Protégée, pas de push direct.develop: l'état qui tourne en staging. Les MR fusionnent ici.feature/xxx,fix/xxx— branches courtes, une par sujet, fusionnées dansdevelop.
Flux nominal : on code sur feature/nouveau-formulaire, on ouvre une Merge Request vers develop, elle est revue, fusionnée, déployée automatiquement sur staging. Quand on est satisfait du recetage, on fusionne develop → main via une MR dédiée, et on déclenche le déploiement prod (manuel, sur bouton).
6. Pipeline GitLab CI : l'exemple concret
Voici un .gitlab-ci.yml réel, adaptable en 10 minutes à un projet Datacampus. Il suppose que vous avez configuré les variables CI : SSH_PRIVATE_KEY, STAGING_HOST, STAGING_USER, PROD_HOST, PROD_USER.
stages:
- lint
- deploy
variables:
DEPLOY_PATH: "/var/www/vhosts/client.fr/httpdocs"
# ---- LINT : PHPCS sur le code custom ----
lint:phpcs:
stage: lint
image: php:8.2-cli
before_script:
- apt-get update && apt-get install -y git unzip
- curl -sS https://getcomposer.org/installer | php
- php composer.phar global require squizlabs/php_codesniffer
- php composer.phar global require wp-coding-standards/wpcs
script:
- ~/.composer/vendor/bin/phpcs --standard=WordPress wp-content/themes/votre-theme
only:
- merge_requests
- develop
- main
# ---- STAGING : déploiement automatique sur develop ----
deploy:staging:
stage: deploy
image: alpine:latest
before_script:
- apk add --no-cache openssh-client rsync
- eval $(ssh-agent -s)
- echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add -
- mkdir -p ~/.ssh && chmod 700 ~/.ssh
- ssh-keyscan -H "$STAGING_HOST" >> ~/.ssh/known_hosts
script:
- rsync -avz --delete
--exclude='.git' --exclude='.gitlab-ci.yml'
--exclude='wp-content/uploads' --exclude='wp-content/cache'
--exclude='.env'
./ "$STAGING_USER@$STAGING_HOST:$DEPLOY_PATH/"
- ssh "$STAGING_USER@$STAGING_HOST" "cd $DEPLOY_PATH && wp cache flush"
environment:
name: staging
url: https://preprod.client.fr
only:
- develop
# ---- PROD : déclenchement MANUEL depuis GitLab ----
deploy:prod:
stage: deploy
image: alpine:latest
before_script:
- apk add --no-cache openssh-client rsync curl
- eval $(ssh-agent -s)
- echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add -
- mkdir -p ~/.ssh && chmod 700 ~/.ssh
- ssh-keyscan -H "$PROD_HOST" >> ~/.ssh/known_hosts
script:
- rsync -avz --delete
--exclude='.git' --exclude='.gitlab-ci.yml'
--exclude='wp-content/uploads' --exclude='wp-content/cache'
--exclude='.env'
./ "$PROD_USER@$PROD_HOST:$DEPLOY_PATH/"
- ssh "$PROD_USER@$PROD_HOST" "cd $DEPLOY_PATH && wp cache flush"
# Purge Cloudflare
- curl -X POST "https://api.cloudflare.com/client/v4/zones/$CF_ZONE_ID/purge_cache"
-H "Authorization: Bearer $CF_API_TOKEN"
-H "Content-Type: application/json"
--data '{"purge_everything":true}'
# Notif Slack
- curl -X POST "$SLACK_WEBHOOK"
-H 'Content-Type: application/json'
--data "{\"text\":\"Déploiement prod client.fr OK (commit $CI_COMMIT_SHORT_SHA)\"}"
environment:
name: production
url: https://www.client.fr
when: manual
only:
- main
Le déclencheur when: manual sur le job prod impose un clic explicite depuis l'interface GitLab. C'est la différence entre « on pousse à main et ça part tout seul » et « on pousse à main, on vérifie le diff, on clique Deploy ».
7. Rafraîchir la base staging depuis la prod
Le script vit dans le dépôt (scripts/pull-prod.sh) et s'exécute côté staging. On le lance à la main avant une recette, ou via un cron hebdo.
#!/usr/bin/env bash
set -euo pipefail
# --- Variables ---
PROD_USER="client-prod"
PROD_HOST="prod.datacampus.fr"
PROD_PATH="/var/www/vhosts/client.fr/httpdocs"
PROD_URL="https://www.client.fr"
STAGING_URL="https://preprod.client.fr"
STAGING_PATH="$(pwd)"
echo "▶ Dump de la prod..."
ssh "$PROD_USER@$PROD_HOST" \
"cd $PROD_PATH && wp db export - --add-drop-table" \
| gzip > /tmp/prod-dump.sql.gz
echo "▶ Import dans la base staging..."
gunzip -c /tmp/prod-dump.sql.gz | wp db import -
rm /tmp/prod-dump.sql.gz
echo "▶ Search-replace des URLs..."
wp search-replace "$PROD_URL" "$STAGING_URL" --all-tables --skip-columns=guid
echo "▶ Sync des uploads..."
rsync -avz --delete \
"$PROD_USER@$PROD_HOST:$PROD_PATH/wp-content/uploads/" \
"$STAGING_PATH/wp-content/uploads/"
echo "▶ Désactivation des plugins 'prod only'..."
wp plugin deactivate woocommerce-stripe-gateway mailchimp-for-woocommerce 2>/dev/null || true
echo "▶ Remise en ordre des permissions (user Linux du site)..."
USER=$(stat -c '%U' .)
chown -R "$USER:$USER" .
echo "▶ Purge du cache staging..."
wp cache flush
echo "✅ Staging rafraîchie depuis la prod."
Chez Datacampus, chaque site tourne sous son propre utilisateur Linux (isolation par vhost Plesk). D'où le stat qui récupère le bon propriétaire : pas besoin de le hardcoder, le script reste portable d'un client à l'autre.
8. Hooks post-déploiement en prod
Après un rsync réussi sur prod, on enchaîne systématiquement :
wp cache flush: cache objet WP (Redis, Memcached).- Purge Cloudflare via l'API (
/zones/{id}/purge_cache). - Notification Slack ou Teams via webhook, avec le SHA du commit déployé.
- Optionnel : ping IndexNow, invalidation OPcache PHP-FPM (
cachetool opcache:reset).
9. Rollback : prévu, pas improvisé
Deux stratégies possibles selon la gravité.
Rollback léger : git revert
git revert <sha-du-commit-cassé>
git push origin main
La CI redéploie automatiquement, le temps total est de l'ordre de la minute. Propre, tracé, sans effacer l'histoire.
Rollback lourd — redéployer un tag précédent
Si on taggue chaque release prod (v2026.04.23), on peut forcer un redéploiement d'un tag antérieur :
git checkout v2026.04.22
git tag -f rollback-$(date +%Y%m%d-%H%M)
git push origin rollback-xxx
Pensez à conserver 2 ou 3 releases de retour dispos (archives côté serveur, ou simplement des tags Git). Une release, c'est trop peu ; cinq, ça devient du bruit.
10. Convention de commits
Adopter Conventional Commits n'est pas un caprice : ça permet de générer un changelog automatique, de faire des squash merges propres, et de parser les commits côté CI (release auto, semver).
feat: ajout du formulaire de contact multilingue
fix: correction du calcul de TVA sur produits variables
chore: mise à jour Composer (WP 6.5.2)
refactor: extraction du helper d'envoi mail
docs: complétion du README déploiement
ci: passage de deploy:prod en when: manual
Règle simple : une ligne de sujet < 72 caractères, verbe à l'infinitif ou au présent, pas de point final. Le corps du commit (optionnel) explique le pourquoi.
11. Code review, même à deux
La règle qui fait la différence entre une agence qui passe à l'échelle et une agence qui subit : personne ne pousse directement sur main. Ni le stagiaire, ni le tech lead, ni le patron. Tout passe par une Merge Request, relue par un pair.
Côté GitLab Datacampus, vous activez sur la branche main :
- Protection de branche (pas de push direct, pas de force-push).
- MR requise avec au moins une approbation.
- Pipeline CI vert obligatoire avant merge.
Oui, même pour une équipe de 2. Le coût marginal est faible (5 à 10 minutes par MR), le bénéfice cumulé est énorme : capture d'erreurs à la source, partage de connaissance, responsabilité partagée.
12. Backup manuel avant une migration majeure
Datacampus sauvegarde vos hébergements : c'est inclus, c'est fiable. Mais avant un changement vraiment risqué (upgrade WP 6.x → 7.x, migration d'un plugin e-commerce, refonte du schéma BDD, passage à Bedrock), faites un dump à la main juste avant, et gardez-le à portée :
ssh client-prod@prod.datacampus.fr
cd /var/www/vhosts/client.fr/httpdocs
wp db export ~/backups/pre-migration-$(date +%Y%m%d-%H%M).sql
tar czf ~/backups/uploads-pre-migration.tar.gz wp-content/uploads
Le backup Datacampus vous sauvera si la prod part en vrille la nuit. Le dump manuel vous sauve quand vous cassez quelque chose à 15 h et qu'il faut remonter en 3 minutes. Les deux sont complémentaires.
hotfix/xxx, poussez, déployez via la CI. Cinq minutes de plus, des semaines économisées.
13. Check-list de mise en route
- Dépôt créé sur
gitlab.datacampus.fr,.gitignorepropre, premier commit avec le thème custom. - Deux vhosts Plesk :
www.client.fretpreprod.client.fr(ce dernier protégé par.htpasswd). - Clé SSH de déploiement générée, clé publique ajoutée sur les deux users Linux, clé privée stockée dans les variables CI GitLab.
.gitlab-ci.ymlposé, premier pipeline vert surdevelop.- Script
pull-prod.shtesté côté staging. - Branche
mainprotégée, MR obligatoire, approbation requise. - Hook Slack et purge Cloudflare câblés sur le job
deploy:prod. - Équipe formée : plus personne ne touche en SFTP direct.
Ressources associées : Déployer via Git, Copier la prod vers la préprod, WP-CLI en ligne de commande, Migrer un WordPress vers Datacampus, Protéger un dossier par .htpasswd.