Docker: настройка nginx, php-fpm и MySQL для локальной разработки с Docker Compose

В данной статье мы продолжим изучение Docker на примере настройки сервера для локальной разработки. Мы будем использовать веб-сервер nginx, php-fpm и базы данных MySQL. А также добавим для визуального редактирования базы данных phpMyAdmin.

Пост будет не коротким, подробным и по большей части практическим. Если вы новичок в докере, для закрепления знаний было бы неплохо повторить то, что будет в этой статье. В качестве редактора кода я буду использовать VS Code, как и в предыдущей статье. Работать буду в среде wsl.

Данная статья является частичным вольным переводом данной статьи, дополненным личным опытом и знаниями из других источников. Если английский для вас не проблема — могу посоветовать заглянуть к этому автору. У него есть интересные материалы, посвященные докеру и его различным применениям.

Nginx

В прошлой статье мы уже запускали php с апачем в докере. Здесь у нас будет nginx вместо апача, а это значит, что сервис самого веб-сервера будет отделён от сервиса php. Напомню, ранее мы это делали командой: docker create —name php-apache -p 80:80 -v «.:/var/www/html» php:8.3-apache

На этот раз мы создадим отдельный файл, в котором будут прописаны настройки. Называется такой файл Dockerfile. Пишется именно так, с большой буквы и без какого-либо расширения. Давайте создадим структуру в нашем проекте, чтобы не было путаницы. Я буду использовать директорию из прошлого урока.

projects/docker-lesson
|
|-/app
| |-index.html
| |-test.php
|-Dockerfile

На данный момент структура будет примерно такая. В директории проекта создадим директорию app, в которую перенесём файлы нашего «сайта», это index.html и test.php, чтобы отделить директорию приложения, которое должно работать на сервере, от технической инфраструктуры. Также в директории проекта создадим Dockerfile, в котором пропишем настройки для веб-сервера.

Dockerfile:

FROM nginx

Для начала мы просто укажем название репозитория из докер-хаба. Далее вводим команду:

docker build -t web-server .

Данной командой мы собрали образ, из которого можно создавать контейнер. Здесь у нас есть новые ключевые слова и флаги:

build - собственно говорит докеру о том что надо собрать образ; 
-t - обозначает "тэг", другими словами это некая человекопонятная метка, по которой можно найти наш образ; 
web-server - это собственно наш тэг, здесь может быть любое обозначение, которое вам удобно; 
. - это указание того, где находится наш докерфайл. Он у нас находится в корневой директории, поэтому ставим просто точку. Обязательно ставим. Также здесь можно указать не докерфайл, а другой источник - гит-репозиторий или название образа из докер-хаба.

Далее собственно мы можем создать и запустить контейнер. Делать мы это будем тоже немного иначе, чем в прошлой статье.

docker run -d --name my-nginx -p 80:80 web-server

Команда run отличается от команды create тем, что мы не просто создаём контейнер, но и сразу же его запускаем. Т.е. фактически это аналог последовательного ввода команд create + start для контейнера. Далее у нас указан флаг -d, что означает —detach. Суть в том, что, если мы не укажем этот флаг, мы будем находиться в процессе контейнера и не сможем ничего делать в терминале. Флаг -d позволяет нам запустить процесс контейнера в фоновом режиме и вернуться в терминал для последующей работы. Далее следуют уже знакомые нам флаги и параметры —name с последующим указанием имени создаваемого контейнера, -p с последующим указанием портов локальной машины:контейнера и web-server — название нашего образа, который был собран с помощью докерфайла.

Если сейчас мы перейдём в браузер и зайдем на http://localhost:80, то увидим приветственное сообщение от сервера nginx.

У нас пока ничего нет в контейнере. Снова, да. Нам нужно скопировать файлы из директории app в директорию контейнера. Для этого нам надо связать директории через флаг -v, как мы уже делали в прошлый раз. Здесь есть одна особенность. Если у апача дефолтная директория находится в привичном /var/www/html, то в nginx дефолтная директория находится непонятно где, точнее понятно в /usr/share/nginx/html, но это очень неудобно. Поэтому прежде чем привязывать директории, мы создадим конфигурационный файл для нашего будущего сайта, в котором укажем более удобную и привычную директорию.

Для этого в корне проекта создадим файл default.conf. Это будет простейший файл настроек сайта для nginx. Добавим в него следующее содержимое:


default.conf:

server {
    listen :80;
    listen [::]:80;
    server_name localhost;

    location / {
        root /var/www/html; 
        index index.php index.html index.htm;
    }
}

Это дефолтный файл конфигурации, в котором я поменяла значение root и в location/ index добавила index.php. Теперь нам нужно этот файл загрузить в контейнер, причём сделать это надо так, чтобы он уже был там к моменту запуска контейнера. Для этого у нас есть команда COPY для докерфайла, которая скопирует нужный нам файл ещё на этапе сборки образа. Изменим докерфайл, добавив в него эту команду. Директория, в которой хранятся файлы настроек для сайтов на nginx находится по пути /etc/nginx/conf.d/ . У меня сейчас это выглядит так:


