Aller au contenu principal
Retour au blog

Docker macOS + Symfony : VirtioFS, Mutagen, DDEV — le benchmark

Flavien Métivier18 juin 202410 min

Si tu travailles avec Docker sur macOS et un projet Symfony de taille réelle, tu as forcément rencontré ce mur : le serveur de dev qui met 8 secondes à répondre, le cache:warmup qui s'étire sur 20 secondes, le composer install qui s'étale sur plusieurs minutes. Ce n'est pas une mauvaise configuration de ta part — c'est une limite structurelle documentée et quantifiée. Le CNCF l'a formalisé dans un article de février 2023, madewithlove l'a mesuré sur des projets PHP réels : les volumes Docker sur macOS peuvent générer des latences I/O jusqu'à dix fois supérieures à Linux natif sur des répertoires à forte densité de fichiers. Symfony, avec ses 4 000+ fichiers en vendor et son cache compilé, est précisément dans cette catégorie critique. Trois approches se disputent aujourd'hui le titre de meilleure solution : VirtioFS (natif Docker Desktop, zéro configuration), Mutagen (synchronisation bidirectionnelle asynchrone), et DDEV (environnement clé en main). Je les ai toutes les trois confrontées sur un vrai projet Symfony 7.1 en production. Voici ce que j'ai mesuré, et surtout, comment choisir selon ton contexte.

Pourquoi macOS plombe structurellement les volumes Docker

Docker sur macOS ne fait pas tourner les conteneurs directement sur le système hôte — il passe obligatoirement par une machine virtuelle Linux légère (LinuxKit) gérée via Apple Hypervisor Framework. Chaque accès fichier entre ton code sur macOS et le système de fichiers du conteneur traverse ce pont VM, avec traduction de syscalls et copie de données entre deux mondes radicalement distincts.

Jusqu'à Docker Desktop 4.6, le mécanisme par défaut était osxfs, une implémentation FUSE en espace utilisateur catastrophiquement coûteuse en context switches. Docker Desktop 4.6 a introduit gRPC FUSE comme alternative — mieux, mais toujours en espace utilisateur. Un simple scandir() récursif sur le vendor Symfony pouvait prendre 15 à 20 secondes là où Linux l'exécute en 0,3 seconde.

Le problème est fondamentalement architectural : sur Linux, Docker accède aux fichiers en natif, le démon tourne sur le même OS. Sur macOS, chaque accès fichier depuis un conteneur déclenche une cascade — conteneur → VM Linux → pont FUSE → macOS APFS. Multiplie par les milliers d'appels stat(), open(), read() que PHP émet au démarrage pour résoudre l'autoloading Composer, et le feedback loop devient insoutenable.

Le setup de benchmark : Symfony 7.1, PHP 8.3, 4 200 fichiers

Pour ce comparatif, j'ai utilisé une API Platform 3.2 sur Symfony 7.1 : Doctrine ORM, Messenger, un bundle maison, vendor complet (4 200 fichiers, 68 MB). PHP 8.3.8 avec OPcache désactivé pour isoler la charge I/O pure. Trois métriques ont été retenues pour couvrir différents profils d'usage :

  • Temps cache:warmup : proxy direct de la charge I/O sur les 1 000+ fichiers YAML/PHP de configuration Symfony
  • Temps de première requête HTTP (cache froid) via curl chronométré : ressenti développeur au quotidien
  • Temps composer install (vendor absent) : proxy de la lecture/écriture intensive sur des milliers de fichiers simultanément

Hardware : MacBook Pro M3 Pro, 18 GB RAM, macOS 14.5 Sonoma. Docker Desktop 4.31 avec les limites de ressources suivantes : 8 CPUs, 8 GB RAM. Chaque mesure est la médiane de cinq runs à froid, services Docker redémarrés entre chaque run.

# Script de benchmark utilisé (identique pour les trois approches)

# Métrique 1 : cache:warmup
docker compose exec php bin/console cache:clear --no-warmup 2>/dev/null
time docker compose exec php bin/console cache:warmup

# Métrique 2 : première requête HTTP (cache froid)
docker compose exec php rm -rf var/cache/dev
time curl -s -o /dev/null -w "%{time_total}s\n" http://localhost:8080/api

# Métrique 3 : composer install depuis zéro
docker compose exec php rm -rf vendor
time docker compose exec php composer install \
  --no-interaction \
  --prefer-dist \
  --optimize-autoloader

VirtioFS — le meilleur compromis natif, zéro friction

VirtioFS est la technologie de partage de fichiers introduite par Docker Desktop, basée sur le protocole VirtIO standard — le même protocole que QEMU/KVM utilisent sur Linux depuis des années. Contrairement à gRPC FUSE qui passait par un daemon en espace utilisateur, VirtioFS implémente le partage au niveau du noyau de la VM, réduisant drastiquement les context switches et la latence de chaque syscall. Depuis Docker Desktop 4.22 (octobre 2023), VirtioFS est le mode par défaut sur Apple Silicon. Sur Intel, active-le manuellement : Settings → General → Virtual file sharing → VirtioFS. Un redémarrage de Docker Desktop suffit — ton docker-compose.yml ne change pas d'une virgule.

