Le multisite WordPress est souvent présenté comme l'outil des réseaux de blogs d'université ou des marques à filiales. On l'utilise aussi, plus modestement, pour héberger quelques sites indépendants sur une seule base de code. C'est notre cas : un blog consacré à l'Atari ST et un comparateur de prix du contrôle technique tournent sur la même installation, chacun sur son propre sous-domaine. Voici ce que nous avons appris en l'exploitant.
Ce qu'un multisite partage, et ce qu'il sépare
Comprendre la répartition des tables évite la moitié des erreurs. Un réseau multisite repose sur une seule base de données, mais pas sur un seul jeu de tables :
- Tables globales, communes à tout le réseau :
wp_usersetwp_usermeta(un compte existe une fois pour tous les sites),wp_blogs(la liste des sites et leurs domaines),wp_siteetwp_sitemeta(les réglages du réseau). - Tables par site, préfixées par le numéro du site :
wp_2_posts,wp_2_postmeta,wp_2_options,wp_2_terms, etc.
Le site n° 1 fait exception : il utilise les tables wp_posts, wp_options… sans numéro. De même, ses fichiers envoyés vont directement dans wp-content/uploads/, quand ceux du site n° 2 vont dans wp-content/uploads/sites/2/. Cette asymétrie a des conséquences concrètes : un script qui cherche les options de « tous les sites » en bouclant sur wp_N_options oubliera le premier.
Des domaines sans rapport, sans extension
Le multisite propose deux modes à l'installation : sous-répertoires (exemple.fr/site2/) ou sous-domaines (site2.exemple.fr). Ce choix, fixé par la constante SUBDOMAIN_INSTALL, est beaucoup moins contraignant qu'il n'y paraît : depuis WordPress 4.5, chaque site peut recevoir un domaine arbitraire depuis l'administration du réseau, sans extension de « domain mapping ».
Notre configuration en est l'illustration. Le réseau est déclaré en mode sous-répertoires :
define( 'MULTISITE', true );
define( 'SUBDOMAIN_INSTALL', false );
define( 'DOMAIN_CURRENT_SITE', getenv_docker('WP_DOMAIN', 'greenlog.fr') );
define( 'PATH_CURRENT_SITE', '/' );
Ce choix a une raison pratique : en développement, tout le réseau tourne sur localhost:8084, où les sous-domaines ne se résolvent pas sans bricolage du fichier hosts. En production, chaque site a pourtant son propre domaine, renseigné dans sa fiche : atari.greenlog.fr pour l'un, mon-controle-technique.greenlog.fr pour l'autre. Le domaine du réseau lui-même est lu dans une variable d'environnement, ce qui permet au même fichier de configuration de servir en local et en production.
Le site principal n'est pas un site comme les autres
Le site n° 1 porte le rôle de site principal du réseau. On ne peut pas le supprimer, et c'est sur son domaine que vit l'administration du réseau. Notre histoire l'illustre : à l'origine, le site n° 1 était servi sur greenlog.fr. Lorsque nous avons voulu faire de ce domaine racine un portail indépendant, construit avec Laravel, le blog du site n° 1 a été déplacé sur atari.greenlog.fr, et Nginx route désormais greenlog.fr vers une autre application.
Cette bascule est possible, mais elle demande de mettre à jour le domaine du site dans wp_blogs, ses options siteurl et home, et toutes les URL absolues stockées dans son contenu. Et elle laisse une singularité à documenter : le domaine du réseau (DOMAIN_CURRENT_SITE) n'est plus celui où le site principal est servi. Rien ne casse, mais c'est le genre de détail qu'un intervenant découvrant la configuration doit trouver écrit quelque part.
Remplacer des URL : toujours par site
Le contenu WordPress stocke des URL absolues, parfois dans des données sérialisées où une substitution SQL naïve casse la longueur déclarée des chaînes. L'outil adapté est wp search-replace, qui gère la sérialisation. En multisite, il faut soit cibler un site par --url, soit traiter tout le réseau avec --network — en sachant exactement ce que l'on remplace. Un dry run (--dry-run) avant toute exécution réelle n'est pas optionnel.
Options par site : le piège de la mauvaise table
Chaque site a sa table d'options. Notre comparateur de prix appelle une API interne dont l'adresse est une option du site n° 2 : elle vit dans wp_2_options, pas dans wp_options. Deux conséquences :
- Pour le code,
get_option()lit l'option du site courant,get_site_option()celle du réseau. Une extension qui utilise l'une à la place de l'autre fonctionnera sur le site principal et échouera silencieusement ailleurs. - Pour l'exploitation, modifier une option en SQL exige de connaître le numéro du site. Une requête sur
wp_options« parce que c'est la table des options » modifie le mauvais site.
Détail qui compte : cette adresse d'API pointe vers le nom du conteneur sur le réseau Docker interne, et non vers le domaine public. Passer par l'extérieur faisait traverser à chaque appel le proxy et son limiteur de débit, avec des refus 429 à la clé. Nous avons détaillé ce découplage dans le guide découpler WordPress d'une API externe.
Extensions : disponibles partout, actives là où il faut
Une extension installée l'est pour tout le réseau, puisque le code est commun. Son activation, en revanche, se décide site par site — sauf « activation réseau », qui l'impose partout. Notre règle : n'activer au niveau réseau que ce qui doit l'être par nature (cache objet, durcissement de sécurité), et activer le reste site par site. L'extension métier du comparateur de prix n'a rien à faire sur le blog Atari : l'y activer ajouterait des requêtes, des tâches planifiées et une surface d'attaque sans aucun bénéfice.
La contrepartie est à garder en tête lors des mises à jour : mettre à jour une extension la met à jour pour tous les sites qui l'utilisent. Un correctif pensé pour l'un peut casser l'autre. Tester sur chacun des sites concernés fait partie de la procédure.
WP-CLI : une commande, un site
C'est le piège le plus coûteux, parce qu'il ne produit aucune erreur. Toute commande WP-CLI s'exécute dans le contexte d'un seul site, désigné par --url. Sans ce paramètre, elle agit sur le site principal. Une purge de cache, une mise à jour d'option ou l'exécution des tâches planifiées lancée sans --url semble réussir… sur le mauvais site.
Notre conteneur de tâches planifiées l'illustre : il exécute explicitement une commande par site.
wp cron event run --due-now --url=atari.greenlog.fr
wp cron event run --due-now --url=mon-controle-technique.greenlog.fr
En développement local, la même commande ne cible que localhost:8084, donc le site principal : les tâches du second site n'y tournent pas. C'est acceptable parce que c'est su ; ce serait un défaut s'il fallait le découvrir en cherchant pourquoi une tâche ne s'exécute pas. Le sujet est développé dans notre guide sur le remplacement de WP-Cron par une tâche système.
Sauvegarde et restauration
Une seule base de données, c'est une sauvegarde simple — et une restauration délicate. Restaurer tout le dump pour réparer un site ramène l'autre en arrière. Pour pouvoir restaurer un site isolément, il faut savoir extraire ses tables (wp_2_* pour le site n° 2) et son dossier d'uploads, en gardant à l'esprit que les utilisateurs, eux, sont dans des tables communes. Il vaut mieux avoir répété cette opération une fois à froid que la découvrir pendant un incident.
Alors, multisite ou pas ?
Pour nous, le bilan est positif : un seul cœur à mettre à jour, un seul conteneur applicatif, un seul cache objet Redis, des extensions maison développées une fois. Mais ce bilan repose sur des conditions précises : un seul administrateur, des sites de taille modeste, et une discipline stricte sur --url et sur les tables par site. Pour des sites confiés à des équipes différentes, avec des cycles de mise à jour distincts ou des exigences d'isolement fortes, des installations séparées restent le choix le plus sûr.
- Choisissez le multisite si les sites partagent du code maison, une équipe et un calendrier de maintenance.
- Évitez-le si un site doit pouvoir être cédé, migré ou restauré indépendamment sans effort, ou si ses extensions sont incompatibles avec celles des autres.