Dockerfile:
FROM nginx 

COPY ./default.conf /etc/nginx/conf.d/

Давайте заново соберём образ. Для этого не забудьте удалить старый собранный образ:

docker image rm web-server

А затем выполнить:

docker build -t web-server .

Теперь запустим контейнер. Давайте сразу привяжем к нему директорию через флаг -v, как это уже делали в прошлой статье:

docker run -d --name my-nginx -p 80:80 -v ./app/:/var/www/html/ web-server

После запуска контейнера, мы можем зайти в браузер и перейти на http://localhost:80. И должны увидеть там сообщение из прошлой статьи Hello from Apache 😅. Это потому что мы не поменяли содержимое файла ./app/index.html. Ну что ж, если вас смущает, можете сами изменить его.

Но вот более серьёзная проблема: на нашем веб-сервере пока ещё нет php. Если мы перейдём на http://localhost:80/test.php, то браузер просто попробует скачать этот файл, но не сможет отобразить его содержимое.

Итак, пришло время добавить ещё один сервис в наш проект. Для этого сначала давайте немного обновим его структуру. В корне проекта создадим директорию services. Внутри неё создадим директорию nginx и переместим в неё докерфайл, который создавали ранее. Также удобнее будет, если конфигурационный файл нашего сайта мы положим в отдельную директорию внутри nginx. Давайте создадим в ней директорию conf.d и поместим в неё наш файл default.conf.

Новая структура будет выглядеть так:

Также, в связи с тем, что расположение файлов поменялось, давайте обновим докерфайл, в котором изменим команду COPY:


Dockerfile:

# было 
COPY ./default.conf /etc/nginx/conf.d/ 
# стало 
COPY ./conf.d/ /etc/nginx/conf.d/

Как видите теперь мы копируем не один конкретный файл, а всю директорию. Да, в ней находится один конкретный файл, но если бы их было больше — копировались бы все. Мы можем пересобрать наш образ, но это пока что ничего не поменяет. Поэтому пока не будем. Лучше займёмся php.

PHP-fpm

Итак, у нас есть выбор разных версий php. Для nginx нам нужен php-fpm. Можем зайти на докер-хаб, найти там php и посмотреть какие варианты доступны. Мы с вами будем использовать php 8.3. Ну раз она уже вышла )

Для начала создадим директорию для сервиса. В директории services создаём php, внутри которой создаём новый Dockerfile. Также здесь же давайте создадим файл hello.php, которым мы просто протестируем что php работает после запуска контейнера. В данный докерфайл помещаем следующий код:


./services/php/Dockerfile:
FROM php:8.3-fpm 

COPY ./hello.php /var/www/

hello.php:
<?php 

echo "Hello World\n";

Что ж. Пора собрать образ и запустить контейнер с php. Выполняем следующую команду:

docker build -t php-fpm ./services/php/

Как видите теперь вместо . мы используем путь ./services/php/ — потому что нужный нам докерфайл лежит именно там. Дожидаемся сборки образа и запускаем контейнер.

docker run -d --name fpm -v ./app/:/var/www/html/ php-fpm

Мы не привязываем порты, здесь это просто не нужно. Теперь проверим всё ли работает. Для этого нам нужно попасть внутрь контейнера. Для этого у нас существует следующая команда:

docker exec -it fpm bash

Данная команда позволяет выполнить нам любую команду внутри запущенного контейнера. А если мы используем флаг -it и команду bash, это позволяет нам получить доступ к виртуальному терминалу контейнера.

У вас может появиться вот такое сообщение: «the input device is not a TTY. If you are using mintty, try prefixing the command with ‘winpty’»

Если это так, то попробуйте использовать команду выше в таком варианте:

winpty docker exec -it fpm bash

Введя команду, мы попали в терминал операционной системы, которая установлена в контейнере. Можете, кстати, в этом убедиться, если введёте команду:

cat /etc/os-release

Вам отобразится информация вроде такой:


PRETTY_NAME="Debian GNU/Linux 12 (bookworm)" 
NAME="Debian GNU/Linux" 
VERSION_ID="12" 
VERSION="12 (bookworm)" 
VERSION_CODENAME=bookworm 
ID=debian 
HOME_URL="https://www.debian.org/" 
SUPPORT_URL="https://www.debian.org/support" 
BUG_REPORT_URL="https://bugs.debian.org/"

Если вспомнить прошлую статью, то мы знаем, что наша ос внутри wsl — это Ubuntu, а не Debian. Значит мы действительно в терминале контейнера.

Продолжим. Нам нужно проверить работает ли php… Вообще удивительно было бы, если б он не работал, но на всякий случай вводим:

php /var/www/hello.php

