Docker Connexion à MariaDB depuis PHP

Le problème exposé dans ce sujet a été résolu.

Bonjour,

Je me suis récemment mis à docker aussi pour mes développements personnels, et là il y a un truc que je ne comprends pas trop comment faire.

J’ai un site web en PHP qui doit se connecter à une base MariaDB, rien d’extraordinaire.

Par contre, je n’arrive pas à me connecter à la base en spécifiant localhost ou 127.0.0.1. Pour que ça marche, je dois lui donner mon IP locale en 192.168.1.X.

Pourquoi ? Il me semblais qu’un réseau docker commun était automatiquement construit…

Est-ce qu’il y a un moyen de lui faire accepter une connexion avec localhost ou 127.0.0.1, sachant que c’est le host utilisé en prod ?

Je n’ai pas envie de coder en dur 192.168.1.X, car cette IP change selon que je suis en wifi ou en connexion filaire, et puis j’ai plusieurs PC où j’aimerais pouvoir développer mon projet si possible sans avoir à changer quelque chose dans le code de spécifique (c’était aussi un des buts que de passer à docker).

Alternative à la question précédente: y a-t-il un moyen de transmettre mon IP locale actuelle à PHP ? Par une variable d’environnement ou je ne sais quoi. Sachant que la prod n’est pas en docker, et que $_SERVER['SERVER_ADDR'] ne peut pas fonctionner car elle retourne l’IP utilisée en interne par docker en 172.X.X.X (c’est logique)

Question plus générale: comment puis-je faire pour ne pas avoir à modifier mon code entre mon environnement de dev et de prod ? Jusqu’à maintenant j’utilisais une constante LOCAL, mais n’y a-t-il pas plus propre ?
define('LOCAL', $_SERVER['REMOTE_ADDR']=='127.0.0.1' || $_SERVER['REMOTE_ADDR']=='::1' || $_SERVER['REMOTE_ADDR']==$_SERVER['SERVER_ADDR'] || substr($_SERVER['SERVER_ADDR'], 0, 10)=='192.168.1.');
Je pourrais bien sûr ajouter le 172.X.X.X donné par docker, mais si un jour je passe à docker aussi en prod (ce n’est pas exclu), je serai coincé.

Voici le script php de test de connexion:

<?php
$host = '127.0.0.1';
$user = '...';
$dbname = '...';
$password = '...';
$db = new PDO("mysql:host=$host;dbname=$dbname", $user, $password);
die('DB connection OK');
?>

L’erreur quand je ne spécifie pas mon IP locale 192.168.1.X:

Fatal error: Uncaught PDOException: SQLSTATE[HY000] [2002] Connection refused 

ET mon docker-compose.yml:

services:
  php:
    image: webdevops/php-apache:7.4
    ports: 
    - 0.0.0.0:80:80
    - 0.0.0.0:443:443
    working_dir: /app
    volumes:
    - ./html:/app
    environment:
      PHP_DATE_TIMEZONE: Europe/Paris
      PHP_DISPLAY_ERRORS: 1
  
  mariadb:
    image: mariadb
    restart: always
    ports: [0.0.0.0:3306:3306]
    volumes:
    - ./mariadb-data/:/var/lib/mysql/
    environment:
      MARIADB_DATABASE: ...
      MARIADB_USER: ...
      MARIADB_PASSWORD: ...
      MARIADB_ROOT_PASSWORD: ...
  
  adminer:
    image: adminer
    restart: always
    ports: [0.0.0.0:8080:8080]

A noter que je dois aussi spécifier 192.168.1.X pour me connecter à la base avec adminer, localhost ou 127.0.0.1 ne fonctionne pas non plus.

A noter aussi que je tiens à exposer le port 3306, car je me connecte aussi à la base avec un autre programme qui, lui, n’est pas dockerisé, et ne le sera sans doute jamais. Pour le coup avec cette config, ça fonctionne très bien, le programme se connecte bien en spécifiant localhost comme host. Ce qui est logique aussi, car comme il n’est pas dockerisé, il se fiche pas mal de savoir si MariaDB est réellement installé ou pas, du moment que le port 3306 est ouvert.

Merci pour votre aide !

+0 -0

Hello,

Si tu fais docker network inspect <l'id du réseau lié au conteneur PHP> (tu peux l’obtenir en inspectant ton conteneur avec docker inspect <conteneur>, ou en listant tous les réseaux via docker network ls) est-ce que tu vois bien les deux conteneurs sur le même réseau ?

En passant, préciser 0.0.0.0 dans le mapping de ports est pas forcément une bonne idée pour la sécurité : on préfère généralement écouter uniquement sur les adresses réellement utilisées plutôt que n’importe laquelle.


Pour info, en général on passe par des variables d’environnement (voire un fichier .env, notamment en local) pour gérer des configs différentes entre les serveur (local/test/prod/…). Si besoin, ça peut t’aider pour la suite. ;)

