🚀 Premiers pas

Workflow staging → prod pour une agence WordPress : un exemple qui marche

Git, deux environnements, déploiement automatique, refresh ponctuel de la base. Le workflow concret qu'on voit tourner chez les agences matures qui gèrent 5 à 30 sites clients sans se faire déborder.

intermédiaire ⏱ 15 min Mise à jour : 2026-04-24

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

  1. Le code vit dans Git (gitlab.datacampus.fr, GitHub, Bitbucket — peu importe, mais un seul endroit).
  2. Deux environnements seulement : staging (preprod.client.fr, protégé par .htpasswd) et prod (www.client.fr).
  3. La base de données et les uploads sont synchronisés prod → staging ponctuellement. Jamais l'inverse.
  4. Le déploiement est automatique sur push (staging sur develop, prod sur main avec bouton manuel).
💡
Le ROI devops sur un site WordPress
Les agences qui mettent ce workflow en place divisent par trois leurs incidents de production dans les six premiers mois. C'est le ROI le plus immédiat de la culture devops sur un projet WP, avant même les tests automatisés, avant même le monitoring.

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 .env non versionné).
  • Les scripts utilitaires : pull-prod.sh, deploy.sh, .gitlab-ci.yml.
  • Un composer.json si 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é par wp-cli ou 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 dans develop.

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 developmain 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.

🔒
Désactivation systématique de Stripe / Mailchimp / webhooks externes
Une fois la base de prod importée sur staging, tous les plugins sont actifs, y compris ceux qui parlent à des API externes en production. Désactivez-les par script : Stripe, Mailchimp, connecteurs CRM, webhooks. Sinon, le premier test d'achat sur staging déclenche une vraie transaction. Vécu, vu, redouté.

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.

⚠️
Le hotfix SFTP direct en prod : la dette qui ne se rembourse jamais
« Je corrige vite ce bug en SFTP, je mettrai Git à jour après. » Non. Vous ne le mettrez pas à jour après. Trois semaines plus tard, le prochain déploiement écrase votre correctif, le bug revient, et vous perdez une heure à comprendre d'où il sort. Un workflow Git ne tolère aucune divergence silencieuse — si l'urgence est réelle, faites le correctif dans une branche 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, .gitignore propre, premier commit avec le thème custom.
  • Deux vhosts Plesk : www.client.fr et preprod.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.yml posé, premier pipeline vert sur develop.
  • Script pull-prod.sh testé côté staging.
  • Branche main proté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.

Pour aller plus loin

Besoin d'aide ?

Cette documentation ne couvre pas votre cas ? Notre support humain est là.