И видим результат Hello World в терминале. Пробуем зайти на http://localhost:80/test.php. Нет, не работает, снова браузер пытается скачать php-файл. На самом деле опять же не удивительно. Веб-сервер у нас в одном контейнере, а php — в другом. Контейнеры изолированы друг от друга и не взаимодействуют. Для того чтобы они начали обмениваться информацией между собой и работать сообща, их надо объединить в сеть, она же network.

Да, не забудьте выйти из контейнера. Делается это простой командой:

exit

И мы снова оказываемся в директории нашего проекта.

Docker Network

Пришло время познакомиться с сетями внутри докера. Сейчас мы сделаем всё вручную для начала, чтобы вы понимали как это работает, а затем будет магия.

Итак, network в докере позволяет связать несколько контейнеров между собой и обмениваться не только данными, но и грубо говоря функционалом. Например, при связке nginx+php, наш контейнер на nginx будет пользоваться сревисом из контейнера php для отображения файлов, написанных, собственно, на php. Для начала давайте посмотрим, что там вообще сейчас есть:

docker network ls

У меня вот такой набор. У вас чего-то может не быть, что-то может быть иначе, но плюс-минус будет такое:


NETWORK ID     NAME         DRIVER   SCOPE 
3fa1054f50fe   bridge       bridge   local 
585243df24e4   host         host     local 
03cb15416026   none         null     local 
f37322f37c34   web-network  bridge   local

Что у нас тут есть. ID — это понятно, Name — это тоже понятно, Driver — это способ коммуникации между контейнерами. Они отличаются, но мы пока будем использовать один — bridge — это драйвер по умолчанию и он нас пока что полностью устроит. Ну и Scope — это рабочее пространство, у нас всё local, так что не заморачиваемся сильно.

Чтобы ничего не нарушить в существующих нетворках, давайте создадим отдельно свой.

docker network create --driver bridge docker-network

Теперь повторим предыдущую команду docker network ls и увидим, что у нас появилась новая сеть с именем docker-network. Отлично. Теперь нужно в эту сеть добавить оба наших контейнера: веб-сервер на nginx и php-fpm. Убеждаемся, что контейнеры запущены:


docker ps -a 

output: 
СONTAINER ID   IMAGE       COMMAND                  CREATED               STATUS             PORTS                NAMES 
20cd177c4f77   php-fpm     "docker-php-entrypoi…"   4 seconds ago         Up 4 seconds       9000/tcp             fpm 
0396d52a0992   web-server  "/docker-entrypoint.…"   About a minute ago    Up About a minute  0.0.0.0:80->80/tcp   my-nginx

Добавляем наши контейнеры в сеть:

docker network connect docker-network my-nginx
docker network connect docker-network fpm

Также можно добавить контейнер в сеть на этапе запуска через команду docker run, добавив в неё флаг —network и после него указав название сети. Но раз уж у нас всё запущено, то делаем как делаем.

Теперь стоит проверить, действительно ли мы всё добавили в сеть. Для этого выполняем команду:

docker network inspect docker-network

И видим соответствующий вывод:


[ 
    { 
        "Name": "docker-network", 
        "Id": "2ea988a779c6d14c262f8daa436fa2a0d79f7e8618a8f51070c11270d0dab6e2", 
        "Created": "2023-12-30T09:08:04.879420819Z", 
        "Scope": "local", 
        "Driver": "bridge", 
        "EnableIPv6": false, 
        "IPAM": { 
            "Driver": "default", 
            "Options": {}, 
            "Config": [ 
                { 
                    "Subnet": "172.30.0.0/16", 
                    "Gateway": "172.30.0.1" 
                } 
            ] 
        }, 
        "Internal": false, 
        "Attachable": false, 
        "Ingress": false, 
        "ConfigFrom": { 
            "Network": "" 
        }, 
        "ConfigOnly": false, 
        "Containers": { 
            "0396d52a09929c7cb092bc7ebf4e482dd3694cdb46c1041335e50b436f0d7367": { 
                "Name": "my-nginx", 
                "EndpointID": "5797e1e42e95c503cc87c23cbf9de50597230baa4d3170a9cae82daed109ef33", 
                "MacAddress": "02:42:ac:1e:00:02", 
                "IPv4Address": "172.30.0.2/16", 
                "IPv6Address": "" 
            }, 
            "20cd177c4f77cdc7d48e741d01d535328d315817a0f8971664401d19291b7898": { 
                "Name": "fpm", 
                "EndpointID": "cfc09289ff56e50d3cb21fd39d03725f260b95e1e9ba3964185465ac6d04f9b3", 
                "MacAddress": "02:42:ac:1e:00:03", 
                "IPv4Address": "172.30.0.3/16", 
                "IPv6Address": "" 
            } 
        }, 
        "Options": {}, 
        "Labels": {} 
    } 
]

В разделе Containers видно что два наших контейнера есть в сети, у них свои ip адреса, внутренние идентификаторы и т.д. и т.п. Настало время проверить, а есть ли доступ из одного контейнера в другой. Давайте зайдём в контейнер сервера и попробуем пропинговать контейнер php.