Salut,
De ce que je comprends de cette page tes containers sont bien tous sur un même réseau créé par défaut, mais c’est donc comme si c’était plusieurs machines sur le même réseau, tu ne peux pas accéder au travers localhost.
Docker Compose en créant ce réseau met en place une résolution des noms de domaine correspondant au nom que tu as donné au service. Donc je pense qu’il faut que ton php se réfère à la base de donnée par l’url mariadb:3306 selon ton docker-compose.yml.
C’est ce que dit ce paragraphe :

Each container can now look up the hostname web or db and get back the appropriate container’s IP address. For example, web’s application code could connect to the URL postgres://db:5432 and start using the Postgres database.


Question plus générale: comment puis-je faire pour ne pas avoir à modifier mon code entre mon environnement de dev et de prod ?

QuentinC

C’est ce à quoi servent les outils de configuration. Il y a quelques méthodes différentes, mais les premières qui me viennent à l’esprit c’est soit d’accompagner ton application d’un fichier de configuration (au format INI/TOML/XML…), soit de récupérer tes infos de variables d’environnements, soit encore d’utiliser des registres système (sous windows).

+0 -0

Salut Quentin 👋,

Par contre, je n’arrive pas à me connecter à la base en spécifiant localhost ou 127.0.0.1. Pour que ça marche, je dois lui donner mon IP locale en 192.168.1.X.

Chaque conteneur Docker a son propre localhost (c’est comme des machines indépendantes). Quand tu tentes de te connecter à localhost ou 127.0.0.1, en réalité tu tentes de te connecter à l’intérieur du même conteneur.

Dans ton cas, tu as trois conteneurs : l’un pour PHP, un autre pour MariaDB et enfin, un pour Adminer. Pour que PHP ou Adminer se connecte à MariaDB, tu dois spécifier le nom du service MariaDB comme nom d’hôte, et non localhost. Docker compose crée un réseau par défaut où tous les services se "voient" via leur nom.

Donc, dans ton script PHP, change simplement l’adresse de l’hôte de '127.0.0.1' à 'mariadb' :

$host = 'mariadb';
$user = '...';
$dbname = '...';
$password = '...';
$db = new PDO("mysql:host=$host;dbname=$dbname", $user, $password);
die('DB connection OK');

Voici un diagramme explicatif (que j’ai fais rapidement) sur comment les choses communiques entre hôte et services docker:

Les zones bleues représentent les réseaux (hôte, et docker). Ils communiquent via un Bridge (que fais docker, si tu utilises le driver réseau par défaut de docker).

Le réseau en orange présente les sous-réseaux virtuels créés par Docker pour communiquer entre les services de ton docker-compose. Comme dit précédemment, ils peuvent être atteints via leurs noms (php, mariadb ou adminer) ou via leurs ip de sous réseau docker (172.X.Y.Z).

Lorsque tu envoies une requête (avec l’hôte 127.0.0.1) via Adminer, c’est le service adminer qui reçois la requête. Il va alors tenter de se connecter à la base de données via l’hôte 127.0.0.1, sauf que le localhost ici, c’est lui-même (adminer), donc il est tout à fais normal qu’il ne parviennent pas à se connecter à la base de données.

J’ai également fait figurer deux requêtes:

La requête en verte montre le chemin pour atteindre la base de données depuis le service adminer vers la base de données (adminer). Ici, l’hôte passé à l’authentification d’Adminer est mariadb.

La seconde requête est un exemple un peu hors scope, mais qui peut être utile. Elle permet, depuis un conteneur Docker d’accéder à des services qui tourne sur l’hôte sans que ce soit dans un conteneur Docker. Pour cela, on utilise l’URL: host.docker.internal.

comment puis-je faire pour ne pas avoir à modifier mon code entre mon environnement de dev et de prod ?

Tu peux utiliser des variables d’environnement. Par exemple, tu peux définir une variable d’environnement DB_HOST et l’utiliser dans ton script PHP (j’ai mis des variables d’environnement pour tout ce qui est variable dans la connexion a la BDD) :

$host = getenv('DB_HOST');
$user = getenv('DB_USER');
$dbname = getenv('DB_NAME');
$password = getenv('DB_PASSWORD');
$db = new PDO("mysql:host=$host;dbname=$dbname", $user, $password);
die('DB connection OK');

Et dans ton fichier docker-compose.yml, tu peux définir ces variables d’environnement :