# Vérifier que VirtioFS est bien actif
docker info | grep -i "virtual file"
# Attendu : Virtual file sharing: VirtioFS

# Ton docker-compose.yml existant fonctionne tel quel
# Les bind mounts classiques bénéficient automatiquement de VirtioFS
docker compose up -d

C'est l'avantage décisif de VirtioFS : il est entièrement transparent. Pas de wrapper, pas de volume nommé supplémentaire, pas de processus de synchronisation à démarrer. Les résultats sur notre benchmark Symfony 7.1 : cache:warmup 4,1 s (contre 22 s en gRPC FUSE), première requête 310 ms, composer install 48 s. Un facteur ×5 d'amélioration pour zéro effort opérationnel — c'est le ratio effort/gain le plus favorable de ce comparatif.

Mutagen — synchronisation asynchrone, performances quasi-natives

Mutagen adopte une philosophie radicalement différente : plutôt que d'optimiser le partage de fichiers entre macOS et la VM, il élimine le problème à la racine. Tes fichiers sont synchronisés vers un volume Docker natif Linux. Le conteneur lit et écrit sur ce volume local — pas de pont VM, pas de latence réseau, pas de FUSE. La synchronisation est bidirectionnelle et asynchrone : une modification dans ton éditeur (côté macOS) ou dans le conteneur (génération de cache Symfony) est propagée en quelques dizaines de millisecondes.

# Installation de Mutagen et de son wrapper Compose
brew install mutagen-io/mutagen/mutagen
brew install mutagen-io/mutagen/mutagen-compose

# Vérification
mutagen version       # ex: 0.17.6
mutagen-compose version
# docker-compose.yml adapté pour Mutagen
services:
  php:
    build:
      context: .
      dockerfile: docker/php/Dockerfile
    volumes:
      - app-sync:/var/www/html
    environment:
      APP_ENV: dev
    depends_on:
      - db

  nginx:
    image: nginx:1.27-alpine
    volumes:
      - app-sync:/var/www/html:ro
    ports:
      - "8080:80"
    depends_on:
      - php

  db:
    image: postgres:16-alpine
    volumes:
      - db-data:/var/lib/postgresql/data
    environment:
      POSTGRES_DB: app
      POSTGRES_USER: app
      POSTGRES_PASSWORD: "${DB_PASSWORD}"

volumes:
  app-sync:
  db-data:

# Extension Mutagen — ignorée par docker compose standard
x-mutagen:
  sync:
    defaults:
      ignore:
        vcs: true
        paths:
          - "var/cache"
          - "var/log"
          - ".php-cs-fixer.cache"
    app:
      alpha: "."
      beta: "volume://app-sync"
      mode: "two-way-resolved"
      configurationBeta:
        permissions:
          defaultOwner: "id:1000"
          defaultGroup: "id:1000"
          defaultFileMode: "0644"
          defaultDirectoryMode: "0755"
# Démarrer avec Mutagen (remplace docker compose up)
mutagen-compose up -d

# Vérifier l'état de la synchronisation (attendre "Watching for changes")
mutagen sync list

# Arrêt propre (nettoie aussi les sessions Mutagen)
mutagen-compose down

Deux points d'attention critiques avec Mutagen. D'abord, la synchronisation initiale : au premier up, Mutagen copie l'intégralité du projet vers le volume Docker. Sur un Symfony avec vendor, compte 20 à 40 secondes. C'est un coût unique par démarrage à froid — les modifications incrémentales suivantes sont quasi-instantanées. Ensuite, l'exclusion de var/cache et var/log est indispensable : ces répertoires sont écrits intensément par Symfony (centaines de fichiers à chaque warmup) et n'ont aucun intérêt à être reflétés côté macOS. Les ignorer réduit aussi le risque de conflits de synchronisation lors d'un cache:clear. Résultats mesurés : cache:warmup 2,3 s, première requête 162 ms, composer install 31 s — soit 85 à 90 % des performances Linux natif.

Besoin d'un expert Symfony ?

Réserver un appel

DDEV — plug-and-play avec Mutagen intégré

DDEV est un environnement de développement local complet conçu pour PHP. Il gère Docker, nginx, PHP-FPM, MariaDB/PostgreSQL, Mailpit (remplacement de Mailhog), les certificats HTTPS locaux via mkcert, et depuis la version 1.21 (mi-2023), Mutagen est activé par défaut sur macOS. Tu obtiens les performances de la synchronisation asynchrone sans une ligne de configuration supplémentaire.

# Installation
brew install ddev/ddev/ddev

# Dans ton projet Symfony existant
cd /path/to/my-symfony-project
ddev config \
  --project-type=symfony \
  --php-version=8.3 \
  --docroot=public \
  --database=mariadb:10.11

ddev start
# .ddev/config.yaml — versionnable et partageable en équipe
name: my-symfony-project
type: symfony
docroot: public
php_version: "8.3"
webserver_type: nginx-fpm
database:
  type: mariadb
  version: "10.11"