docker exec -it my-nginx bash

И после этого пробуем следующее (у моего контейнера fpm внутренний ip — 172.30.0.3, у вас скорее всего будет свой, так что не забудьте поменять):

ping 172.30.0.3 -c 2

Я лично вижу у себя сообщение «bash: ping: command not found». Это значит, что в оболочке контейнера нет утилиты для пингования. Не беда. Установим его. Сначала вводим команду:

apt-get update

Ждём окончания обновления. Затем:

apt-get install -y iputils-ping

Дожидаемся установки и снова пробуем пинговать:


ping 172.30.0.3 -c 2 

output: 
PING 172.30.0.3 (172.30.0.3) 56(84) bytes of data. 
64 bytes from 172.30.0.3: icmp_seq=1 ttl=64 time=0.177 ms 
64 bytes from 172.30.0.3: icmp_seq=2 ttl=64 time=0.048 ms 
--- 172.30.0.3 ping statistics --- 
2 packets transmitted, 2 received, 0% packet loss, time 1019ms 
rtt min/avg/max/mdev = 0.048/0.112/0.177/0.064 ms

2 пакета отправлено, 2 доставлено, 0% потерь. Всё ок, наш контейнер nginx имеет доступ к контейнеру php. Осталась самая малость. Нужно настроить для нашего сайта использование php. Для этого поправим файл настроек ./services/nginx/conf.d/default.conf. После правки он у меня выглядит так:


server { 
    listen 80; 
    listen [::]:80; 
    root /var/www/html; 

    location / { 
        index index.php index.html index.htm; 
    } 

    location ~ \.php$ { 
        fastcgi_pass 127.0.0.1:9000; 
        fastcgi_index index.php; 
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; 
        include fastcgi_params; 
    } 
}

Во-первых я вынесла root из раздела location / в корень настроек сервера. Во-вторых я добавила ещё один блок location — location ~ .php$, т.е. инструкция как серверу быть с php-файлами. Здесь мы говорим по какому порту у нас работает php-fpm и прочее.

Давайте пересоздадим наш контейнер веб-сервера, чтобы обновить файл настроек внутри контейнера.


docker stop my-nginx 
docker rm my-nginx 
docker image rm web-server

После этих команд выполняем снова:


docker build -t web-server ./services/nginx/ 
docker run -d --name my-nginx -p 80:80 -v ./app/:/var/www/html/ -v ./services/nginx/conf.d/:/etc/nginx/conf.d/ web -server

Я обновила команду docker run для нашего сервера добавив вторую привязку директорий. ./services/nginx/conf.d/ в /etc/nginx/conf.d/ чтобы мы могли работать с файлом конфигурации сайта также, как и с приложением, без пересоздания образа и контейнера.

Когда снова всё запустилось привязываем контейнер к сети заново:

docker network connect docker-network my-nginx

Замечательно. Можно перейти на http://localhost:80/test.php иииии….. 502 ошибка. Что-то пошло не так. Точнее не туда. Вспоминаем, что контейнеры у нас собраны в сеть, а у сети этой внутренние ip не равны стандартным 127.0.0.1. Это значит, что нам надо в файле конфигурации обновить строку «fastcgi_pass 127.0.0.1:9000;», заменив в ней стандартный ip подходящим из нашей сети. Но мы сделаем лучше, потому что это докер )

Заходим в контейнер веб-сервера:

docker exec -it my-nginx bash

Вспоминаем что после пересоздания образа и контейнера у нас отсутствует утилита для пингования. Вводим:

apt-get update && apt-get install -y iputils-ping

Ждём установки и делаем следующее:

ping fpm -c 2

И получаем результат: 2 пакета отправлено, 2 пакета принято, 0 потерь. Т.е. внутри сети мы можем обращаться к контейнерам не по ip, а прямо по именам. Пока не выходим из контейнера, оставим его в терминале. Идём в файл default.conf и меняем:


fastcgi_pass 127.0.0.1:9000; 
на 
fastcgi_pass fpm:9000;

Т.к. мы связали директории, то ничего пересоздавать не нужно. Нужно просто перезагрузить nginx. Делаем это прямо внутри контейнера командой:

nginx -s reload

Видим сообщение «signal process started». И снова идём на http://localhost:80/test.php и ура, наконец-то. Мы видим долгожданную надпись Hello from PHP (ну или информацию phpinfo(), если в прошлой статье вы меняли код файла test.php).

Замечательно. Мы связали два контейнера и заставили их работать вместе. Но как-то громоздко это всё. Не находите? Если добавлять сюда таким же образом ещё базу данных и phpMyAdmin, то мы тут от старости помрём. Давайте лучше сразу к магии 😉

Docker Compose

Docker Compose — это ещё один инструмент внутри докера, который позволяет создать «композицию» контейнеров. Иными словами сборку того что вам нужно. При этом быстро запускать её и вообще очень классная штука. Начинается всё с файла compose.yml или docker-compose.yml. Второй вариант считается устаревшим, но вроде как всё ещё работает. Но зачем писать больше, если можно написать меньше. Используем первый вариант.