services:
  php:
    image: webdevops/php-apache:7.4
    ports: 
    - 0.0.0.0:80:80
    - 0.0.0.0:443:443
    working_dir: /app
    volumes:
    - ./html:/app
    environment:
      PHP_DATE_TIMEZONE: Europe/Paris
      PHP_DISPLAY_ERRORS: 1
      DB_HOST: mariadb
      DB_USER: myUser
      DB_PASSWORD: Secr3t
      DB_NAME: name_of_the_database

  mariadb:
    image: mariadb
    restart: always

    # Si tu n'as pas besoin de te connecter à ce service depuis l'extérieur de ce docker-compose, alors tu n'as pas besoin d'exposer le port
    # ports: [0.0.0.0:3306:3306]
    
    volumes:
    - ./mariadb-data/:/var/lib/mysql/
    environment:
      MARIADB_DATABASE: name_of_the_database
      MARIADB_USER: myUser
      MARIADB_PASSWORD: Secr3t
      MARIADB_ROOT_PASSWORD: R0oT_Secr3t
  
  adminer:
    image: adminer
    restart: always
    ports: [0.0.0.0:8080:8080]
    environment:
      DB_HOST: mariadb
      DB_USER: myUser
      DB_PASSWORD: Secr3t
      DB_NAME: name_of_the_database

De cette façon, tu peux avoir des valeurs différentes pour DB_* dans tes environnements de dev et de prod, sans avoir besoin de modifier ton code source.

À savoir que pour éviter la redondance des variables d’environnement (ici, les copié/collé des DB_* dans différents services), tu peux également utiliser un fichier de variables d’environnement (.env par exemple).

+2 -0

Bonjour,

Merci beaucoup pour vos réponses très complètes.

@Viki53:

En passant, préciser 0.0.0.0 dans le mapping de ports est pas forcément une bonne idée pour la sécurité : on préfère généralement écouter uniquement sur les adresses réellement utilisées plutôt que n’importe laquelle.

Dans la mesure où, pour l’instant, je ne fais du docker qu’en local pour le développement, je pense qu’on peut s’en ficher complètement. Mais effectivement, à ne pas oublier si je décide de passer à docker aussi en production.

@Romantik:

Docker Compose en créant ce réseau met en place une résolution des noms de domaine correspondant au nom que tu as donné au service. Donc je pense qu’il faut que ton php se réfère à la base de donnée par l’url mariadb:3306 selon ton docker-compose.yml. C’est ce que dit ce paragraphe :

Each container can now look up the hostname web or db and get back the appropriate container’s IP address. For example, web’s application code could connect to the URL postgres://db:5432 and start using the Postgres database.

J’ai bien lu ce paragraphe dans le tutoriel, mais je n’ai pas tilté. Je croyais qu’il fallait remplacer les noms "web" ou "db" de l’exemple par mon IP locale, localhost, 127.0.0.1 ou autre chose.

C’était bien ça. Effectivement en spécifiant "mariadb" dans le host de adminer ou dans mon script php, ça marche.

Il y a quelques méthodes différentes, mais les premières qui me viennent à l’esprit c’est soit d’accompagner ton application d’un fichier de configuration (au format INI/TOML/XML…)

C’est justement la solution que je veux éviter, entres autres parce que je ne peux pas versionner un tel fichier de config dans git, et du coup ça complique mon truc. J’ai fait en sorte que quand je push sur master, le site pull de son côté, comme ça je n’ai pas à me préoccuper de mettre le site à jour manuellement.

@Hesiode:

Merci beaucoup pour ces explications bien fournies. J’avais plus ou moins l’idée de pourquoi ça ne marchait pas, mais là c’est beaucoup plus clair.

Je vais voir du côté des fichiers contenant des variables d’environnement, ça me parait pas mal.

La seule difficulté est de faire en sorte que ça puisse aussi marcher sur le serveur de production qui n’utilise pas docker.

J’ai pensé à utiliser SetEnv dans le .htaccess, mais vu que le .htaccess est le même en dev qu’en prod, ça n’est pas une solution…

J’ai essayé de faire ceci: définir une variable d’environnement pour le développement en local, et pour la production, si la variable d’environnement n’est pas définie (getenv() retourne une chaîne vide), je me base sur le hostname (function gethostname()) pour accéder à un fichier de config dédié. Ca me permet d’avoir des fichiers différents: config-dev-local pour le local, et config-monsite.com pour la production. Je ne sais pas si c’est une super idée, mais ça marche

JE me doute que ce n’est peut-être pas génial comme idée parce que si gethostname() accède au réseau, c’est probablement plusieurs ordres de grandeur plus lent que d’accéder à un bête petit fichier de config. ET vue que chaque requête HTTP ou presque va faire le même appel, ça peut certainement vite peser lourd.

Merci encore pour vos réponses.

+0 -0

C’est justement la solution que je veux éviter, entres autres parce que je ne peux pas versionner un tel fichier de config dans git, et du coup ça complique mon truc. J’ai fait en sorte que quand je push sur master, le site pull de son côté, comme ça je n’ai pas à me préoccuper de mettre le site à jour manuellement.

QuentinC