nodejs_version: "20"
mutagen_enabled: true  # Défaut sur macOS depuis DDEV 1.21

hooks:
  post-start:
    - exec: "composer install --no-interaction --prefer-dist --optimize-autoloader"
    - exec: "php bin/console doctrine:migrations:migrate --no-interaction --allow-no-migration"
    - exec: "php bin/console cache:warmup"
# Commandes DDEV au quotidien sur Symfony
ddev exec bin/console cache:clear
ddev exec bin/console make:entity
ddev exec composer require symfony/uid
ddev exec bin/phpunit --testdox

# Accès BDD direct
ddev mysql
ddev redis-cli  # si Redis configuré

# URLs générées automatiquement avec HTTPS
ddev describe
# → https://my-symfony-project.ddev.site (avec certificat valide local)

L'atout majeur de DDEV est l'onboarding zéro friction pour une équipe : le .ddev/config.yaml versionné dans le dépôt remplace trois pages de README Docker. Un développeur qui rejoint le projet fait brew install ddev && ddev start — et c'est opérationnel en moins de 10 minutes, certificats HTTPS inclus. En revanche, DDEV impose sa propre structure et peut générer de la friction si ta stack a des besoins très spécifiques : services exotiques, réseau multi-compose complexe, contraintes d'image particulières. Résultats mesurés : cache:warmup 2,6 s, première requête 185 ms, composer install 34 s — des performances quasi identiques à Mutagen pur, à moins de 15 % près. L'intégration de Mutagen par DDEV est solide et n'ajoute pas de surcoût significatif.

Bilan comparatif

Approche             | cache:warmup | 1re requête | composer install | Config
---------------------|:------------:|:-----------:|:----------------:|:--------:
gRPC FUSE (ancien)   |    22,0 s    |  ~1 800 ms  |   ~3 min 40 s    | Aucune
VirtioFS             |     4,1 s    |    310 ms   |       48 s       | Aucune
Mutagen              |     2,3 s    |    162 ms   |       31 s       | Modérée
DDEV + Mutagen       |     2,6 s    |    185 ms   |       34 s       | Faible

VirtioFS divise les temps par 5 en moyenne sans aucune modification de ta stack. Mutagen pousse les performances à 85-90 % du natif Linux, au prix d'une adaptation du docker-compose.yml. DDEV atteint des performances équivalentes à Mutagen tout en prenant en charge l'ensemble de l'environnement de développement. L'écart entre Mutagen standalone et DDEV est inférieur à 15 % — il ne justifie pas un choix technique, mais un choix organisationnel.

Comment choisir selon ton contexte

VirtioFS — adoption immédiate, zéro coût

  • Tu es sur Apple Silicon (M1/M2/M3) : VirtioFS est déjà actif par défaut depuis Docker Desktop 4.22, tu n'as rien à faire
  • Tu veux un gain ×5 immédiat sans modifier ta configuration Docker existante
  • Tu travailles seul ou en petite équipe sans processus d'onboarding formalisé
  • Tu es sur Intel et tu n'as pas encore activé VirtioFS dans les préférences Docker Desktop

Mutagen — performances maximales, contrôle total

  • Le feedback loop entre sauvegarde et rechargement du serveur dev est ton critère principal
  • Tu lances des suites de tests complètes en local et chaque seconde de warmup compte
  • Tu maîtrises Docker Compose et tu peux adapter ton docker-compose.yml au modèle volume + synchronisation
  • Tu veux garder une stack Docker standard sans dépendance à un outil tiers spécialisé PHP

DDEV — productivité d'équipe et onboarding zéro friction

  • Tu intègres régulièrement de nouveaux développeurs sur le projet — la config .ddev/config.yaml versionnée est un atout direct
  • Tu veux HTTPS local avec certificats valides, Mailpit et accès base de données sans configuration manuelle
  • Ton équipe est mixte, avec des profils moins à l'aise avec les internals Docker
  • Tu gères plusieurs projets PHP en parallèle avec des versions PHP et base de données différentes

En résumé

Il n'y a pas de réponse universelle — mais il y a une logique de progression. Si tu es sur Apple Silicon, commence par vérifier que VirtioFS est actif : tu obtiens ×5 de performance pour zéro effort. Si tes temps de test ou de warmup restent un frein, passe à Mutagen ou à DDEV selon que tu veux garder le contrôle de ta stack Docker ou déléguer la gestion de l'environnement. La bonne nouvelle : ces trois approches sont composables dans le temps. Tu peux commencer avec VirtioFS aujourd'hui, ajouter Mutagen sur les projets les plus intensifs, et migrer vers DDEV si ton équipe grandit. Le plafond de performance de macOS reste inférieur à Linux natif — mais avec VirtioFS ou Mutagen, l'écart tombe à 10-15 %, ce qui est largement acceptable pour une expérience de développement fluide au quotidien.

Cet article vous a plu ? Partagez-le !

Besoin d'un expert Symfony ?

20 ans d'expérience sur l'écosystème PHP/Symfony.