Создаём в корне нашего проекта файл compose.yml. Здесь есть свои правила, основное из них — отступы. Всё должно быть ровненько, если вложено, то на одну табуляцию, если не вложено, то вровень с остальным. Для начала давайте остановим наши контейнеры и удалим их, и их образы. И сеть желательно тоже удалить, мы её будем создавать через compose.yml и здесь это сделать гораздо проще. Главное не удалять докерфайлы и конфиг для сайта.


docker rm -f fpm 
docker rm -f my-nginx 
docker image rm web-server
docker image rm php-fpm 
docker network rm docker-network

Как видите вместо остановки и затем удаления контейнеров я использовала флаг -f, что означает forced, т.е. принудительное удаление. Оно производит сразу остановку и удаление контейнера. Но будьте осторожны, всё что связано со словами forced и hard при ошибках имеет серьёзные последствия.

Переходим в файл compose.yml и сразу создадим сеть. Для этого указываем:


networks: 
    docker-lesson: 
        driver: bridge

Наша новая сеть будет называться docker-lesson, также как и директория проекта. По сути всё, сеть готова, нужно только добавить к ней наши сервисы. А для этого их надо создать. Идём ниже и создаём раздел сервисов и помещаем в него веб-сервер и php:


services: 

    nginx: 
        container_name: nginx 
        build: 
            context: ./services/nginx/ 
        ports: 
            - 80:80 
        volumes: 
            - ./services/nginx/conf.d/:/etc/nginx/conf.d/ 
            - ./app/:/var/www/html/ 
        networks: 
            - docker-lesson 

    php: 
        container_name: fpm 
        build: 
            context: ./services/php/ 
        volumes: 
            - ./app/:/var/www/html/ 
        networks: 
            - docker-lesson

Ключевое слово services открывает раздел описания сервисов. Следующее слово на уровень глубже будет именем сервиса. Я указала просто nginx. Но можно было указать и web-server, и просто server или как угодно. Далее идёт описание сервиса. container_name — очевидно имя контейнера, которое мы задавали через флаг —name. build — это настройка сборки образа. Мы используем докерфайл, который у нас уже был создан ранее, поэтому в свойстве context указываем путь к докерфайлу. Далее идут ports — порты. volumes — это те самые привязки директорий, которые мы делали через флаг -v. Ну и указание нетворка, в котором контейнер будет работать. Если вы, как и я здесь, указываете нетвок последним, добавьте после него пустую строку, иначе парсер не поймёт, что у вас указан список, и будет ругаться.

Собственно в php настройки те же, только их меньше. Нам не нужны порты и у нас нет дополнительного вольюма.

А теперь самое интересное. Идём в терминал и набираем команду:

docker compose up -d

Происходит сборка и запуск ваших контейнеров. Давайте заглянем на http://localhost/test.php и убедимся, что веб-сервер работает и взаимодействует с php. И всё действительно запустилось и работает.

Выключение всей этой пачки происходит командой:

docker compose down

Контейнеры выключаются и удаляются. Остаются только собранные образы. Образы при сборке через docker compose создаются с уникальными именами, которые являются соединением названия директории и названия сервиса. В нашем случае, если мы зайдём в Docker Desktop в раздел образов, найдём образы docker-lesson-nginx и docker-lesson-php.

Добавляем MySQL и phpMyAdmin

Мы потихоньку подбираемся к окончанию статьи. Осталось совсем немного. Давайте установим MySQL в нашу сборку. Делается это достаточно просто. Заходим в докер-хаб и вбиваем в поиске mysql. Переходим в репозиторий и листаем страничку до раздела «… via docker-compose».

В этом разделе указан код для файла compose.yml. На момент написания там указаны устаревшие варианты, например файл docker-compose.yml. Это ничего страшного, потому что по сути ничего не поменялось. В раздел сервисов у нас в проекте в файл compose.yml копируем раздел db со страницы репозитория:


db: 
    image: mysql 
    # NOTE: use of "mysql_native_password" is not recommended: https://dev.mysql.com/doc/refman/8.0/en/upgrading-from-previous-series.html
    #upgrade-caching-sha2-password 
    # (this is just an example, not intended to be a production configuration) 
    command: --default-authentication-plugin=mysql_native_password 
    restart: always 
    environment: 
        MYSQL_ROOT_PASSWORD: example

Удаляем комментарий, для разработки это не важно. Но! если будете использовать докер в продакшене, не забывайте, что аутентификация с нативным паролем является малонадёжной. В документации MySQL есть на эту тему много указаний. Также я поменяла пароль для пользователя root, добавила имя контейнера и порты, и добавила контейнер в наш нетворк. У меня получилось следующее:


