Table des matières :
- Objectif d’un staging PrestaShop « fidèle prod » : la parité ou rien
- Fixer une cible technique réaliste (versions PrestaShop/PHP, serveur web, dépendances)
- Architecture Docker recommandée : découpage en services, réseaux, volumes, exposition minimale
- Construire l’image PHP-FPM « comme en prod » : extensions, OPcache, FPM pool, UID/GID
- Données et configuration : cloner la prod sans fuite, gérer les secrets, neutraliser les effets de bord
- Fidélité HTTP/SEO : TLS, headers, cache, indexation, instrumentation et tests
- Checklist de validation (et anti-patterns) avant de livrer le staging aux équipes
Objectif d’un staging PrestaShop « fidèle prod » : la parité ou rien
Un staging PrestaShop utile n’est pas un bac à sable « qui marche à peu près ». C’est un environnement isopérimètre de la production : mêmes versions (PHP, serveur web, MariaDB/MySQL), mêmes extensions PHP, même chaîne de build (Composer/Front), mêmes caches, mêmes jobs asynchrones, mêmes en-têtes HTTP et même stratégie TLS. Tout le reste (staging « plus simple », « plus léger », « plus permissif ») produit des faux négatifs : la régression n’apparaît qu’en prod.
Le principe est connu et documenté depuis longtemps : le manifeste The Twelve-Factor App résume la règle ainsi : « Keep development, staging, and production as similar as possible. » (Dev/Prod Parity, https://12factor.net/dev-prod-parity). Sur PrestaShop, la difficulté n’est pas conceptuelle mais pratique : le cœur reste très stateful (fichiers générés, cache Smarty, images, logs, var/cache Symfony côté back-office), et une partie non négligeable des modules écrivent sur disque ou supposent des permissions « à la main ».
Deux dérives reviennent systématiquement : (1) le staging tourne sur Apache mod_php alors que la prod est en Nginx + PHP-FPM, (2) le staging n’a pas les mêmes caches/OPcache/Redis, donc les symptômes (CPU, latence, erreurs fatales liées aux paths) ne se reproduisent pas. Si vous voulez une pré-prod qui sert réellement à sécuriser une mise à jour, traquer un bug de module ou valider un plan SEO, vous devez accepter une contrainte : Docker ne simplifie pas la prod, il l’industrialise.
Un bon test pour vérifier que vous êtes dans la « parité » (et pas dans la « ressemblance ») : prenez un incident typique et demandez-vous s’il pourrait se reproduire en staging.
- Incident de perf : en prod, un pic de trafic sature PHP-FPM (pool trop petit) + DB (requêtes lentes). En staging, si vous n’avez ni OPcache, ni le même
pm.max_children, ni les mêmes index, vous ne verrez jamais le vrai goulot. - Incident de fichiers : un module d’export écrit dans
var/oudownload/. En staging, si vous avez un bind-mount en écriture avec777, ça « marche ». En prod (volume NFS,umask, user non-root), ça casse. - Incident SEO : un canonical, une redirection ou un header varie entre Apache et Nginx ; vous validez un comportement qui n’existe pas en prod.
Enfin, la fidélité concerne aussi le contexte : un staging accessible sur Internet, souvent hors WAF, parfois ouvert à plus d’adresses IP que la prod, devient un risque (données personnelles, scans, indexation accidentelle). La parité n’est pas que technique : c’est aussi une parité d’exploitation (réseau, contrôle d’accès, logs, supervision).
Fixer une cible technique réaliste (versions PrestaShop/PHP, serveur web, dépendances)
Avant d’écrire une ligne de docker-compose.yml, choisissez une matrice PrestaShop ⇄ PHP ⇄ DB cohérente avec votre production. Exemple concret (à adapter) : PrestaShop 8.x (ou la version majeure utilisée en production), PHP 8.2/8.3 (selon compatibilité de votre stack et de vos modules), MariaDB 10.6+ ou MySQL 8.0, et un frontal Nginx avec PHP-FPM. L’objectif n’est pas d’être « à jour », c’est d’être identique à prod. Si votre prod est sur PHP 8.1, copier en staging un PHP 8.3 est une erreur méthodologique.
Le plus fiable est de partir des prérequis officiels PrestaShop, puis de figer vos versions à l’identique de la prod (images Docker taguées, et idéalement digests immuables) : https://devdocs.prestashop-project.org/ (rubrique System requirements selon version). Cela évite le piège des « upgrades invisibles » : une image mariadb:10.11 peut évoluer dans le temps, tout comme php:8.2-fpm.
Le choix Apache vs Nginx n’est pas cosmétique. Entre mod_php, proxy_fcgi et PHP-FPM, la gestion des headers, du buffering, des timeouts, des limites d’upload et même la façon dont les PATH_INFO sont propagés peut diverger. Si vous êtes sur Nginx + FPM en prod, alignez le staging. La plupart des 403/404 « mystérieuses » viennent de divergences de réécritures et de résolution de script (try_files, fastcgi_param SCRIPT_FILENAME, gestion des URL encodées, etc.).
Côté base de données, soyez attentif aux différences MySQL/MariaDB qui ne se voient pas tout de suite :
- Collations : une base créée en MySQL 8 peut utiliser
utf8mb4_0900_ai_ci, non supportée telle quelle par certaines versions de MariaDB. Un import « marche » puis certains tris/comparaisons changent. - SQL modes :
STRICT_TRANS_TABLES,ONLY_FULL_GROUP_BYet consorts peuvent modifier le comportement d’anciennes requêtes (modules legacy). - Timezones : un staging sans timezone alignée peut fausser la TVA, les règles de panier, ou les exports.
Enfin, listez explicitement les extensions PHP et libs système attendues : intl, gd (ou imagick si vous l’utilisez), zip, pdo_mysql, opcache, soap (encore utilisé par certains ERP), sodium, etc. Pour vous éviter les oublis, une méthode simple consiste à exporter la liste depuis la prod (php -m, php -i) et à la comparer automatiquement en staging (diff en CI).
En staging fidèle, on n’ajoute pas Xdebug dans l’image qui simule la prod : on le met dans un profil dédié. Idem pour display_errors, qui doit rester off si vous cherchez une parité comportementale (sinon vous « normalisez » des warnings/notices qui ne remonteront jamais de la même façon en prod).
Architecture Docker recommandée : découpage en services, réseaux, volumes, exposition minimale
Un staging « prod-like » évite l’image monolithique qui fait tout. Découpez en services : nginx (front), php (FPM + code), db (MariaDB/MySQL), redis (cache/sessions si utilisé), éventuellement search (Elasticsearch/Meilisearch selon votre moteur), et un service mail de capture (MailHog/Mailpit) pour éviter les emails réels. Docker le permet précisément parce que, comme le rappelle la doc : « A container is a standard unit of software that packages up code and all its dependencies… » (https://docs.docker.com/get-started/).
Structurez vos réseaux : un réseau internal non exposé (DB, Redis), et un réseau edge exposé (Nginx ou reverse-proxy). Ne publiez que le port HTTP/HTTPS nécessaire. Dans Compose, utilisez ports: uniquement pour le frontal ; pour le reste, préférez expose: (accessible aux autres conteneurs, pas à l’hôte).
Ajoutez des healthcheck et des dépendances explicites pour éviter un staging « up » alors que la DB n’est pas prête. Attention : depends_on ne garantit pas « prêt à servir » ; combinez-le avec de vrais checks (ping DB, endpoint HTTP /health, etc.). En pratique, sur PrestaShop, c’est aussi un moyen de détecter tôt un problème de migrations ou de permissions (le container « tourne », mais l’app est en erreur 500).
Le point le plus sous-estimé : les volumes. PrestaShop mélange code, médias et runtime. En staging, isolez :
- le code (idéalement en lecture seule, pour coller à un déploiement immuable),
- les médias (
img/,download/,upload/) en volume persistant, - le runtime (
var/, cache Smarty, logs) en volume persistant mais jetable si vous faites des resets.
Évitez de binder tout le dossier projet en écriture sous Linux : vous récupérez des soucis de UID/GID et des permissions incohérentes, puis vous « corrigez » au chmod -R 777, ce qui tue la valeur du staging (et augmente la surface de risque).
Exemple minimal (à adapter) :
services:
nginx:
image: nginx:1.27-alpine
ports: ["8080:80"]
volumes:
- ./docker/nginx/conf.d:/etc/nginx/conf.d:ro
- prestashop_media:/var/www/html/img
- prestashop_runtime:/var/www/html/var
depends_on:
- php
networks: [edge, internal]
read_only: true
tmpfs:
- /var/cache/nginx
php:
build: ./docker/php
environment:
PS_DOMAIN: staging.local
volumes:
- ./app:/var/www/html:ro
- prestashop_media:/var/www/html/img
- prestashop_runtime:/var/www/html/var
healthcheck:
test: ["CMD-SHELL", "php -v || exit 1"]
interval: 10s
timeout: 3s
retries: 3
networks: [internal]
db:
image: mariadb:10.11
environment:
MARIADB_DATABASE: prestashop
MARIADB_USER: prestashop
MARIADB_PASSWORD: prestashop
MARIADB_ROOT_PASSWORD: root
volumes:
- db_data:/var/lib/mysql
networks: [internal]
networks:
edge:
internal:
internal: true
volumes:
db_data:
prestashop_media:
prestashop_runtime:
Deux détails qui font une vraie différence en « fidélité prod » :
internal: trueempêche l’exposition accidentelle de la DB/Redis hors du réseau Docker.read_only: true+tmpfssur Nginx vous force à identifier ce qui doit vraiment être écrit (logs, cache), au lieu de laisser le conteneur « bricoler » sur disque.
Construire l’image PHP-FPM « comme en prod » : extensions, OPcache, FPM pool, UID/GID
Si votre prod repose sur une image PHP custom, le staging doit consommer la même image (même Dockerfile, même tags/digests). Ne partez pas d’une image générique différente « parce que c’est plus rapide ». Une base saine : php:8.2-fpm (ou version alignée prod) + installation contrôlée des extensions, + un php.ini versionné. Les réglages OPcache doivent être cohérents : en staging fidèle, vous cherchez à reproduire les effets de cache et de realpath_cache, pas à les masquer.
Exemple (très) simplifié de Dockerfile orienté « reproductibilité » :
FROM php:8.2-fpm
# Paquets système nécessaires à des extensions courantes (gd, intl, zip…)
RUN apt-get update && apt-get install -y --no-install-recommends \
libicu-dev libzip-dev libpng-dev libjpeg62-turbo-dev libfreetype6-dev \
&& docker-php-ext-configure gd --with-freetype --with-jpeg \
&& docker-php-ext-install -j$(nproc) intl zip pdo_mysql gd opcache \
&& rm -rf /var/lib/apt/lists/*
# php.ini versionné (parité)
COPY ./php.ini /usr/local/etc/php/conf.d/zzz-app.ini
# Pool FPM versionné
COPY ./www.conf /usr/local/etc/php-fpm.d/www.conf
# User non-root (adaptez UID/GID à votre contexte)
RUN usermod -u 1000 www-data && groupmod -g 1000 www-data
USER www-data
Sur OPcache, évitez les defaults. Une config réaliste ressemble souvent à :
opcache.enable=1opcache.memory_consumption=128(ou plus selon votre codebase + modules)opcache.interned_strings_buffer=16opcache.max_accelerated_files=20000(ordre de grandeur)opcache.validate_timestamps=0si vous déployez de façon immuable (image reconstruite à chaque release), sinon1avec unrevalidate_freqcohérent.
Ce n’est pas un détail : des bugs n’apparaissent qu’avec OPcache activé (classes legacy chargées dans un ordre différent, chemins résolus différemment, fichiers générés mais non invalidés). Si votre prod a OPcache, votre staging doit l’avoir, avec des valeurs proches.
Côté FPM, copiez les paramètres critiques de prod : pm (dynamic/ondemand), pm.max_children, request_terminate_timeout, php_admin_value[memory_limit], catch_workers_output, etc. Un pm.max_children sous-dimensionné en staging vous donne un faux sentiment de « lenteur applicative » alors que c’est une saturation du pool. À l’inverse, un pool surdimensionné masque des lenteurs SQL et vous évite d’ouvrir le vrai chantier.
Un micro-exemple de points à figer dans www.conf (à adapter) :
pm = dynamic
pm.max_children = 20
pm.start_servers = 4
pm.min_spare_servers = 2
pm.max_spare_servers = 6
request_terminate_timeout = 120
catch_workers_output = yes
php_admin_value[memory_limit] = 512M
Enfin, ne négligez pas l’identité Unix. Faites tourner php-fpm avec un user non-root, et alignez UID/GID avec votre pipeline de build/CI ou votre volume NFS si vous en avez un. Ça évite les surprises : fichiers générés illisibles par Nginx, cache Smarty non effaçable, imports médias qui échouent silencieusement.
Pour le débogage, gardez un profil séparé (Compose profiles: ou une deuxième image) qui ajoute Xdebug. Mélanger « image prod-like » et « image debug » est le meilleur moyen de biaiser vos validations (performances, timeouts, consommation mémoire).
Données et configuration : cloner la prod sans fuite, gérer les secrets, neutraliser les effets de bord
Un staging fidèle n’a aucun intérêt si les données sont fantaisistes… mais il devient dangereux si vous clonez la prod sans garde-fous. La règle : dump DB + médias, puis anonymisation (emails, téléphones, adresses), puis rotation des secrets (clés cookie, webservice, tokens), puis neutralisation des intégrations externes. Sur PrestaShop, beaucoup de modules stockent leurs clés en base : vous devez auditer ps_configuration et les tables de modules (Stripe/PayPal/ERP/SMTP, etc.). L’approche « je clone et je croise les doigts » finit tôt ou tard en envoi d’emails aux vrais clients.
Côté anonymisation, cherchez l’équilibre : garder la structure (nombre de commandes, paniers, déclinaisons, historiques) tout en supprimant le caractère identifiant. Exemple de stratégie simple (à exécuter uniquement sur la base de staging, après import, et après sauvegarde) :
-- Exemple : remplacer emails par un domaine réservé à cet usage (RFC 2606 : .invalid)
UPDATE ps_customer
SET email = CONCAT('user+', id_customer, '@example.invalid'),
firstname = 'Client',
lastname = CONCAT('Staging-', id_customer);
UPDATE ps_address
SET firstname = 'Client',
lastname = 'Staging',
address1 = 'Adresse anonymisée',
postcode = '00000',
city = 'Ville',
phone = NULL,
phone_mobile = NULL;
Pour les secrets, évitez de committer des .env pleins de clés. Utilisez au minimum un .env non versionné et injectez les secrets via variables d’environnement, ou via mécanismes dédiés (Docker secrets en Swarm, ou un vault en CI/CD). Ce n’est pas optionnel : un staging est souvent plus accessible qu’une prod (IP autorisées plus larges, moins de WAF), donc plus exposé.
Neutralisez les effets de bord :
- Emails : routez tout vers un collecteur (Mailpit/MailHog) ou désactivez l’envoi SMTP externe.
- Paiement : forcez le mode sandbox, et vérifiez que les clés prod ne sont pas présentes en base.
- Webhooks / ERP / CRM : remplacez les endpoints par des URL de test, ou coupez le module.
- Flux (marketplaces, comparateurs) : désactivez les exports automatiques si le staging est accessible.
PrestaShop exécute aussi des traitements via cron (indexation, emails, exports, modules). En staging, si vous ne déclenchez jamais les cron, vous ne testez pas la moitié du comportement. Le pattern propre : un container cron qui exécute crond et appelle les endpoints nécessaires, ou un scheduler externe qui cible le staging avec une IP de confiance. Dans les deux cas, loggez (stdout, fichiers, ou centralisation) : un staging silencieux est un staging inutile.
Un dernier point « opérationnel » souvent oublié en contexte FR/UE : si votre staging contient encore des données clients même pseudonymisées, documentez qui y a accès (prestataire, agence, interne), et appliquez des règles cohérentes (durée de conservation, rotation, suppression). Ce n’est pas du juridique théorique : c’est une condition pour garder un staging exploitable sans créer une zone grise.
Fidélité HTTP/SEO : TLS, headers, cache, indexation, instrumentation et tests
Si votre production est en HTTPS avec des redirections strictes, le staging doit reproduire la chaîne : terminaison TLS (reverse-proxy), redirection HTTP→HTTPS, headers de sécurité, et comportement canonical (domaine, slash, trailing). Sinon vous validez une page en HTTP et vous cassez en prod via une règle HSTS mal posée ou un mix content.
Le staging doit aussi être inindexable par défaut. Ne vous contentez pas d’un robots.txt : c’est un signal, pas un contrôle d’accès. Protégez par authentification HTTP, IP allowlist, ou réseau privé. Et ajoutez un X-Robots-Tag: noindex, nofollow au niveau du reverse-proxy (ou un meta robots si vous contrôlez les templates). Google est explicite sur le mécanisme : « To prevent your content from appearing in Google Search results, add a noindex tag to the page. » (Google Search Central, https://developers.google.com/search/docs/crawling-indexing/block-indexing). En staging, vous voulez zéro découverte, pas « probablement pas indexé ».
Exemple minimal côté Nginx (à adapter) :
server {
listen 80;
server_name staging.example.test;
# Protection simple (mieux : SSO, VPN ou allowlist IP)
auth_basic "Staging";
auth_basic_user_file /etc/nginx/.htpasswd;
add_header X-Robots-Tag "noindex, nofollow, noarchive" always;
location ~ \.php$ {
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME /var/www/html$fastcgi_script_name;
fastcgi_pass php:9000;
}
location / {
try_files $uri $uri/ /index.php?$args;
}
}
Note de terrain : privilégiez un domaine réservé au test (ex. .test) plutôt que .local, qui peut interagir avec mDNS/Bonjour selon les postes et compliquer les tests côté agences/clients.
Dernier point : la performance et l’observabilité. Si la prod utilise Redis, CCC, cache Smarty et éventuellement CDN, votre staging doit au minimum reproduire la configuration applicative pour éviter de valider des pages « rapides » qui ne le seront plus (ou l’inverse). L’objectif n’est pas d’avoir « de bonnes perfs en staging », mais de pouvoir comparer :
- temps de réponse (TTFB),
- erreurs applicatives (500, timeouts),
- saturation FPM (process busy),
- latences DB (slow queries),
- hit/miss de cache (si vous instrumentez).
Concrètement, gardez au moins :
- des logs Nginx exploitables (format incluant
$request_time/$upstream_response_time), - les logs PHP-FPM (erreurs + timeouts),
- un slow query log DB activable à la demande,
- et des smoke tests (pages clés, tunnel commande, back-office login) exécutés automatiquement après déploiement.
Checklist de validation (et anti-patterns) avant de livrer le staging aux équipes
Vérifiez la parité de manière auditable : comparez php -v, php -m, le php.ini effectif (php --ini), la version DB, les paramètres de collation/charset, et les modules chargés. Contrôlez aussi les timeouts : fastcgi_read_timeout, client_max_body_size, max_execution_time, post_max_size, upload_max_filesize. Une validation simple consiste à versionner un script de diagnostic qui sort ces infos et à l’exécuter en prod et staging (même commande, même diff). Vous évitez ainsi les discussions « chez moi ça marche ».
Mini-script (exécutable dans le container PHP) pour produire une empreinte comparable :
php -v
php -m | sort
php -i | grep -E 'Loaded Configuration File|Scan this dir for additional|memory_limit|upload_max_filesize|post_max_size|opcache.enable|opcache.validate_timestamps'
mysql --version 2>/dev/null || true
Checklist rapide « avant ouverture » (équipes dev/SEO/ops) :
- [ ] Domaine et canonical alignés (pas de redirections surprenantes)
- [ ]
X-Robots-Tag: noindexet contrôle d’accès actif - [ ] Sortie email neutralisée (capture locale ou SMTP de test)
- [ ] Paiement sandbox confirmé (aucune clé prod en DB)
- [ ] Cron exécuté (au moins sur un cycle), avec logs consultables
- [ ] OPcache activé et configuré comme en prod
- [ ] DB importée + anonymisée + collations vérifiées
- [ ] Volumes séparés (médias vs runtime), pas de
777correctif - [ ] Ports exposés minimaux (DB non publiée)
Anti-patterns fréquents (et coûteux) : (1) staging public sans auth + simple Disallow: / (un crawler tiers indexera quand même), (2) staging sur une stack « simplifiée » (Apache au lieu de Nginx-FPM, pas de Redis, pas d’OPcache), (3) image PHP avec Xdebug activé en permanence (vous faussez les temps), (4) bind-mount du projet en écriture avec permissions bricolées, (5) pas de cron, donc vous ne testez ni emails ni traitements différés, (6) paiement réel non neutralisé, (7) secrets réutilisés (cookie key, webservice key), ce qui est une faute opérationnelle.
Quand le staging est réellement fidèle, vous pouvez l’utiliser pour des validations qui ont de la valeur : upgrade PrestaShop, upgrade PHP, refonte thème, ajout d’un module, changement de règles de cache, ou tests de réécritures SEO. Et surtout, vous pouvez documenter un protocole d’ouverture : déploiement immuable, import DB anonymisée, purge contrôlée des caches, exécution des migrations, exécution des tests, puis ouverture restreinte. C’est cette discipline — pas Docker en lui-même — qui transforme un staging PrestaShop en outil de réduction de risque.




