В данной статье мы продолжим изучение 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
Добавить комментарий для Kostiantyn Отменить ответ