db: 
    container_name: db 
    image: mysql 
    command: --default-authentication-plugin=mysql_native_password 
    restart: always 
    environment: 
        MYSQL_ROOT_PASSWORD: qwerty 
    ports: 
        - 3306:3306 
    networks: 
        - docker-lesson

Как видите у нас тут есть несколько новых ключевых слов.


image - здесь указывается название образа из репозитория на докер-хабе. По сути это можно использовать вместо build context и докерфайла, если вы ничего в докерфайле не собираетесь делать. Мы чуть позже также обновим настройку для nginx, т.к. по сути нам докерфайл для него больше не нужен. 

command - это ключевое слово используется для указания команды, с которой будет запускаться контейнер. Для некоторых контейнеров это необходимо, для некоторых - нет. Поэтому если используете новый контейнер, ознакомьтесь с его описанием на докер-хабе. Если для запуска необходимы какие-либо команды - там это будет указано. 

restart - данная директива говорит о том, как поступать с контейнером, если он вдруг "упал". В нашем случае указано restart: always, что значит в случае отключения - перезапустить. 

environment - это раздел, где указываются переменные среды для контейнера. Также, как и command эта директива обязательна или желательна для определённых контейнеров, для других же она может быть и не нужна. Здесь у нас указана переменная MYSQL_ROOT_PASSWORD, т.е. пароль пользователя root.

В описаниях репозиториев некоторых контейнеров можно найти также описания часто используемых переменных среды. Также советую обращать на это внимание.

Давайте сразу добавим phpMyAdmin и будем смотреть что получилось. Для phpMyAdmin повторяем те же шаги, что и для MySQL. Идём в докер-хаб, ищем phpMyAdmin, заходим и ищем инструкции для Docker Compose. В репозитории для MySQL вы могли заметить в инструкции для Docker Compose два сервиса: саму базу данных и панель управления adminer. По сути adminer похож чем-то на phpMyAdmin, но он гораздо беднее по интерфейсу (моё личное мнение если что, прошу несогласных не ругаться). Также и в репозитории phpMyAdmin мы видим два сервиса: базу данных, только здесь указана MariaDB, а не MySQL, и собственно phpMyAdmin. Копируем последний и вставляем в наш compose.yml:


phpmyadmin: 
    image: phpmyadmin 
    restart: always 
    ports: 
        - 8080:80 
    environment: 
        - PMA_ARBITRARY=1

Также, как и с контейнером базы данных, я добавила имя контейнера, нетворк, и ещё одну директиву. Получилось вот что:


phpmyadmin: 
    container_name: phpmyadmin 
    image: phpmyadmin 
    restart: always 
    ports: 
        - 8080:80 
    environment: 
        - PMA_ARBITRARY=1 
    networks: 
        - docker-lesson 
    depends_on: 
        - db

Здесь уже все знакомые нам параметры, кроме одного. depends_on — означает зависимость от контейнера. В данном случае нам важно, чтобы сама база данных уже была запущена к моменту запуска контейнера phpMyAdmin. Следовательно указываем depends_on и пишем имя сервиса (именно сервиса, не контейнера), от которого у нас зависимость. Теперь при запуске нашей пачки контейнеров будет всегда сначала запускаться MySQL, и только после этого phpMyAdmin. И в обратном порядке при выключении — будет сначала выключаться phpMyAdmin, а уже затем MySQL. Зависимость от php нам тут не нужна, т.к. в контейнере phpMyAdmin уже есть свой предустановленный php.

Ну что ж, останавливаем предыдущую сборку и пробуем запускать заново:


docker compose down
docker compose up -d

В Docker Desktop на вкладке контейнеров у меня теперь запущено 4 контейнера: nginx, fpm, db и phpmyadmin. По виду — всё работает. Давайте перейдём на http://localhost:8080, чтобы посмотреть phpMyAdmin. Там сразу будет понятно работает ли база данных и т.д. и т.п.

У меня открылась админка phpMyAdmin, я ввела в поля логин и пароль root и qwerty соответственно и попала внутрь. Здесь у нас чистая система, только дефолтные базы данных, которые есть всегда по умолчанию. Это всё, конечно, хорошо. Но нам надо проверить а действительно ли наша база данных в phpMyAdmin, в MySQL одна и та же, и к ней есть доступ из контейнера nginx и php.

Давайте в файле ./app/test.php создадим запрос к базе, а потом через phpMyAdmin посмотрим есть ли в ней что-нибудь. Переходим в test.php и пишем следующий код (для начала просто пытаемся подключиться к базе данных):


<?php 

$db = new mysqli('localhost', 'root', 'qwerty'); 

var_dump($db);

И я у себя вижу Fatal error. Отсутствует класс mysqli. Т.е. расширение mysqli для php не установлено. Вот тут нам и пригодится докерфайл. Идём в ./services/php/, открываем Dockerfile и дописываем в конец файла следующее:


RUN pecl install xdebug \ 
    && docker-php-ext-enable xdebug \ 
    && docker-php-ext-install pdo mysqli pdo_mysql