Je me doutes que tu n’as pas des exigences aussi élevé que l’industrie, mais en principe, la mise en production n’est pas anodine. Il est possible d’automatiser des étapes, mais il y a quand même un certain contrôle à avoir. Et le déploiement est rarement aussi simple qu’un pull du dépôt, pour ça on a des outils qui font partie de l’intégration continue, comme les pipelines de gitlab.
Pour l’instant si ça suffit je comprends que tu gardes ce simple pull, j’espère juste que si tu rencontres des petits blocages, tu ne t’accroche pas à ça en faisant des workaround, mais que tu cherches des vraies solutions car elles existent.

JE me doute que ce n’est peut-être pas génial comme idée parce que si gethostname() accède au réseau, c’est probablement plusieurs ordres de grandeur plus lent que d’accéder à un bête petit fichier de config. ET vue que chaque requête HTTP ou presque va faire le même appel, ça peut certainement vite peser lourd.

QuentinC

Je ne comprends pas pourquoi c’est chargé à chaque requête, ce sont des configurations liées à l’environnement de déploiement, ça ne va pas changer, pourquoi ça ne peut pas être chargé une seule fois ?
Et du coup si c’est chargé qu’une fois ce n’est pas grave que ça ne soit pas performant. Je trouve juste que tu te casse la tête pour faire un workaround (voir paragraphe précédant).

+0 -0

Je me doutes que tu n’as pas des exigences aussi élevé que l’industrie, mais en principe, la mise en production n’est pas anodine. Il est possible d’automatiser des étapes, mais il y a quand même un certain contrôle à avoir. Et le déploiement est rarement aussi simple qu’un pull du dépôt, pour ça on a des outils qui font partie de l’intégration continue, comme les pipelines de gitlab. Pour l’instant si ça suffit je comprends que tu gardes ce simple pull, j’espère juste que si tu rencontres des petits blocages, tu ne t’accroche pas à ça en faisant des workaround, mais que tu cherches des vraies solutions car elles existent.

JE sais bien que gitlab existe, c’est ce qui est utilisé au boulot.

Mais j’ai pas envie de m’embarrasser avec autant de trucs pour mes projets perso. Je suis tout seul à coder, et ça ne risque pas de changer avant très longtemps. J’ai pas besoin de quelque chose d’aussi gros.

Je ne comprends pas pourquoi c’est chargé à chaque requête, ce sont des configurations liées à l’environnement de déploiement, ça ne va pas changer, pourquoi ça ne peut pas être chargé une seule fois ?

C’est un des inconvénients de PHP par rapport à une app plus moderne en Java, en Node, en Python, en Go, en Ruby ou à peu près n’importe quoi d’autre.

Sauf si ça a changé depuis, fondamentalement, les scripts PHP sont réexécutés du début à la fin à chaque requête. Les versions compilées des scripts sont peut-être mis en cache, mais ils sont quand même réexécutés à chaque fois.

Cela dit, j’avoue que je n’ai pas énormément suivi ce qui s’est passé sur la planète PHP à partir de PHP 7 et au-delà. Ca a peut-être changé depuis. Certaines parties du code ont pas loin de 15 ans.

+1 -0

Je vais voir du côté des fichiers contenant des variables d’environnement, ça me parait pas mal.

La seule difficulté est de faire en sorte que ça puisse aussi marcher sur le serveur de production qui n’utilise pas docker.

Pour charger tes variables d’environnement depuis un fichier en PHP, tu peux utilise une lib comme phpdotenv, que tu peux installer facilement via Composer (si tu utilises un gestionnaire de dépendance en PHP).

En ajoutant le fichier .env dans ton .gitignore, tout sera bon; tu pourras avoir ton fichier .env en dev sur ta machine, et un .env en prod. Et tout cela, sans que git t’embête avec des modifs non indexées sur ces fichiers, car ces modifications (sur les fichiers .env seront ignorées).

+0 -0

Bonsoir,

Intéressant ! Mais je n’utilise pas de gestionnaire de dépendance en PHP, tout simplement parce que je n’ai jamais eu aucune dépendance. Je connais bien maven pour Java, mais en PHP je n’ai jamais eu besoin de quelque chose de similaire.

Dans tous les cas ça revient peu ou prou au même, soit je respecte les bonnes pratiques avec des fichiers .env non versionnés comme tu le proposes mais c’est un poil plus embêtant, soit je m’en fous et bon, pas grave si des mots de passe sont en clair dans le repo git vu qu’il est de toute façon privé.

Merci.

JE crois qu’on a fait le tour du sujet, je le passe en résolu.

+0 -0
Connectez-vous pour pouvoir poster un message.
Connexion

Pas encore membre ?

Créez un compte en une minute pour profiter pleinement de toutes les fonctionnalités de Zeste de Savoir. Ici, tout est gratuit et sans publicité.
Créer un compte