Mailpit, c'est un de ces petits outils qu'on installe une fois et qu'on finit par utiliser partout. Un faux serveur SMTP, une interface web agréable, aucune authentification nécessaire en local, et surtout la certitude que les emails générés pendant le développement ne vont pas atterrir chez de vrais utilisateurs.
Sur le papier, c'est presque trop simple : un conteneur Docker, deux ports, une variable MAILER_DSN, et terminé.
Sauf que Docker, les réseaux et la résolution DNS peuvent parfois transformer une configuration de trois lignes en une petite séance de diagnostic réseau.
Voici un retour d'expérience concret, avec les vérifications qui permettent de comprendre pourquoi une application PHP refuse de communiquer avec Mailpit, même lorsque celui-ci fonctionne parfaitement.
Pourquoi Mailpit ?
Mailpit remplace avantageusement un serveur SMTP traditionnel dans un environnement de développement.
Il permet notamment de :
- Capturer les emails sans les expédier aux destinataires réels.
- Consulter les messages dans une interface web.
- Vérifier le rendu HTML, les pièces jointes et les en-têtes.
- Rechercher des messages par destinataire, expéditeur ou contenu.
- Automatiser certaines vérifications grâce à son API REST.
- Conserver les messages entre les redémarrages avec un stockage persistant.
Il est léger, développé en Go et fonctionne très bien dans Docker.
Mais son intérêt devient encore plus évident lorsqu'on dispose de plusieurs projets PHP, Symfony ou autres, qui doivent tous utiliser le même serveur SMTP de développement.
Plutôt que de multiplier les instances Mailpit, autant en mutualiser une seule.
Et c'est précisément là qu'il faut faire attention aux réseaux Docker.
1. Installer Mailpit avec Docker Compose
Pour commencer, voici une configuration simple :
services:
mailpit:
image: axllent/mailpit:latest
restart: unless-stopped
ports:
- "127.0.0.1:1025:1025"
- "127.0.0.1:8025:8025"
volumes:
- mailpit_data:/data
environment:
MP_MAX_MESSAGES: 5000
MP_DATABASE: /data/mailpit.db
volumes:
mailpit_data:
Deux ports sont utilisés :
- 1025 : le serveur SMTP, qui réceptionne les messages.
- 8025 : l'interface web de consultation.
Ici, les ports sont publiés uniquement sur l'interface locale de l'hôte. Cela évite d'exposer inutilement un SMTP sans authentification sur le réseau.
Attention : cette configuration est volontairement restrictive. Si un autre conteneur doit accéder à Mailpit via la passerelle Docker, il faudra publier le port SMTP sur une adresse accessible depuis ce conteneur, tout en limitant les accès par des règles réseau appropriées. Une publication sur 127.0.0.1 ne suffit généralement pas pour un accès via host-gateway.
Les messages sont enregistrés dans un volume Docker pour survivre aux redémarrages et aux recréations du conteneur.
Attention également aux anciens tutoriels : la variable MP_DATA_FILE a été remplacée par MP_DATABASE.
2. Configurer Symfony : la partie facile
Dans un projet Symfony, la configuration tient normalement en une ligne :
MAILER_DSN=smtp://mailpit:1025
Pas de login, pas de mot de passe et pas de TLS pour une instance Mailpit utilisant sa configuration SMTP par défaut.
Cela fonctionne immédiatement lorsque le conteneur Symfony et Mailpit partagent un réseau Docker et que mailpit est le nom du service.
Mais imaginons une autre architecture.
Vous disposez d'un conteneur Mailpit mutualisé, utilisé par plusieurs projets Docker indépendants, et accessible sur le nom local mailpit.local.
Naturellement, on configure :
MAILER_DSN=smtp://mailpit.local:1025
Et là, Symfony retourne :
Connection could not be established
with host "mailpit.local:1025"
stream_socket_client():
Connection refused
Premier réflexe : vérifier le DSN, changer les identifiants, ajouter des options SMTP, vider le cache Symfony...
Mauvaise piste.
Le DSN peut être parfaitement correct. Le problème se situe ailleurs.
3. Première leçon : Connection refused n'est pas une erreur d'authentification
Une erreur Connection refused signifie que la connexion TCP a été refusée à l'adresse et au port ciblés.
Cela peut notamment vouloir dire :
- Aucun service n'écoute sur le port demandé.
- La connexion vise la mauvaise adresse IP.
- Le port Docker n'est pas publié au bon endroit.
- Une règle réseau rejette activement la connexion.
En revanche, ajouter un utilisateur et un mot de passe SMTP ne réglera pas ce problème.
Avant de toucher à Symfony, il faut donc vérifier ce que le conteneur PHP essaie réellement de contacter.
4. Le vrai piège : un domaine qui pointe vers 127.0.0.1
Depuis le conteneur PHP, commençons par vérifier la résolution du nom :
getent hosts mailpit.local
Dans notre cas, le résultat ressemblait à ceci :
127.0.0.1 localhost mailpit.local
Voilà le coupable !
Le nom mailpit.local était résolu vers 127.0.0.1.
Or, dans Docker, 127.0.0.1 correspond à la boucle locale du conteneur dans lequel on exécute la commande.
Pas à celle du serveur Docker.
Pas à celle du conteneur Mailpit.
Et encore moins à une autre machine du réseau.
Autrement dit, notre application PHP essayait de se connecter à elle-même sur le port 1025.
Évidemment, aucun serveur SMTP n'y écoutait.
Résultat : Connection refused.
À retenir : un domaine qui fonctionne dans le navigateur de votre PC ne fonctionne pas nécessairement de la même manière depuis un conteneur Docker.
5. Vérifier d'où vient la résolution DNS
Avant de modifier quoi que ce soit, regardons la configuration réseau du conteneur :
cat /etc/hosts
cat /etc/resolv.conf
Docker utilise généralement un résolveur DNS interne sur les réseaux Docker personnalisés :
nameserver 127.0.0.11
Il peut ensuite transférer les requêtes vers les résolveurs configurés sur l'hôte.
Dans notre cas, mailpit.local n'apparaissait pas dans /etc/hosts, mais était malgré tout résolu vers 127.0.0.1.
Cela indiquait que la réponse provenait de la résolution DNS plutôt que d'une entrée statique dans le fichier hosts du conteneur.
Il faut également se méfier des domaines terminés par .local, suffixe historiquement réservé à mDNS. Selon le système et la configuration réseau, leur résolution peut réserver des surprises.
Pour un environnement maîtrisé, on peut utiliser le DNS Docker, un domaine de développement correctement configuré ou une entrée explicite dans /etc/hosts.
6. Tester le SMTP sans installer d'outils supplémentaires
Les images Docker PHP ne disposent pas toujours de telnet, nc, dig ou d'autres outils réseau.
Et il n'est pas forcément souhaitable d'installer tout un arsenal de diagnostic dans un conteneur applicatif.
Bonne nouvelle : PHP suffit.
php -r '$s=@fsockopen("host.docker.internal",1025,$e,$m,3); echo $s ? "SMTP OK\n" : "ERREUR $e : $m\n"; if ($s) fclose($s);'
Ce test ouvre une connexion TCP au port SMTP avec un timeout de trois secondes.
Dans notre cas :
SMTP OK
Nous avions donc une information importante : Mailpit était joignable depuis le conteneur PHP en passant par l'hôte Docker.
Le problème n'était ni Symfony, ni Mailpit, ni le port 1025.
Il s'agissait simplement de la résolution du nom mailpit.local.
Attention : SMTP OK confirme uniquement l'ouverture de la connexion TCP. Cela ne prouve pas encore qu'un email complet pourra être envoyé.
7. Corriger proprement avec extra_hosts
Docker Compose permet d'ajouter des correspondances entre noms d'hôtes et adresses IP grâce à extra_hosts.
Dans le service PHP de notre application :
services:
php:
# Autres paramètres du service...
extra_hosts:
- "mailpit.local:host-gateway"
La valeur spéciale host-gateway permet à Docker d'utiliser l'adresse de passerelle de l'hôte configurée à cet effet.
Cette solution évite d'inscrire en dur une IP privée susceptible de changer.
Elle convient lorsque Mailpit publie son port SMTP sur l'hôte, à une adresse accessible depuis le conteneur PHP. Il faut donc s'assurer que la publication du port et les éventuelles règles de pare-feu autorisent ce chemin réseau.
On peut maintenant conserver un DSN lisible :
MAILER_DSN=smtp://mailpit.local:1025
C'est plus agréable à maintenir qu'une adresse IP inscrite directement dans les fichiers de configuration.
Attention : modifier docker-compose.yml ne suffit pas
C'est un autre piège rencontré pendant le dépannage.
Après avoir ajouté extra_hosts, un simple :
php bin/console cache:clear
ne change absolument rien à la résolution réseau du conteneur.
Le cache Symfony n'a aucun rapport avec le fichier /etc/hosts généré par Docker.
Il faut recréer le conteneur concerné.
Avant cela, vérifions que Docker Compose reconnaît notre configuration :
docker compose config | grep -A 5 extra_hosts
On doit retrouver quelque chose comme :
extra_hosts:
- mailpit.local=host-gateway
Ensuite, depuis l'hôte Docker :
docker compose up -d --no-deps --force-recreate php
Cette commande recrée uniquement le service PHP, sans redémarrer ses dépendances.
Attention à ne pas confondre recréation et simple redémarrage : un docker compose restart ne suffit pas à appliquer une nouvelle configuration extra_hosts.
8. Dernière vérification : le domaine pointe-t-il enfin au bon endroit ?
Une fois le conteneur recréé :
getent hosts mailpit.local
Cette fois, nous obtenons une adresse de passerelle Docker, par exemple :
172.30.0.1 mailpit.local
Plus de 127.0.0.1.
Dernier test TCP :
php -r '$s=@fsockopen("mailpit.local",1025,$e,$m,3); echo $s ? "SMTP OK\n" : "ERREUR $e : $m\n"; if ($s) fclose($s);'
Résultat attendu :
SMTP OK
On peut enfin tester l'envoi d'un véritable email depuis Symfony et vérifier sa réception dans l'interface Mailpit.
Cette fois, tout fonctionne.
9. Et si Mailpit et PHP partagent déjà le même réseau Docker ?
C'est une distinction importante : extra_hosts n'est pas obligatoire dans tous les cas.
Si Mailpit et l'application PHP partagent un réseau Docker utilisateur, le plus simple est généralement d'utiliser directement le nom du service :
MAILER_DSN=smtp://mailpit:1025
Le DNS interne de Docker se charge de la résolution.
Pas besoin de passer par la passerelle de l'hôte.
Pas besoin d'exposer le port SMTP sur l'hôte si seuls les conteneurs de ce réseau doivent y accéder.
Pour plusieurs projets Docker indépendants, on peut aussi créer un réseau Docker externe partagé :
docker network create mailpit
Puis rattacher les services Mailpit et PHP à ce réseau dans leurs fichiers Compose respectifs.
Du côté du projet PHP :
services:
php:
networks:
- default
- mailpit
networks:
mailpit:
external: true
Du côté de Mailpit :
services:
mailpit:
image: axllent/mailpit:latest
networks:
- mailpit
networks:
mailpit:
external: true
Avec les deux services connectés au même réseau, le nom mailpit devient directement accessible via le DNS Docker.
Cette approche évite de faire transiter le SMTP par un port publié sur l'hôte.
Deux architectures, deux solutions :
- Services partageant un réseau Docker : utiliser le nom du service Mailpit.
- Service accessible via un port publié sur l'hôte : utiliser une adresse joignable depuis le conteneur, éventuellement associée à un nom avec
extra_hosts.
Le choix dépend de la manière dont l'infrastructure est organisée.
10. Les erreurs à ne plus commettre
Après ce dépannage, voici les quelques règles qui méritent de rester dans un coin de la tête.
- Ne pas confondre localhost et hôte Docker. Dans un conteneur,
127.0.0.1désigne généralement ce conteneur lui-même. - Ne pas modifier les identifiants SMTP pour une erreur TCP. Une connexion refusée doit d'abord être diagnostiquée au niveau réseau.
- Ne pas supposer que le DNS de l'hôte et celui des conteneurs donnent les mêmes réponses. Toujours tester depuis le conteneur concerné.
- Ne pas oublier de recréer le conteneur après une modification de extra_hosts. Vider le cache PHP ou Symfony ne sert à rien pour ce type de problème.
- Ne pas exposer inutilement le port SMTP. Mailpit sans authentification est parfait dans un environnement de développement contrôlé, beaucoup moins sur un réseau public.
- Ne pas oublier la persistance. Sans stockage persistant, une recréation du conteneur peut faire disparaître les emails conservés.
- Ne pas confondre un test TCP réussi avec un envoi d'email validé. Les deux vérifications sont complémentaires.
Conclusion : cinq minutes de diagnostic valent mieux qu'une heure à modifier des configurations
Ce dépannage illustre parfaitement un problème classique avec Docker.
Tous les éléments fonctionnaient individuellement :
- Le conteneur Mailpit était opérationnel.
- Le port SMTP répondait.
- Symfony était correctement configuré.
- Le DSN ne contenait aucune erreur.
- Le conteneur PHP disposait bien d'un accès réseau vers l'hôte.
Et pourtant, impossible d'envoyer un email.
Une simple commande a permis de comprendre pourquoi :
getent hosts mailpit.local
Le domaine pointait vers 127.0.0.1, et PHP tentait donc de joindre un serveur SMTP qui n'existait pas dans son propre conteneur.
La correction a finalement tenu à une ligne dans Docker Compose :
extra_hosts:
- "mailpit.local:host-gateway"
Puis à une recréation du conteneur PHP.
C'est tout.
La leçon dépasse largement Mailpit : Redis, MariaDB, RabbitMQ, Elasticsearch ou n'importe quel service réseau peuvent provoquer exactement les mêmes symptômes lorsqu'un nom d'hôte ne pointe pas là où on le croit.
Alors, la prochaine fois qu'un service Docker vous répond Connection refused, avant de retourner toute votre configuration, vérifiez simplement où il essaie de se connecter.
Ça paraît évident. Jusqu'au jour où ça ne l'est plus.