Я решила сразу же установить xdebug, для более комфортного вывода ошибок и дампов. Затем xdebug надо включить командой docker-php-ext-enable xdebug. Ну и ещё установить расширения pdo, mysqli и pdo_mysql (у последнего не должно быть i в конце, это важно). По идее pdo должен быть установлен, но на всякий случай запросим установку. Дважды всё равно не установится, а вот два последних точно нужны.

Теперь нам нужно остановить нашу сборку, удалить образ контейнера для php, т.к. он собран с более ранним вариантом докерфайла и запустить сборку заново. Сборка будет происходить дольше, т.к. для контейнера php нужно будет порядочно докачать всякого. Пишем в терминале:

docker compose down

Ждём остановки всех контейнеров и идём в Docker Desktop. Открываем раздел образов и ищем наш образ. Он называется docker-lesson-php. Можем удалить прямо отсюда, можем через терминал командой:

docker image rm docker-lesson-php

После удаления снова запускаем нашу сборку:

docker compose up -d

И ждём окончания запуска. У меня сборка всех образов и контейнеров заняла почти 70 секунд. Это при условии, что базовые образы все уже скачаны и нужно было только докачать и установить расширения.

Ну что ж. Идём снова на http://localhost/test.php и смотрим, осталась ли ошибка. Старая ошибка исчезла, но при этом у меня появилась новая: Fatal error: Uncaught mysqli_sql_exception: No such file or directory in /var/www/html/test.php on line 3.

Мы не можем получить доступ к базе данных. Здесь есть несколько вариантов решений, либо иначе запустить сборку, либо можно просто подключаться по ip или имени контейнера. Контейнер у нас называется db, значит в файле test.php меняем localhost на db и пробуем снова.


<?php 

$db = new mysqli('db', 'root', 'qwerty'); 

var_dump($db);

Отлично. Подключение к серверу базы данных создано, а благодаря расширению xdebug — информация выводится в удобном и подробном виде. Пришло время создать базу данных и посмотреть поменяется ли что-нибудь в phpMyAdmin. Изменим в test.php код на следующий:


<?php

$db->query("CREATE DATABASE test"); 
$db->connect('db', 'root', 'qwerty', 'test'); 
var_dump($db->errno, $db);

У меня выводится 0 и описание подключения к базе данных. Т.е. ошибок данный код не вызвал. Идём на http://localhost:8080 и видим, что база данных test действительно существует. Давайте создадим в ней через админку простую таблицу и запросим данные через php. Так сказать обратная связь.

Создаём таблицу с именем my_table с двумя полями id — int и text — varchar(255). Вставляем в них любые удобные значения и сохраняем. Идём в test.php и меняем код на:


<?php 

$db = new mysqli('db', 'root', 'qwerty', 'test'); 
$result = $db->query("SELECT * FROM my_table"); 
var_dump($result, $result->fetch_all(MYSQLI_ASSOC));

Переходим в браузер и видим результат (у меня такой):

Volumes

Осталось разобрать один очень важный и полезный инструмент докера — это volumes. Знакомое слово, мы его указывали в файле compose.yml, но его можно использовать ещё немного иначе.

Давайте полноценно перезапустим нашу сборку.


docker compose down 
docker compose up -d

И снова зайдем на http://localhost/test.php. Что мы видим… ошибка, Connection refused. Зайдём в phpMyAdmin и… Нашей базы данных test больше нет. Контейнер по какой-то причине умер, запустился заново и мы потеряли все ценные данные. Тут нам на помощь придут вольюмы. Они позволяют сохранять состояние контейнера даже при его полном перезапуске.

Снова открываем compose.yml и идём в самый-самый низ файла. Дописываем на уровне networks и services (помните, что вложенность читается в yml именно отступами, а мы не сервис делаем, а новый раздел):


volumes: 
    db_data: 
        driver: local

Таким образом мы создаём volume с именем db_data, который будет хранить в себе данные контейнера. Теперь привязываем его к контейнеру базы данных:


db: 
    container_name: db 
    image: mysql 
    ... 
    volumes: 
        - db_data:/var/lib/mysql/ 
    ...

Здесь мы указываем какие данные хранить в вольюме. Данные баз данных (простите за тавтологию) хранятся в директории /var/lib/mysql/ — это в MySQL. В других базах местоположение данных будет отличаться. Давайте снова полностью перезапустим нашу сборку.


docker compose down 
docker compose up -d

И создадим какую-нибудь базу данных через phpMyAdmin. Не обязательно её заполнять хоть чем-то. Если она сохранится после полного перезапуска — значит всё ок и вольюм работает. Перезапускаем, проверяем.

Возможно кто-то меня спросит, почему я не использую docker compose restart вместо двух команд down + up. Объясню… Когда вы запускаете рестарт — у вас перезапускаются контейнеры, но не происходит ребилд всего, что есть в сборке. Т.е. если вы что-то поменяли в файле compose.yml, оно применится только после down + up, а при рестарте всё будет работать также, как и до изменений. Попробуйте поменять MYSQL_ROOT_PASSWORD и сделать рестарт. Вас даже не разлогинит из phpMyAdmin.

Завершение

Итак, подведём итоги. Мы с вами начинали с запуска контейнеров в терминале по одному, ручной связке их в сеть и т.д. и т.п. и закончили одним файлом, который упрощает запуск всех необходимых сервисов и сетей одной командой. Да, соглашусь, тема не из простых в целом. Но на базовом уровне пользоваться докером с использованием compose.yml в целом не сложно. Последние проекты у меня все запускаются в среде докера. Это гораздо удобнее, чем настраивать локальную машину для работы с определённым стеком технологий. Тем более, что никто не запрещает использовать заготовки. Я вот завела себе репозиторий на гитхабе, где буду хранить определённые сборки, с которыми чаще всего приходится работать. Можете пользоваться кстати.

Клонируете репозиторий куда вам удобно, переходите в ветку с соответствующей конфигурацией сборки и всё. docker compose up -d и можно работать.

На этом на сегодня закончим. Благодарю, что прочли.

Удачи вам в работе и обучении 🐾

P.S.:

Обещала оптимизацию контейнера nginx. Можно поступить так: удаляем Dockerfile из ./services/nginx/. Затем в файле compose.yml удаляем строки:


build: 
    context: ./services/nginx/

И вместо них пишем:

image: nginx

Таким образом мы будем в сборке всегда иметь свежую версию nginx и у нас не будет лишнего докерфайла.

Ну и напоследок оставлю вам полный текст моего compose.yml, который у меня получился к концу данной статьи:


networks:
  docker-lesson:
    driver: bridge

services:
  
  nginx:
    container_name: nginx
    image: nginx
    ports:
      - 80:80
    volumes:
      - ./services/nginx/conf.d/:/etc/nginx/conf.d/
      - ./app/:/var/www/html/
    networks:
      - docker-lesson

  php:
    container_name: fpm
    build:
      context: ./services/php/
    volumes:
      - ./app/:/var/www/html/
    networks:
      - docker-lesson

  db:
    container_name: db
    image: mysql
    command: --default-authentication-plugin=mysql_native_password
    restart: always
    volumes:
      - db_data:/var/lib/mysql/
    environment:
      MYSQL_ROOT_PASSWORD: qwerty
    ports:
      - 3306:3306
    networks:
      - docker-lesson

  phpmyadmin:
    container_name: phpmyadmin
    image: phpmyadmin
    restart: always
    ports:
      - 8080:80
    environment:
      - PMA_ARBITRARY=1
    networks:
      - docker-lesson
    depends_on:
      - db

volumes:
  db_data:
    driver: local

Категории:

,

Комментарии

  1. Аватар пользователя Александр К.
    Александр К.

    Alina, спасибо! Пригодилось. Можете, кстати, обратить внимание на ddev. Для локалтной разработки очень хорошая вещь.

    1. Аватар пользователя AlinaLedova

      Рада, что смогла помочь )

      Ddev штука неплохая, но всё же это больше похоже (как мне кажется) на варианты по типу open server или подобных заготовок, только на базе докера. Мне по большей части самого докера хватает. Тем более что докер можно использовать не только для локальной разработки.

  2. Аватар пользователя Kostiantyn
    Kostiantyn

    Спасибо, статья хорошая, но к сожалению работы php_fpm не добился! Делал сначала на базе своих контейнеров потом сделал полностью по вашей инструкции, может гдето чего то не хватает! После запуска до сборки докера увидел страничку index.html, т.е. резолва на php fpm не произошло!

    1. Аватар пользователя AlinaLedova

      Спасибо что прочли. Могу лишь гадать, почему php_fpm не завёлся. Есть несколько предположений:
      1. Проверьте пути связки контейнеров и проекта: и в nginx контейнере, и в php контейнере путь к приложению должен быть одинаковым;
      2. Проверьте конфигурацию nginx — в секции location / параметр index должен быть в первую очередь указан index.php, затем уже index.html. Можно вообще без последнего, если главная страница будет 100% только php-файлом;
      3. Также в конфигурации nginx важный момент в секции location ~ \.php$ нужно в строке fastcgi_pass 127.0.0.1:9000 заменить ip на внутренний ip нетворка, либо на название контейнера типа такого fastcgi_pass fpm:9000 (у меня сервис называется php, а имя контейнера fpm, нужно именно имя контейнера); Это нужно т.к. сеть докера общается по внутренним виртуальным ip адресам, для него 127.0.0.1 фактически существует столько штук, сколько у вас контейнеров и каждый адрес 127.0.0.1 ссылается на свой контейнер;
      4. Ну и крайний вариант — попробовать зайти в контейнер php и проверить вообще он работает или нет. Мало ли, всякое случается.

Добавить комментарий для AlinaLedova Отменить ответ

Ваш адрес email не будет опубликован. Обязательные поля помечены *