В этой статье я расскажу несколько возможных вариантов настройки локального окружения для php разработки с использованием docker. Изначально я планировал написать более широкую статью, и расписать как локальную часть разработки, то есть установку всего на компьютере конкретного разработчика, так и устройство удаленного dev сервера на котором происходит тестирование проекта и сливается код от нескольких программистов. Но статья выходит слишком большая, поэтому в данной статье мы рассмотрим только локальное окружение.
В статье я больше буду акцентироваться именно на windows системе (в примере все работает на windows 10). Однако на mac насколько я понимаю все работает примерно так же, там тоже поднимается отдельная виртуалка с докером, на которой уже запускаются контейнеры. На линуксе контейнеры запускаются безо всяких виртуалок, прям на месте, так что для линуксоидов все будет даже еще проще чем описано в статье. Не стоит рассматривать описанное в статье окружение как готовый рецепт, пусть все работает при правильной реализации, но это просто пример, только один из возможных вариантов.
Что я вообще хочу от докера при разработке.
Во первых, мне нужна локальная копия сайта, именно на этой локальной копии я буду смотреть как собственно работает код. Эта локальная копия должна открываться по красивому адресу, например если основной рабочий сайт это example.com, то моя локальная копия должна открываться по адресу my.example.com. Безо всяких портов типа my.example.com:8747. Я часто открываю сразу несколько проектов и все открытые проекты не должны конфликтовать, то есть например у меня открыто два окна phpstorm в одном ведется разработка проекта example.com, во втором ведется разработка test.ru, и для каждого из этих проектов одновременно должны работать два локальных сайта my.example.com и my.test.ru.
Во вторых, мне под каждый проект нужны специальные настройки (mbstring, timezone) и расширения php (типа xdebug).
В третьих, мне нужны внешние инструменты типа phpmyadmin для работы с БД проекта или node.js для работы с gulp/webpack.
И наконец в четвертых, все это должно быстро подниматься для новых разработчиков которые подключаются к проекту.
Поехали. Попробуем это все реализовать 🙂
Содержимое статьи
- 1 Дополнительное уточнение
- 2 Что такое docker?
- 3 Старт проекта
- 4 Docker compose
- 5 Создание front сети
- 6 Доступ к контейнерам пробросом портов
- 7 Доступ к контейнерам docker по IP
- 8 Доступ к контейнерам через nginx reverse proxy
- 9 Отладка php в docker через xdebug
- 10 Известные баги под windows
- 11 Подключение новых программистов
- 12 Итого
TL;DR
Используем docker-compose. В compose используем официальные контейнеры с docker hub, плюс небольшие правки в dockerfile. Докер в винде работает на виртуалке. Добраться до контейнеров внутри виртуалки можно тремя способами — пробросом портов, открытием публичного доступа по IP к контейнерам (плюс внутренний dns который автоматически строит пути), и reverse proxy на входе в виртуалку. Лучший способ работы с контейнерами внутри виртуалки это reverse proxy.
Дополнительное уточнение
В статье я постарался написать все довольно подробно. Но совсем уж до нуля разжевывать у меня не получится. Например про установку докера я ничего рассказывать не буду. В интернете очень много статей на эту тему, для начала прочитайте getting started на официальном сайте докера. Про то что такое volumes и как они работают я немного пояснил, но вам лучше тоже изучить этот вопрос отдельно. Аналогично про dockerfile, docker-compose и т.д. Лучше всего пробовать развернуть свое локальное окружение и все непонятные моменты искать в поисковиках или спрашивать в комментариях.
Кроме того желательно понимать зачем вам вообще локальное окружение именно на докер. Для большинства людей хватит виртуалок, обычного удаленного хостинга или любых других уже готовых сборок типа denver, xampp, open-server и т.д. Их достаточно установить обычным инсталлером винды и у вас будут работать локальные сайты. Для разработки большинства проектов ничего больше и не нужно. Пробовать организовать локальное окружение на докере стоит только если вы понимаете зачем это делаете, например вы хотите построить процесс continous integration, или вам нужны программы определенных версий, ну либо если вам нравится сам докер и принципы его работы, как по мне тоже существенная причина 🙂
Если вы будете разворачивать собственное окружение, то поймете, что там все не так просто и придется изучить много тонкостей. То что на хостингах все работает по умолчанию, это заслуга админов хостинга, здесь же вам придется настраивать все практически с нуля. Если вам интересно программирование и совсем не интересно администрирование серверов, то докер не для вас. Даже просто первый запуск сайта в докер окружении вскрывает целый пласт новых знаний, которые вам придется изучить. Без которых нет никакого смысла что-либо делать вообще. Например вам придется более детально разобраться в расширениях php, чем отличаются расширения mysql и mysqli? А без понимания у вас не факт что получится заставить работать тот же битрикс с БД на докере. И таких вопросов довольно много, это просто один пример с которым вы наверняка столкнетесь.
В общем если вам интересно только программирование, то заморачиваться с докером смысла нет. Если есть желание развиваться в области devops, то уже можно пробовать создавать что-то свое на основе этой статьи 🙂
Что такое docker?
Для тех кто совсем впервые услышал слово docker сделаю небольшое вступление. Docker это программа для управления контейнерами приложений. То есть нужен вам например сервер апач с модулем php, вы берете чистый линукс, ставите на этот чистый линукс собственно апач с нужными настройками и замораживаете все это дело. Вот этот вот замороженный кубик можно считать образом контейнера. Как будто бы чистая операционка с каким-то приложением. И везде где вам нужен апач вы теперь не ставите его с нуля, а создаете контейнер на основе собранного заранее образа. Так можно завернуть вообще все что угодно в образ. Нужен nginx, берем чистую ОС ставим только nginx и вот у нас образ c nginx. Аналогично образ с базой данных или любым другим приложением.
Образ это как будто бы такой эталон, который лежит в музее под стеклом, а контейнер это конкретная работающая копия эталона. Или еще другое сравнение для программистов, образ это как будто класс, а контейнер это как будто экземпляр класса созданный через $container = new ImageClass();
Докер позволяет наследовать образы. Есть много базовых образов с операционными системами, с ubuntu, debian, centos и т.д. Если вы хотите сделать свой образ с каким то приложением, вы просто говорите от какого базового образа будете наследоваться и далее уже поверх базы приделываете что-то свое. Более того, можно наследоваться не только от базового образа, но и от любого другого. Например вы находите готовый образ апача с модулем php, но в нем не хватает расширения mbstring, вы наследуетесь от этого найденного образа с апачем и добавляете в него нужное расширение. Не нужно проходить всю цепочку установок от голой ОС до апача, можно сразу править образ с апачем и потом на основе правленного образа создавать свои контейнеры. Во многих случаях даже не придется править сам найденный образ, разработчики образа обычно закладывают широкие возможности по кастомизации. Например образ mysql отнаследован от debian, разработчик образа поставил на голом debian mysql и дал возможность сразу при старте контейнера создавать базу данных с произвольным названием, произвольным пользователем и паролем, для добавления базы данных не нужно создавать новый образ, достаточно сконфигурировать контейнер в момент его создания.
Старт проекта
Любой проект начинается с папки. Создадим папку localdev в любом месте. В корне папки localdev сразу создайте пустой git репозиторий. В этой папке мы собственно и будем вести всю разработку php проекта. Ранее все проекты я разрабатывал на виртуалках, то есть на отдельной виртуалке с LAMP (Linux, Apache, Mysql, PHP) я поднимал сайт, который был доступен с хоста по IP адресу виртуалки. Локально на винде я хранил только нужные для разработки файлы, а не полную копию проекта, полная копия была только на самой виртуальной машине к которой я подсоединялся по SSH в IDE и менял там файлы. Поправил что-то, отправил правку вручную (ctrl+shift+S), проверил работу в браузере и дальше что-то пишешь. Пробовал поднимать vagrant, но он мне показался не очень удобным, хватало обычных виртуалок. В связи с этим в папке проекта у меня обычно прямо в корне лежали уже файлы сайта, например localdev/index.php это главная страница сайта. Однако сейчас в связи с переходом на docker мне кажется более удобным двухуровневая структура проекта. Но при этом против корня сайта сразу в корне проекта я ничего не имею 🙂
В данной же статье я опишу именно двухуровневую структуру, где корень сайта вынесен в подпапку проекта. В корне проекта создайте папку docker. В ней мы будем хранить все что касается докера. В ней мы будем билдить образ апача, будем хранить конфиги для всех приложений, в ней будет храниться база данных и т.д. В общем все что касается докера будет именно в этой папке. Я решил не размазывать это все по разным папкам в корне, так как там у нас будет еще много чего другого.
Далее опять таки в корне проекта создайте папку html. Именно эта папка будет корнем сайта. Файл /localdev/html/index.php будет главной страницей сайта.
Здесь же можно добавлять все что вы хотите, например можно поставить composer и папка vendor которую создает композер будет лежать тоже в корне проекта. Сюда можно добавить gulp или webpack и тогда в проекте добавятся папка node_modules, можно добавить bower и тогда в корне добавится папка bower_components. Кроме того в корне будет лежать тьма файлов для конфигурации, типа package.json, webpack.config.js, .gitignore и т.д. В общем корень проекта пустым точно не будет. И все это будет отделено от непосредственно файлов сайта.
Docker compose
Приступим собственно к сборке окружения. Выше я пояснял что такое контейнеры. Однако для нормальной работы сайта нам понадобится сразу несколько контейнеров. Как минимум контейнер php и контейнер с базой данных, но по факту их будет больше. Для конкретного сайта нам нужна группа контейнеров работающая совместно. Именно для совместного запуска группы контейнеров существует утилита docker-compose. Она запускает контейнеры в соответствии с заранее описанным файлом конфигурации. В статье я буду рассматривать вот такой файл docker-compose.yml:
version: "3"
services:
nginx:
image: nginx
volumes:
- ./docker/nginx/:/etc/nginx/conf.d/
networks:
- front
- backend
apache:
build: ./docker/apache
volumes:
- ./:/var/www/
- ./docker/apache/php.ini:/usr/local/etc/php/php.ini
networks:
- backend
db:
image: "mysql:5.7"
volumes:
- ./docker/db:/var/lib/mysql
environment:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: local
MYSQL_USER: local
MYSQL_PASSWORD: local
networks:
- backend
phpmyadmin:
image: phpmyadmin/phpmyadmin:latest
environment:
- PMA_HOST=db
- PMA_USER=local
- PMA_PASSWORD=local
volumes:
- /sessions
networks:
- backend
networks:
front:
external:
name: front
backend:
Еще раз повторю, это просто демонстрация, а не законченная конфигурация контейнеров которую я рекомендую использовать. Но для начала можно взять и этот вариант. Если своего варианта у вас пока что нет, то создайте пустой текстовый файл localdev/docker-compose.yml и вставьте туда вышеприведенный листинг. В дальнейшем вы в любом случае будете сами писать такие файлы с нуля или вносить нужные правки.
Если я открою проект в phpstorm и запущу всего одну команду
$ docker-compose up -d --build
То у меня заработает сайт my.example.com (который сразу, в течение пары секунд будет доступен в браузере) и я уже могу писать код и тестировать. Но у вас этот файл скорее всего не запустится, так как пока что не настроены сети, нет нужных конфигов в папке localdev/docker и т.д.
Рассмотрим что вообще написано в этом файле по порядку. Первая строка
version: "3"
Говорит программе docker-compose какая версия конфига используется. Третья версия нужна по сути только для docker swarm, которого здесь нет. В примере будет достаточно и второй версии. Но пусть будет третья 🙂
Строка
services:
говорит композу, что далее идет список контейнеров которые нужно запустить. Начнем изучать их по порядку
Nginx
Скопирую отдельно из общего файла конфига выше:
nginx:
image: nginx
volumes:
- ./docker/nginx/:/etc/nginx/conf.d/
networks:
- front
- backend
Строка image: nginx говорит докеру какой образ использовать для старта контейнера. В данном случае используется официальный образ nginx из официального репозитория докера. Я не вижу смысла указывать версию nginx, пусть он всегда будет latest.
Конфиг nginx
Строка volumes говорит докеру, что при запуске контейнера папку /etc/nginx/conf.d/ нужно заменить на localdev/docker/nginx/ из папки проекта. Причем связь будет постоянная. Если внутри контейнера изменить файл в этой папке, то он мгновенно изменится и на хосте и наоборот. В папке localdev/docker/nginx/ у меня лежат два файла default.conf и phpmyadmin.conf. Они примерно похожи, так что приведу здесь только default.conf:
server {
listen 80;
server_name my.*;
location / {
location ~ [^/]\.ph(p\d*|tml)$ {
try_files /does_not_exists @fallback;
}
location ~* ^.+\.(jpg|jpeg|gif|png|svg|js|css|mp3|ogg|mpe?g|avi|zip|gz|bz2?|rar|swf)$ {
try_files $uri $uri/ @fallback;
}
location / {
try_files /does_not_exists @fallback;
}
}
location @fallback {
proxy_pass http://apache:80;
proxy_redirect http://apache:80 /;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location ~ /\.ht {
deny all;
}
gzip on;
gzip_comp_level 5;
gzip_disable "msie6";
gzip_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript application/javascript;
}
Суть конфига в том, что nginx слушает 80 порт и вообще все запросы к судбдомену my.* редиректит к другому контейнеру (в данном случае к контейнеру apache). В файле phpmyadmin.conf лежит примерно то же самое, только там nginx ожидает субдомен pma.* и если запрашивают именно этот субдомен, то редиректит к контейнеру phpmyadmin.
Ну и плюсом я тут включил gzip сжатие, чтобы сразу оценивать сколько будут весить приходящие в браузер странички на проде.
Сети
Подробнее о сетях я хочу рассказать в следующем большом блоке про доступ к контейнерам по IP, здесь же замечу, что контейнер nginx единственный контейнер который будет находиться сразу в двух сетях. Об этом нам говорит строка networks, где собственно указаны эти две сети front и backend. Вообще все контейнеры будут работать в одной сети backend, которая создается при запуске docker-compose up, и нам вообще не важно что там будет твориться. А сеть front нужна нам для того чтобы обслуживать публичные контейнеры, именно в сети front будет работать локальный DNS и в этой сети у нас будут не вся эта гора контейнеров, которая там вообще не нужна, а только один контейнер nginx. Благодаря этому сеть front будет чистая и красивая 🙂
Если же прямой доступ по IP до контейнеров вам не нужен, то в файле docker-compose.yml можно вообще убрать все упоминания о сетях. Для разработки с использованием обратного прокси отдельные сети это лишний элемент. Пусть все контейнеры будут только в дефолтной сети которую создает docker-compose по умолчанию.
Apache
Апач это единственный контейнер в данной статье которому нужна сборка. Все остальные контейнеры получаются из готовых официальных образов, а контейнер апача нужно прямо билдить.
apache:
build: ./docker/apache
volumes:
- ./:/var/www/
- ./docker/apache/php.ini:/usr/local/etc/php/php.ini
networks:
- backend
Об этом говорит строка build. В директиве build указывается из какого dockerfile строить контейнер. В данном случае контейнер строится на основе localdev/docker/apache/Dockerfile
FROM php:5-apache
# PHP extensions
RUN apt-get update && apt-get install -y \
libfreetype6-dev \
libjpeg62-turbo-dev \
libmcrypt-dev \
libpng-dev \
&& pecl install xdebug \
&& docker-php-ext-install -j$(nproc) mbstring pdo_mysql tokenizer mcrypt iconv mysqli \
&& docker-php-ext-configure gd --with-freetype-dir=/usr/include/ --with-jpeg-dir=/usr/include/ \
&& docker-php-ext-install -j$(nproc) gd \
&& docker-php-ext-enable xdebug
# Apache modules
RUN a2enmod rewrite
Первая же строка говорит нам о том, что этот dockerfile наследуется от официального образа php, где php установлен как модуль апача. По ссылке вы можете выбрать любой другой образ, например вам может быть интереснее запустить проект на php 7, и тогда вы наследуетесь уже от php:7-apache, или вам может быть апач совсем не нужен, и тогда наследуетесь от любого другого образа, где php работает в режиме php-fpm. Cписок меток приведен опять таки по ссылке. В данном случае я делаю окружение с php 5.6, более консервативное так сказать 🙂
Весь состав dockerfile сделан на основе документации официального образа php. Эти страшные строки типа libfreetype6-dev нужны для установки библиотеки gd, можете сами там чуть ниже по ссылке отмотать и посмотреть. Строки вида docker-php-ext-install, docker-php-ext-enable, docker-php-ext-configure взяты тоже из официальной документации. Создатели образа php любезно предоставили нам удобные скрипты для добавления своих расширений в контейнер. Достаточно вызвать их скрипт и параметром передать туда названия расширений. То есть мы берем их официальный образ и специальными скриптами чуть изменяем его, после чего билдим чуть измененный образ отнаследованный от официального.
Строка a2enmod rewrite это включения модуля rewrite для апача. Чтобы в .htaccess можно было всякие правила преобразований прописывать. Эта команда есть во многих дистрибутивах и безо всякого докера, можете поискать a2enmod в поисковиках.
На основе этого dockerfile при старте окружения композом построится нужный нам контейнер. Если нам вдруг понадобится какое-то дополнительное расширение, то мы просто добавим его в Dockerfile и перезапустим окружение через:
$ docker-compose up -d --force-recreate --build
Первый билд может занять довольно продолжительное время, может быть даже несколько минут, все последующие запуски берутся из кеша, так что второй билд отрабатывает быстрее. Обычный запуск окружения, без билда, как я и говорил выше отрабатывает за секунды. То есть в течение нескольких секунд мы получим работающий локально сайт.
Apache volumes
Еще один значимый для понимая всей статьи момент. Для апача в docker-compose.yml у нас прописано сразу два volume (два диска).
Первый диск маунтится в /var/www контейнера apache. То есть вообще вся папка проекта маунтится в /var/www, папка /var/www становится корнем проекта. Так как в конфиге апача DocumentRoot прописан на папку /var/www/html, то это значит что папка html из нашего корня проекта как раз попадает в DocumentRoot. И файл localdev/html/index.php будет лежать в /var/www/html/index.php, в контейнере. На уровень выше от DocumentRoot апача у нас будут лежать все служебные папки типа vendor композера. Если вам по каким то причинам не нравится папка html, то можете поправить это дело в конфиге апача, сделать отдельный volume для /etc/apache2/sites-enabled именно в этой папке внутри контейнера лежат конфиги апача, замените дефолтный 000-default.conf на свой собственный, где пропишите например папку public_html и тогда в корне проекта нужно будет переименовать папку html в public_html.
Второй диск из docker-compose.yml маунтится в /usr/local/etc/php/php.ini, по умолчанию в контейнере вообще нет php.ini, и мы подставляем дефолтный. Вот пример для php.ini
max_execution_time = 60
memory_limit=256M
error_reporting = E_ALL & ~E_NOTICE & ~E_STRICT & ~E_DEPRECATED
[Date]
; Defines the default timezone used by the date functions
; http://php.net/date.timezone
date.timezone = Asia/Yekaterinburg
[mbstring]
mbstring.func_overload = 2
mbstring.internal_encoding = UTF-8
В данном случае я настроил только дату и mbstring. Ну плюс несколько основных параметров. Сюда можно прописать любые другие настройки и рестартовать контейнер. Официальный образ запустится и без этого файла вообще. В этом случае все параметры будут по умолчанию.
Mysql
Тут тоже ничего особо сложного
db:
image: "mysql:5.6"
volumes:
- ./docker/db:/var/lib/mysql
environment:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: local
MYSQL_USER: local
MYSQL_PASSWORD: local
networks:
- backend
Наследуемся от официального образа mysql. Тут я думаю что стоит указывать прямо версию БД, не факт что при следующем обновлении новый образ будет иметь такую же структуру БД что и предыдущая версия, поэтому тут тэг latest лучше не использовать. На момент написания статьи официальный образ содержит версию 5.7, но на ней у меня при развертке резервной копии битрикса возникли сложности. Я посмотрел какая версия стоит на боевом сервере, там где работает сайт с которого делалась резервная копия, оказалось что там версия 5.6. Я сменил одну циферку в docker-compose.yml, рестартовал окружение одной командой и через несколько секунд повторная развертка резервной копии прошла без проблем. Так сказать мгновенная демонстрация гибкости.
Чтобы не похерить БД после docker-compose down (это удаление всего окружения, всех контейнеров) пользуемся диском ./docker/db:/var/lib/mysql, то есть в папке localdev/docker/db у нас будет лежать вообще все что контейнер запишет в папку /var/lib/mysql у себя внутри. Папка /var/lib/mysql содержит весь контент базы данных если кто не в курсе. Даже после удаления контейнера благодаря этому маунту у нас внутри проекта останется содержимое /var/lib/mysql, которое потом можно будет примонтировать новому свежесозданному контейнеру.
Переменные окружения в блоке environment говорят что контейнеру надо сразу при старте создать базу, пользователя и пароль у пользователя. Плюс для рута поставить пароль root. Вы можете любые другие данные прописать.
Контейнер работает все так же во внутренней сети backend текущего окружения. При разворачивании резервной копии нужно указывать host mysql — «db», именно по адресу db:3306 будет работать cам mysql в контейнере после старта окружения. Хост db, база данных local, пользователь local, пароль пользователя local. Вы можете прописать любые другие параметры. Так как все контейнеры кроме nginx работают в сети backend, то по адресу db контейнер апача увидит именно вот этот конкретный контейнер mysql. Если вы запустите второе окружение параллельно и там будет сервис с таким же именем, никакого конфликта не случится. Тот параллельный апач будет видеть свой параллельный db.
PHPMyAdmin
phpmyadmin:
image: phpmyadmin/phpmyadmin:latest
environment:
- PMA_HOST=db
- PMA_USER=local
- PMA_PASSWORD=local
volumes:
- /sessions
networks:
- backend
Чтобы быстро смотреть БД проекта я ставлю локальный для проекта phpmyadmin. Тоже из официального образа phpmyadmin. Пусть он будет latest, ничего плохого в этом не вижу. В переменных окружения сразу конфигурируем контейнер, так чтобы он сразу при открытии подключался к нашему mysql серверу и не требовал пароль.
Строка «PMA_HOST=db» это адрес хоста mysql. Внутри сети backend все контейнеры видят друг друга по названиям. Сервис с базой данных в docker-compose.yml называется db, именно по этому имени его сможет найти любой другой контейнер. В данном случае контейнер phpmyadmin будет стучаться по адресу db:3306, то есть на 3306 порт, и сразу будет пробовать авторизоваться. Если параметры авторизации совпадают с тем, что мы в сервисе БД прописывали, то по адресу pma.example.com вы сразу увидите открытый phpmyadmin.
Создание front сети
Выше мы описали структуру контейнеров. Однако если вы попробуете запустить docker-compose up -d —build, то ничего не сработает и запуск отвалится с ошибкой. У нас прописана сеть front, которую мы пока что не создали. Но даже после создания этой сети у вас с windows хоста не будет доступа к конейнеру, так как мы порты не расшаривали.
Начнем с создания чистовой сети front. Именно в ней у нас будут работать все сайты. В ней не будет никакого хлама, скорее всего тут будет только один контейнер и соответственно один IP на каждый проект.
Откройте консоль, я работаю в windows powershell. В консоли выполняем следующее:
# для начала вообще посмотрим список сетей
$ docker network ls
NETWORK ID NAME DRIVER SCOPE
c20d95220ac6 bridge bridge local
6a2e37ce4a3e host host local
59cc4ca8e638 none null local
#в списке нет нужной нам сети front, создаем ее
$ docker network create front
eddbc417c7821764db0300083cd9072b81a20f5892fb65a91b4d13199f086d44
# и опять посмотрим список сетей
$ docker network ls
NETWORK ID NAME DRIVER SCOPE
c20d95220ac6 bridge bridge local
eddbc417c782 front bridge local
6a2e37ce4a3e host host local
59cc4ca8e638 none null local
Как мы видим в списке добавилась сеть front.
Если теперь выполнить
$ docker-compose up -d --build
В папке с проектом, у нас поднимется окружение.
Попробуем достучаться до контейнеров разными способами.
Доступ к контейнерам пробросом портов
Это самый простой способ добиться ответа от контейнеров. И это обычное первое чему учатся новички. Контейнер nginx если вдруг мы каким то образом сможем добраться до него слушает и отвечает по 80 порту. Но этот порт он слушает где-то в недрах виртуалки докера. Чтобы вытащить 80 порт контейнера с докером на хост можно прописать такое в docker-compose.yml
nginx:
image: nginx
volumes:
- ./docker/nginx/:/etc/nginx/conf.d/
networks:
- front
- backend
ports:
- "80:80"
Я добавил новую секцию ports. И теперь у нас на 127.0.0.1:80 висит nginx из compose файла. Если мы пойдем и подправим файл etc/hosts. В винде он лежит в «c:\Windows\System32\drivers\etc\», файл hosts без расширения редактировать блокнотом. Допишем туда в конец следующие строки:
127.0.0.1 my.example.com
127.0.0.1 pma.example.com
Теперь в браузере у нас по адресу my.example.com откроется апач, а по pma.example.com у нас откроется phpmyadmin.
В апаче папка html у нас пустая, так что там будет Forbidden. Если положим в папку localdev/html/ файл index.php
<?php
phpinfo();
То у нас откроется страница с выводом phpinfo.
Не обязательно пробрасывать именно 80 порт на хосте. Можно в секции ports прописать «8745:80» и тогда 8745 порт хоста будет смотреть на 80 порт nginx. Для контейнера nginx вообще ничего не изменится, но на виндовом хосте нам теперь нужно будет открывать не просто my.example.com в браузере, а тот же самый адрес но уже с портом «my.example.com:8745». При открытии этого адреса отобразится тот же самый вывод phpinfo.
Свободных портов много. И теоретически можно запускать сколько угодно сайтов по разным портам хоста одновременно. Но серьезно, кому нравится смотреть на my.example.com:8745? Уродство же 🙂 Я уж не говорю что все ссылки на сайте не всегда эти порты не учитывают, и по клику браузер может открыть сайт безо всякого порта. Этот вариант я показал просто для полноты картины. Реально для php разработки его использовать не стоит.
Доступ к контейнерам docker по IP
Это другой вариант добраться до 80 порта nginx в недрах виртуалки. Попробуем достучаться до контейнера безо всякого проброса портов, прям по его IP адресу.
Создание статического маршрута до сети
Попробуем найти под каким же ипшником у нас поднялся контейнер nginx. Посмотрим список контейнеров
#
$ docker ps
CONTAINER ID IMAGE PORTS NAMES
90a10937a1be mysql:5.7 3306/tcp localdev_db_1
6622d28d888c nginx 80/tcp localdev_nginx_1
c009597cdea6 localdev_apache 80/tcp localdev_apache_1
7d61cff1f450 phpmyadmin/phpmyadmin:latest 80/tcp localdev_phpmyadmin_1
В списке контейнеров должен быть контейнер nginx. Я тут удалил лишние несолько колонок, чтобы посимпатичнее вывод docker ps смотрелся, чтобы по ширине влезло без переносов. У нас контейнер nginx есть, он запущен с ID 6622d28d888c, и именем localdev_nginx_1. Посмотрим конфиг контейнера
$ docker inspect localdev_nginx_1
Там вылетит многостраничная текстовка, в данном случае нам интересна только вот эта часть
[...]
"Networks": {
"front": {
"IPAMConfig": null,
"Links": null,
"Aliases": [
"nginx",
"6622d28d888c"
],
"NetworkID": "164dd51d87d7e43e8ec85d5a0c95eb781ae57614f4f3a8c4d195bdf1d2662a60",
"EndpointID": "5654803ffb0db4e9bf98c1e43792eca60b0bd281f63cd537fc3fb55b24a6a833",
"Gateway": "172.19.0.1",
"IPAddress": "172.19.0.3",
"IPPrefixLen": 16,
"IPv6Gateway": "",
"GlobalIPv6Address": "",
"GlobalIPv6PrefixLen": 0,
"MacAddress": "02:42:ac:13:00:03",
"DriverOpts": null
},
"localdev_backend": {
"IPAMConfig": null,
"Links": null,
"Aliases": [
"nginx",
"6622d28d888c"
],
"NetworkID": "b4a21948ca183c4d285cfb48e7f125fa32ca8a6bfd7f7c41dcf9f9db637641f4",
"EndpointID": "f9943446b3741f138561aec45198f58f8ea9fa6ac3eee57ca02126f1b4ca6793",
"Gateway": "172.18.0.1",
"IPAddress": "172.18.0.5",
"IPPrefixLen": 16,
"IPv6Gateway": "",
"GlobalIPv6Address": "",
"GlobalIPv6PrefixLen": 0,
"MacAddress": "02:42:ac:12:00:05",
"DriverOpts": null
}
}
Как мы видим, контейнер работает сразу в двух сетях. В сети front IP контейнера 172.19.0.3, в сети localdev_backend IP контейнера 172.18.0.5. Если мы с хоста попробуем пропинговать IP контейнера в сети front, то есть выполнить в консоли ping 172.19.0.3, то мы нифига не увидим. Контейнер оказывается не доступен. Как сделать контейнер доступным по IP с хоста? Это на самом деле не тривиальная задача.
Чтобы связать внутреннюю сеть докера и хост мы должны создать новый статический маршрут. Для начала посмотрим список уже созданных маршрутов. В этом нам поможет команда route. Выполняем на виндовом хосте:
$ route print
[...]
===========================================================================
Постоянные маршруты:
Сетевой адрес Маска Адрес шлюза Метрика
172.0.0.0 255.0.0.0 10.0.75.2 1
===========================================================================
[...]
Вывод довольно здоровый, я оставил только значимую часть, там еще сверху и снизу от вырезанного куска по экрану текста. У вас скорее всего блок «Постоянные маршруты» будет пустой. У меня он уже создан, и поэтому команда route print его показывает.
Если у вас такого нет, то добавляем новый маршрут. Следующую команду нужно выполнять из под админа. То есть нужно запустить консоль с правами администратора и только потом выполнять
$ route /P add 172.0.0.0 MASK 255.0.0.0 10.0.75.2
ОК
Статический маршрут во внутреннюю сеть докера создан. Но это еще не все. Во всех новых версиях докера виртуалка с линуксом запрещает доступ по IP контейнера. Ранее это было возможно, но на текущий момент по дефолту запрещено правилами iptables. Официального метода не существует, разработчики докера считают что это никому не нужно / очень сложно реализовать. Для понимания проблемы почитайте вот это в официальной документации, раздел «Container communication between hosts».
Для фикса этого поведения нужно выполнить команду:
$ docker run --rm -it --privileged --network=none --pid=host justincormack/nsenter1 bin/sh -c "iptables -P FORWARD ACCEPT"
Вот. Теперь есть и статический маршрут до внутренней сети докера и правила на виртуалке разрешают такие подключения. Эта команда если вкратце изменяет правила iptables на виртуалке докера. Пробуем пропинговать контейнер по его IP из сети front, команда ping 172.19.0.3 теперь будет показывать корректный обмен пакетами.
Дополнительные пояснения по маршруту
Еще раз пройдемся по всем пунктам. Мы создали сеть front с драйвером bridge. Это все штатные команды докера, тут ничего особенного нет, все должно быть понятно. Подробнее разберем всю дальнейшую магию.
Контейнеры на винде стартуют на виртуалке. То есть докер создает виртуальную машину, и вообще все контейнеры запускаются именно на виртуалке. У виртуалки есть статический IP адрес, как правило этот адрес 10.0.75.2, вы можете безо всяких маршрутов его попробовать пропинговать. Этот адрес спокойно пингуется. Все сети которые создает докер создаются внутри виртуалки. IP контейнера nginx в сети front 172.19.0.3, это значит, что на докеровской виртуалке создана сеть в которой для контейнера nginx выделен определенный ip адрес. Во внешем мире об этом IP адресе никто не знает.
Когда мы создаем статический маршрут командой
$ route /P add 172.0.0.0 MASK 255.0.0.0 10.0.75.2
ОК
Мы говорим винде, что маршрут до всех IP адресов начинающихся с цифры 172 надо строить через IP адрес 10.0.75.2. Если пользователю вдруг нужен ипшник 172.19.0.3, то винда будет пытаться достучаться до него через 10.0.75.2, или другими словами будет искать этот адрес внутри виртуалки.
И последняя магическая деталь касательно сети. Команда
$ docker run --rm -it --privileged --network=none --pid=host justincormack/nsenter1 bin/sh -c "iptables -P FORWARD ACCEPT"
для изменения правил iptables. Докеровская виртуалка по дефолту всех посылает нахрен, говорит что нужного IP адреса у нее нет. И вот эта команда меняет дефолтное поведение виртуалки. После ее выполнения виртуалка будет корректно достраивать оставшуюся часть маршрута. Офигенный минус в том, что это временное решение. При переустановке докера или еще во многих других случаях, когда машина создается заново, докер забывает эту команду. То есть ее периодически придется выполнять заново. Статический маршрут на хостовой винде вы один раз забили и все, можно про это забыть. А вот это вот поведение забыть не получится. Если вдруг перестали проходить пинги до контейнеров, скорее всего докер забыл про эту команду и надо ее выполнить заново. Если после ее выполнения пинги так и не пошли, значит дело в чем то другом.
На текущий момент я не знаю как обойти это ограничение, возможно в будущем разработчики все же реализуют настройку iptables каким нибудь внешним конфигом, или я может еще найду какое то другое постоянное решение. Пока что я других способов достучаться до контейнеров по IP не знаю.
Собственный DNS на docker
Ну вот вроде бы счастье, контейнеры пингуются, а сайт так и не работает нифига. Для начала пойдем и подправим файл etc/hosts. Допишем туда в конец следующую строку, ну либо подправьте уже существующую запись, если вы с пробросом портов экспериментировали:
172.19.0.3 my.example.com
То есть укажем прямой IP контейнера nginx. Как узнать этот IP я писал выше. У вас цифры могут быть другие.
Теперь открываем в браузере адрес http://my.example.com/ и если все было сделано верно, вы увидите результат работы апача. Это очень круто, однако что будет после рестарта окружения? Контейнеры поднимутся заново и IP адреса всех контейнеров будут другими. Нам поможет dnsdock
Dnsdock это DNS сервер для докер. Если запустить его внутри сети front, то он будет прям на лету смотреть какие контейнеры запущены и выдавать IP адрес контейнера по запросу. По дефолту он возвращает IP по какому то сильно замороченному адресу завязанному на образ контейнера, окружение и т.д. Нам же для целей локальной разработки пригодится другая его фишка — поиск контейнера по метке (label). Обратите внимание на блок label у контейнера nginx
nginx:
image: nginx
volumes:
- ./docker/nginx/:/etc/nginx/conf.d/
networks:
- front
- backend
labels:
com.dnsdock.alias: "my.example.com,pma.example.com"
Метка com.dnsdock.alias говорит нашему DNS серверу, что этот контейнер можно найти по указанным через запятую адресам. То есть если запросить у dnsdock IP для my.example.com, то он вернет IP отмеченного контейнера. Так как nginx у нас сразу в двух сетях, то изредка dnsdock будет возвращать IP nginx в сети backend и сайт работать не будет. Это лечится полным рестартом окружения через docker-compose down. После рестарта он обычно возвращает IP из сети front и все начинает работать нормально.
Давайте поднимем собственно сам контейнер dnsdock, выполним в консоли винды следующую команду
$ docker run -d -v /var/run/docker.sock:/var/run/docker.sock --restart always --name dnsdock --net front -p 53:53/udp aacebedo/dnsdock:latest-amd64 --nameserver="77.88.8.8:53"
Команда достаточно длинная. Если кратко, то эта команда запускает контейнер dnsdock в сети front. Контейнер будет запускаться сразу при старте докера и будет самостоятельно рестартовать если вдруг по каким то причинам завершит свою работу. Контейнер будет отвечать на udp запросы на 53 порту (по этому порту работают все DNS) хоста, то есть он будет отвечать на все запросы по адресу 127.0.0.1:53. Кроме того, дополнительным параметром —nameserver мы говорим контейнеру, что в случае если он не нашел адреса внутри докера, то должен спросить указанный в параметре нормальный DNS сервер, в данном случае это IP адрес яндекс днс. Так же обращаю ваше внимание, что IP адрес внешнего ДНС указан вместе с портом, без порта не заработает.
Протестируем наш локальный DNS. Выполним в винде команду
$ nslookup my.example.com 127.0.0.1
╤хЁтхЁ: localhost
Address: 127.0.0.1
Не заслуживающий доверия ответ:
╚ь : my.example.com
Address: 172.19.0.3
Эта команда ищет указанный адрес у конкретного ДНС сервера. В данном случае она искала указанный домен на нашем локальном dnsdock. При этом если мы попробуем у этого же сервера спросить домен ya.ru, то dnsdock вернет нормальный внешний IP адрес яндекса.
Чтобы вообще все запросы шли к нашему локальному dnsdock и можно было открывать сайт в браузере, достаточно указать его в параметрах адаптера винды, в качестве DNS сервера по умолчанию. Если какой то адрес будет найден внутри докеровской виртуалки, то dnsdock переправит нас на докер, если ничего не будет найдено, то все отработает как обычно и у вас в браузере откроется полноценный интернет сайт. В качестве постоянного локального DNS я бы это решение использовать не стал, однако на время работы по программированию прописать конкретный dns не долго и потом так же быстро можно убрать. Ну еще как вариант вы можете на винде поставить acrylic dns proxy, это аналог линуксового dnsmasq, там можно настроить разные правила для работы с DNS серверами. Возможно еще какие то другие аналоги есть.
Доступ к контейнерам через nginx reverse proxy
Локальный DNS и доступ к контейнерам по их прямым IP это конечно замечательно. Но давайте посмотрим альтернативы. Предыдущий метод желательно знать, однако в целях реальной разработки я его так же как и проброс портов использовать не рекомендую. Предоставляя прямой доступ к контейнерам по IP мы смотрим на внутренности виртуалки. А что если пойти другим путем и попробовать повесить на входе reverse proxy, который бы по одному и тому же основному IP виртуалки выдавал нам разные сайты и сам разрешал все внутренние зависимости?
Для этого уже есть готовый образ jwilder/nginx-proxy. По функционалу он похож на dnsdock, в том плане, что так же слушает докер и на лету генерирует какие то правки. Если мы создали новый контейнер с определенной переменной окружения «VIRTUAL_HOST», то этот прокси тут же сгенерирует nginx конфиг специально для этого контейнера (собственный конфиг, для обслуживания того стороннего контейнера, сторонний контейнер об этом конфиге ничего не знает) и мы по основному IP адресу виртуалки докера сможем увидеть результат. Давайте по порядку. Для начала повесим прокси на входе командой
$ docker run -d --name proxy -v /var/run/docker.sock:/tmp/docker.sock:ro --net host --restart always jwilder/nginx-proxy
Можно даже без проброса портов, в этом случае контейнер будет отвечать на все запросы по IP адресу виртуалки и на 80 порту. То есть по 10.0.75.2:80. Этот контейнер благодаря restart always будет запускаться сразу при старте докера.
Теперь добавим переменную окружения VIRTUAL_HOST в наш контейнер с nginx, в файле docker-compose.yml
nginx:
image: nginx
volumes:
- ./docker/nginx/:/etc/nginx/conf.d/
networks:
- front
- backend
environment:
- VIRTUAL_HOST=my.example.com,pma.example.com
И запустим окружение через
$ docker-compose up -d --force-recreate
Последний шаг, в etc/hosts винды прописываем
10.0.75.2 my.example.com
10.0.75.2 pma.example.com
Пробуем открыть my.example.com в браузере. Должен открыться phpinfo из файла localdev/html/index.php. По pma.example.com как обычно должен открыться phpmyadmin.
Суть метода еще раз
Еще раз другими словами повторю суть этого метода. Докер в винде работает на обычной виртуалке. У этой виртуалки есть IP адрес, по дефолту этот адрес 10.0.75.2. Мы вешаем специальный докер контейнер с nginx по этому адресу, он начинает слушать 80 порт и уже от собственного лица спрашивает все остальные контейнеры внутри виртуалки. Какой из контейнеров будет спрашивать этот прокси от собственного лица определяется переменной окружения VIRTUAL_HOST. Прокси смотрит чего от него хотят, какой домен человеку нужен, далее смотрит вообще все контейнеры которые есть внутри виртуалки, если где-то там в недрах есть контейнер с нужным значением в VIRTUAL_HOST, то прокси от собственного лица спрашивает этот найденный контейнер абсолютно то же самое что спросили его, а результат собственного запроса возвращает наружу. Таким образом создается иллюзия, что сайт работает у самого прокси, хотя реально отработать там может десяток контейнеров где-то в глубине. Такой механизм и называется reverse proxy, или по русски обратный прокси.
Отладка php в docker через xdebug
Пример отладки я буду показывать на phpstorm, если у вас другая IDE, то я думаю вы легко инструкцию адаптируете. После того как вы соберете свое окружение, и оно даже у вас внезапно заработает, для отладки вам останется несколько простых шагов. В зависимости от выбранного метода взаимодействия с контейнерами докер, отладка у вас будет происходить по разному. Первый шаг для всех одинаковый — поставить расширение для браузера которое добавляет ко всем запросам нужный ключ сессии (по сути просто в $_COOKIE добавляется переменная), лично у меня стоит такое. Нужно настроить чтобы расширение для браузера ставило ключ PHPSTORM.
Прямая отладка
Если вы все же по каким то неведомым мне причинам решили настроить локальное окружение через проброс портов, то сделайте так, чтобы контейнер апача слушал 9000 порт. Если вы работаете с контейнерами по IP напрямую, то вам ничего дополнительно настраивать не нужно, разве что попробуйте пропинговать контейнер апача по его IP, он должен быть доступен для подключения, то есть пакеты при пинге должны идти без ошибок.
После этого в php.ini пропишите следующее
[xdebug]
xdebug.idekey = "PHPSTORM"
xdebug.remote_enable = 1
xdebug.remote_connect_back = 1
В самом phpstorm нужено будет просто нажать кнопку «Start listening for PHP debug connections» и открыть нужную страницу в браузере. У вас в IDE появится запрос на подключение.
При включенной в расширении браузера отладке, каждое открытие страницы сайта будет открывать отладочную сессию в phpstorm. Если вам больше не нужна отладка, то либо на стороне phpstorm отключите прослушивание, либо в браузерном расширении отключите отладку.
Отладка через proxy
Чуть сложнее дело обстоит с отладкой в том случае, когда сайты у вас доступны через reverse proxy. Напрямую по IP к контейнеру вы достучаться никак не можете. В этом случае нам поможет специальный proxy который работает по протоколу DBGP. Вот есть статья по работе с этим прокси в phpstorm. Вот готовый собранный образ для докера с этим контейнером. Есть вроде бы и какие то поменьше образы, но пусть будет этот. Можете собрать собственный образ proxy, он там ставится всего в несколько команд.
Этот отладочный proxy нужно запускать следующей командой на хосте:
$ docker run -d --name=dbgpproxy --restart always --net host christianbladescb/dbgpproxy
Команда запускает контейнер в сети host виртуалки, то есть этот контейнер будет видеть вообще все внутренние IP адреса на виртуалке, так что он сможет общаться с контейнером апача. Этот контейнер автоматически будет запускать при каждом запуске докера благодаря параметру restart always.
В php.ini для xdebug вам тоже будут нужны другие настройки
[xdebug]
xdebug.remote_enable = 1
xdebug.remote_host = 10.0.75.2
xdebug.remote_port = 9000
Мы тут убрали idekey и прописали жесткий IP адрес к которому php будет стучаться для отладки. IP 10.0.75.2 это адрес хоста, именно его мы в файле etc/hosts винды прописывали. Все контейнеры внутри виртуалки тоже по этому адресу могут общаться с хостом. То есть он виден как изнутри, так и снаружи виртуалки.
Далее опять таки включаем в браузерном расширении отладку, включаем в phpstorm прослушивание входящих отладочных соединений. Но кроме того нам понадобится зарегистрировать phpstorm на нашем dbgp proxy. Для этого в главном меню выберите Tools -> DBGp proxy -> Register IDE. Если url отладочного прокси не указан, то возможно еще phpstorm попросит вас его указать, если вы ранее уже прописывали адрес прокси, то тут же мгновенно пройдет регистрация. В качестве УРЛ отладочного сервера можете прописать 10.0.75.2, порт 9001.
Известные баги под windows
В целом для разработки на винде докер вполне можно использовать. Однако же на винде он не такой стабильный как хотелось бы и скорее всего придется смириться с некоторыми багами. В этом разделе я расскажу про те, с которыми я столкнулся. Если вы знаете как это исправить, пишете в комментариях
Запуск контейнеров после перезагрузки
После рестарта винды контейнеры не всегда стартуют правильно. Проблемы возникают, когда в контейнер смонтирована какая-то папка с хоста. Довольно часто возникают проблемы типа permission denied. Может помочь
1. Отключение быстрого запуска в параметрах электропитания
Если перезагрузка была без выключения докера, то винда может запомнить состояние процесса и попытаться восстановить его после перезагрузки, чтобы перезагрузка происходила быстрее. Но при этом состояние докера восстанавливается некорректно. Можно попробовать отключить вот это запоминание состояния
2. Зайти в настройки докера, в раздел доступа к дискам и нажать «Применить», чтобы обновить состояние доступа. И после этого выполнить
$ docker-compose down
$ docker-compose up -d
То есть после обновления настроек рестартовать сборку.
Подключение новых программистов
Во вступлении я говорил, что эта статья не для программистов, а больше для devops. И действительно обычному программисту незачем собирать свое окружение. Однако же объяснить программисту порядок развертки окружения на докер не так и сложно. Если вы собрали собственное окружение под проект, то новому программисту для развертки достаточно три шага
- Один раз для всех проектов поставить докер
- Клонировать репозиторий проекта через git clone
- Выполнить в консоли docker-compose up -d —build
Возможно понадобится еще и четвертый шаг, развернуть базу данных сайта + докачать файлы которые не хранятся под гитом. Например для битрикса достаточно скачать резервную копию сайта и развернуть ее в localdev/html.
После этих трех-четырех шагов, новый программист уже видит работающий сайт. И ему не нужно особо задумываться как оно там устроено внутри и почему собственно работает. Он уже может писать код и отправлять коммиты.
Итого
Возможно статью я еще немного дополню, есть дополнительные примеры. Однако основное что я хотел рассказать, в статье уже есть. Мы посмотрели пример настройки конфигурации для compose, где я обозначил несколько вариантов контейнеров — контейнеры собирающиеся из готовых образов с настройкой через переменные окружения типа phpmyadmin, контейнеры создающиеся билдом на примере апача с модулем php, контейнеры с конфигурацией через маунт (через volume), где конфиги подтягиваются с хоста, контейнеры для хранения данных на примере mysql.
Мы посмотрели три основных метода взаимодействия с контейнерами для разработки сайтов, это проброс портов, доступ к контейнерам по IP и работа с контейнерами через nginx reverse proxy. Лично я рекомендую пользоваться именно reverse proxy. Этот вариант мне кажется самым красивым, стабильным, удобным, быстрым и производительным.
Надеюсь статья оказалась вам полезна. Если это действительно так, то скажите спасибо в комментариях и тогда я возможно буду что-нибудь писать чаще чем раз в год 😀
Спасибо! 😀
Всегда пожалуйста 🙂 Уже не зря писал)
Спасибо! 🙂 А как прикрутить к докеру почтовый сервер и настроить его в нем? Допустим, на сайте, который делается в докере, есть регистрация с подтверждением по почте. Что делать, чтоб проверить работу этого скрипта?? Нигде нет простой и понятной инструкции по этому поводу 🙁
Прошу прощения за «быстрый» ответ 🙂 Кратко некрасиво было бы ответить, а на длинный ответ все времени не было. Меня на самом деле уже спрашивали про почту, наверное стоит отдельный раздел прям в статье под это выделить, может напишу как время будет.
Почта в php отправляется через sendmail. Дефолтный официальный образ идет без него. И тут вариантов то на самом деле не много, всего два. Либо через smtp прямо в коде отправлять почту, либо все же ставить MTA внутрь контейнера:
1. Для отправки почты напрямую из php есть дофига всяких готовых решений, типа phpmailer, swiftmailer, zend-mail и т.д. Поставить через композер и ничего в контейнере не нужно настраивать.
2. В качестве MTA можно поставить все тот же sendmail, для этого в dockerfile надо написать собственно команды по его установке. И во время билда образа все подтянется и поставится. Установка в контейнер ничем не отличается от установки на голую ОС. Посмотрите какая операционка внутри контейнера используется и поищите мануал по установке для этой операционки. Примерно так ставится
# apt-get install sendmail
Плюс конфиги.
И дальше прописать в php.ini параметр
sendmail_path = /usr/sbin/sendmail -t -i
То есть путь до бинарника sendmail. Как то так.
В качестве альтернативы sendmail можно еще поставить ssmtp. Это тоже MTA, только полегче чем sendmail. Вот так можно его внутрь докера поставить
https://binfalse.de/2016/11/25/mail-support-for-docker-s-php-fpm/
Мануалов по установке ssmtp довольно много, тоже легко ищется. В php.ini соответственно путь до бинарника ssmtp надо будет прописать, вместо пути до бинарника sendmail. Мне кажется именно для докера ssmtp предпочтительнее.
Сам MTA (mail transfer agent) можно настроить опять таки на отправку через SMTP яндекса или гугла (чтобы не пароль от всего аккаунта прописывать, у них есть специальная штука «пароли приложений», с ограниченным доступом только на работу с почтой например, чтобы не сильно волноваться по поводу хранения пароля в конфиге). Либо еще можно отдельным контейнером поднять exim и настроить MTA на работу с ним. Там дальше разберетесь я думаю 🙂
* * *
В общем контейнер это можно считать отдельная виртуалка. Там никаких прям особенностей заметных нет. В большинстве случаев не нужно искать информацию именно по докеру, достаточно найти инструкцию по установке/настройке на голой ОС и прописать это все в dockerfile.
Спасибо! за статью почти единственая стоящая что я нашел. только чет я никак не могу настроить phpmyadmin.conf иза этого не работает pma.example.com. не мог ли ты дать настройку или скинуть архив от локальной папки с настройками.
Там конфиг один в один такой же как и для основного сайта можно использовать. Отличий не много, во первых субдомен
server_name pma.*;
и во вторых
proxy_pass не apache:80, а phpmyadmin:80
Добрый день. Благодарю за столь полезное и подробное руководство!
А вы можете ваш труд выложить где-нибудь в git, чтобы можно было на лету слить с git и быстро поднять такое же окружение?
Думал об этом, но решил не выкладывать. Я не считаю что мое окружение какое-то эталонное и его стоит всем копировать. В статье я больше рассказывал о принципах сборки, чтобы каждый мог собрать то что ему нужно и понимал что именно он делает. У меня все конечно работает, и мне все нравится, но брать то что я сделал и не задумываясь запускать, мне кажется неправильным.
Если вас интересует именно готовая сборка, то посмотрите например на
https://phpdocker.io/generator
Кто-то сделал специальный сайт для генерации готового окружения на docker-compose, даже сделал свои образы. Возможно вам хорошо зайдет именно такой формат. У меня заработало, но в целом вариант мне не очень понравился. Я хочу понимать чего вообще происходит и как оно все работает 🙂 Просто на посмотреть — годный вариант, можно потыкать галочки и глянуть что там получится, а потом по аналогии что-то подобное самому сделать
Здесь прямо дискуссия развернулась по поводу необходимости доступа к контейнерам по ip. https://github.com/docker/for-win/issues/221
Упрямый индус из команды разработчиков утверждает, что такой необходимости нет и это все блажь. Я с ним согласен, доступ к контейнеру по доменному имени, забитому в /etc/hosts — это никому не нужная фигня.
По поводу временного способа docker run —rm -ti —privileged —network=none —pid=host justincormack/nsenter1 bin/sh -c «iptables -A FORWARD -j ACCEPT»
В комментариях предлагается еще один вариант, прописать в настройках демона «iptables»:false. В этом случае, файрвол виртуалки отрубается и она перестает отбивать запросы по ip. Но там же в комментариях люди говорят, что могут быть побочные эффекты (какие именно — не очень понятно).
А вообще, огромное спасибо за статью, я уже думал, что не найду решения этой задачи. На линуксе, для сравнения, все делается без лишних телодвижений и работает из коробки.
Да, я доступ к контейнерам по IP описывал только потому что на линуксе это штатно работает. Чтобы было понимание о том, что такой способ вообще есть.
По ссылке читал дискуссию еще до написания статьи, думал на нее тоже ссылку поставить, но в официальной доке более короткое пояснение про iptables нашел, решил на официальный сайт лучше сослаться. Отключение iptables, если верить дискуссии по ссылке, вызывает такое поведение:
— Yes I can now ping 172.x from windows machine
— docker is not publishing ports (on windows machine)
— containers can not reach windows dev machine and external hosts
То есть после отключения контейнеры с хоста стали пинговаться нормально. Касательно портов я тоже не понял что ломается, можно еще потестировать. А вот третья строка означает, что изнутри виртуалки возникают проблемы с доступом ко внешним ресурсам. Если отключить iptables и потом зайти внутрь контейнера
$ docker exec -it container_name /bin/bash
И внутри контейнера попытаться ya.ru по IP пропинговать, увидим что пинги не идут
root@3aa75a955d85:/var/www/www# ping 87.250.250.242
PING 87.250.250.242 (87.250.250.242): 56 data bytes
^C— 87.250.250.242 ping statistics —
390 packets transmitted, 0 packets received, 100% packet loss
То есть доступ во внешний интернет пропадает. При этом контейнеры в той же сети нормально пингуются, ping phpmyadmin будет показывать обмен пакетами. Если обратно включить iptables, то до яндекса пинг начинает проходить
root@3aa75a955d85:/var/www/www# ping 87.250.250.242
PING 87.250.250.242 (87.250.250.242): 56 data bytes
64 bytes from 87.250.250.242: icmp_seq=0 ttl=37 time=0.581 ms
64 bytes from 87.250.250.242: icmp_seq=1 ttl=37 time=0.799 ms
В принципе отключение iptables на винде можеть быть и рабочий вариант. Если доступ во внешний интернет совсем не нужен. Я пока остановился на варианте с reverse proxy, все нормально запускается с дефолтными настройками демона 🙂 Если будет хороший способ работы с контейнерами по IP, то может перейду на него
Дополнение касательно маршрута. Согласно https://www.arin.net/knowledge/address_filters.html, только часть адресов из пула 172 резервируются под частные сети.
Так что там должен быть маршрут такого вида: route /P add 172.16.0.0 MASK 255.240.0.0 10.0.75.2
Иначе возникают проблемы с доступом к реальным сайтам в интернете.
Дебагер не заработал, пробовал в UBUNTU 17
Ну по такому описанию я ничего не подскажу 🙂
Используется dbgproxy или напрямую с контейнером идет работа? Какая IDE? Что в php.ini касательно xdebug написано? Какие ошибки показываются?
Привет, спасибо за статью. Благодаря ей, достаточно быстро преодолел порог вхождения в docker. В знак благодарности, хочу дополнить статью одним полезным свойством, для тех кто работает на mac и использует «Docker for Mac». Я столкнулся с очень медленной работой файловой системы, смонтируемой в volumes. Оказывается проблема актуальна и решается с помощью достаточно новой фичи «caching options» подробнее на оф. сайте https://docs.docker.com/compose/compose-file/#caching-options-for-volume-mounts-docker-for-mac.
Вкратце проблема производительности решается очень просто, нужно в docker-compose.yml в свойстве volumes , где монтируется директория с проектом, дописать cached, например вот так:
volumes:
— ./web:/var/www/html:cached
К слову связка apache2 + php5 + mysql 5.5 начала летать не хуже MAMP PRO (а может даже чуть быстрее).
Есть еще вопрос. Опираясь на конфиг nginx из данной статьи вот в такой конфигурации ports: — «8888:80 (больше ничего не меняя), url проекта открывается, но почему то все заголовки для получения файлов просят 80 порт. Грубо говоря, php отрабатывает, а вот вся статика (картинки, js, css итд) не загружаются.
Признаюсь, пока особо сильно не копал в эту сторону, но может уже кто-то сталкивался?
networks:
front:
external:
name: front
backend:
Мне кажется или здесь что-то не дописано?
backend это просто именованная сеть. Чтобы не folder_name_default сеть создавалась, а с заданным именем. Именовать сеть не обязательно, я это сделал чтобы явно подчеркнуть взаимосвязь контейнеров, чтобы выделить кто кого видит
Подскажите как исправить?
ERROR: The Compose file ‘.\docker-compose.yml’ is invalid because:
Unsupported config option for services.networks: ‘front’
Docker version 17.12.0-ce, build c97c6d6
docker-compose version 1.18.0, build 8dd22a96
Так, ладно. С этим разобрался.
Нужно было вынести раздел networks выше services.
Теперь другая проблема:
ERROR: Service ‘apache’ failed to build: Get https://registry-1.docker.io/v2/: net/http: request canceled while waiting for connection (Client.Timeout exceeded while awaiting headers)
Это похоже баг самого докера. Попробуй для теста выполнить в консоли
$ docker login
Это авторизация на докерхабе. Если будет такая же ошибка после ввода пароля, то значит связи с серверами докера нет. Много багрепортов на гитхабе по этому поводу. У меня такая же ошибка последний месяц была. Помогло обновление до edge версии докера
Столкнулся с такой же проблемой, сделал рестарт докера и проблема ушла. А сама ошибка появилась после поднятия сети front.
Помогла смена днс в настройках сети на гугловские 8.8.8.8 и 8.8.4.4
Спасибо. Понравилась статья. Одна из немногих которая подробно показывает как начать использовать докер в разработке. Давно искал что-то подобное =) Буду ждать других статей на тему докера.
Спасибо! Самая крутая статья по теме из всех что удалось найти в интернетах
$ docker-compose up build
build path D:\proj\docker\localdev\docker\apache either does not exist, is not accessible, or is not a valid URL.
Чел ты крут! Очень подробное обьяснение. Это похоже наследственное у битрикс разработчиков 🙂 У ЧСВешных прогеров такое не встретишь.
Ждешь баттлфилд 5?
Спасибо за подробную статью! И не только а эту. Есть у Вас талант подавать материал :).
По теме статьи
Помогите пожалуйста понять где и что надо поправить.
При билде такие ошибки:
running: find «/tmp/pear/temp/pear-build-defaultuserqmlWrL/install-xdebug-2.7.2» | xargs ls -dils
199950 4 drwxr-xr-x 3 root root 4096 Aug 10 16:55 /tmp/pear/temp/pear-build-defaultuserqmlWrL/install-xdebug-2.7.2
203902 4 drwxr-xr-x 3 root root 4096 Aug 10 16:55 /tmp/pear/temp/pear-build-defaultuserqmlWrL/install-xdebug-2.7.2/usr
202166 4 drwxr-xr-x 3 root root 4096 Aug 10 16:55 /tmp/pear/temp/pear-build-defaultuserqmlWrL/install-xdebug-2.7.2/usr/local
202931 4 drwxr-xr-x 3 root root 4096 Aug 10 16:55 /tmp/pear/temp/pear-build-defaultuserqmlWrL/install-xdebug-2.7.2/usr/local/lib
202167 4 drwxr-xr-x 3 root root 4096 Aug 10 16:55 /tmp/pear/temp/pear-build-defaultuserqmlWrL/install-xdebug-2.7.2/usr/local/lib/php
202932 4 drwxr-xr-x 3 root root 4096 Aug 10 16:55 /tmp/pear/temp/pear-build-defaultuserqmlWrL/install-xdebug-2.7.2/usr/local/lib/php/extensions
200974 4 drwxr-xr-x 2 root root 4096 Aug 10 16:55 /tmp/pear/temp/pear-build-defaultuserqmlWrL/install-xdebug-2.7.2/usr/local/lib/php/extensions/no-debug-non-zts-20180731
3035456 1780 -rwxr-xr-x 1 root root 1821328 Aug 10 16:55 /tmp/pear/temp/pear-build-defaultuserqmlWrL/install-xdebug-2.7.2/usr/local/lib/php/extensions/no-debug-non-zts-20180731/xdebug.so
Build process completed successfully
Installing ‘/usr/local/lib/php/extensions/no-debug-non-zts-20180731/xdebug.so’
install ok: channel://pecl.php.net/xdebug-2.7.2
configuration option «php_ini» is not set to php.ini location
You should add «zend_extension=/usr/local/lib/php/extensions/no-debug-non-zts-20180731/xdebug.so» to php.ini
error: /usr/src/php/ext/mcrypt does not exist
usage: /usr/local/bin/docker-php-ext-install [-jN] ext-name [ext-name …]
ie: /usr/local/bin/docker-php-ext-install gd mysqli
/usr/local/bin/docker-php-ext-install pdo pdo_mysql
/usr/local/bin/docker-php-ext-install -j5 gd mbstring mysqli pdo pdo_mysql shmop
if custom ./configure arguments are necessary, see docker-php-ext-configure
Possible values for ext-name:
bcmath bz2 calendar ctype curl dba dom enchant exif fileinfo filter ftp gd gettext gmp hash iconv imap interbase intl json ldap mbstring mysqli oci8 odbc opcache pcntl pdo pdo_dblib pdo_firebird pdo_mysql pdo_oci pdo_odbc pdo_pgsql pdo_sqlite pgsql phar posix pspell readline recode reflection session shmop simplexml snmp soap sockets sodium spl standard sysvmsg sysvsem sysvshm tidy tokenizer wddx xml xmlreader xmlrpc xmlwriter xsl zend_test zip
Some of the above modules are already compiled into PHP; please check
the output of «php -i» to see which modules are already loaded.
ERROR: Service ‘apache’ failed to build: The command ‘/bin/sh -c apt-get update && apt-get install -y libfreetype6-dev libjpeg62-turbo-dev libmcrypt-dev libpng-dev && pecl install xdebug && docker-php-ext-install -j$(nproc) mbstring pdo_mysql tokenizer mcrypt iconv mysqli && docker-php-ext-configure gd —with-freetype-dir=/usr/include/ —with-jpeg-dir=/usr/include/ && docker-php-ext-install -j$(nproc) gd && docker-php-ext-enable xdebug’ returned a non-zero code: 1
PS C:\localdev>
Какую версию php пробуешь собирать? Если пятую, то попробуй седьмую. И скинь полный dockerfile куда нить на pastebin
Аналогичная проблема
Это из-за попытки установить «mcrypt».
Удали его из списка на установку.
Добавь в конец :
&& pecl install mcrypt-1.0.2 \
&& docker-php-ext-enable mcrypt
Готовый ./apache/Dockerfile :
FROM php:7.2-apache
# PHP extensions
RUN apt-get update && apt-get install -y \
libfreetype6-dev \
libjpeg62-turbo-dev \
libmcrypt-dev \
libpng-dev \
&& pecl install xdebug \
&& docker-php-ext-install -j$(nproc) mbstring pdo_mysql tokenizer iconv mysqli \
&& docker-php-ext-configure gd —with-freetype-dir=/usr/include/ —with-jpeg-dir=/usr/include/ \
&& docker-php-ext-install -j$(nproc) gd \
&& docker-php-ext-enable xdebug \
&& pecl install mcrypt-1.0.2 \
&& docker-php-ext-enable mcrypt
# Apache modules
RUN a2enmod rewrite
Добрый день! Пробую собрать php 7
docker-compose.yml
version: «3»
services:
nginx:
image: nginx
volumes:
— ./doker/nginx/:/etc/nginx/conf.d/
networks:
— front
— backend
apache:
build: ./docker/apache
volumes:
— ./:/var/www/
— ./docker/apache/php.ini:/usr/local/etc/php/php.ini
networks:
— backend
db:
image: «mysql:5.7»
volumes:
— ./docker/db:/var/lib/mysql
environment:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: local
MYSQL_USER: local
MYSQL_PASSWORD: local
networks:
— backend
phpmyadmin:
image: phpmyadmin/phpmyadmin:latest
environment:
— PMA_HOST=db
— PMA_USER=local
— PMA_PASSWORD=local
volumes:
— /sessions
networks:
— backend
networks:
front:
external:
name: front
backend:
Dockerfile
FROM php:7-apache
# PHP extensions
RUN apt-get update && apt-get install -y \
libfreetype6-dev \
libjpeg62-turbo-dev \
libmcrypt-dev \
libpng-dev \
&& pecl install xdebug \
&& docker-php-ext-install -j$(nproc) mbstring pdo_mysql tokenizer mcrypt iconv mysqli \
&& docker-php-ext-configure gd —with-freetype-dir=/usr/include/ —with-jpeg-dir=/usr/include/ \
&& docker-php-ext-install -j$(nproc) gd \
&& docker-php-ext-enable xdebug
# Apache modules
RUN a2enmod rewrite
ERROR: Service ‘apache’ failed to build ?
Спасибо за статью!
Начинаю изучать докер, хотелось установить контейнер с чем-то знакомым (например с php + phpextensions + mysql + apache), поэтому она мне очень полезна.
Но в целом… Раз уж вам в windows необходимо сначала поставить виртуалку с linux, почему бы просто не установить потом на этой виртуалке xampp? Там есть всё, что вам надо.
Нет в xampp не всё что мне надо. Разные версии PHP, разный набор расширений, разные базы данных (реально нужны postgres, mysql, redis), разные дополнительные контейнеры, например тот же rabbitmq. В общем xampp совсем не то же самое.
А так, докер и правда не всем нужен. Про xampp цитата из самого начала статьи:
Добрый день, с помощью этой статьи завёл сайт + pma в докере. Проблем не возникло.
Но теперь не совсем понимаю как рядом поднять ещё один проект (например другой сайт с другой версией mysql и php).
Если собирать docker-compose.yml с nginx, то при старте двух проектов второй ругается, что nginx в первом уже использует порт.
Понятно, что тут надо как-то через проксирование это делать, но непонятно как. Как вы решали такую проблему?
Или в таком случае надо вообще исключать nginx из сборок и запускать его отдельно?
Заметная часть статьи посвящена этой проблеме. Ради этого собственно статья и писалась. Попробуйте оставшуюся часть дочитать. Я понимаю что информации дофига, но тем не менее. В статье обозначено три варианта доступа к контейнерам — проброс портов, реверс прокси и доступ через ДНС.
Проброс портов это самый простой и наименее функциональный вариант. Пробросом портов мы занимаем 80 или какой там указан порт на хостовой машине. После проброса по адресу localhost:80 на хосте будет открываться 80 порт контейнера. И соответственно только один контейнер может этот порт на хостовой машине занимать.
Далее в статье приводятся два варианта — реверс прокси и динамический днс.
С реверс прокси на 80 порту хоста ставится прокси, который уже дальше от своего лица опрашивает другие контейнеры. Он видит запрос, спрашивает то же самое у конкретного контейнера в зависимости от домена который его спросили и возвращает наружу ответ.
Динамический ДНС это практически дефолтный вариант на линуксах. Там докер работает не внутри виртуалки, а прямо на хосте. И прямо с хоста можно пинговать контейнеры по их уникальным ИП адресам. И остается взять только контейнер с ДНС который будет через апи докера знать ИП адреса контейнеров и подставлять этот ИП адрес в ответ на запрос по какому либо домену. То есть нужен нам домен test.com, этот спец контейнер с ДНС знает благодаря меткам у контейнеров, что для test.com он может вернуть не внешний какой то ипшник, а локальный из докера. И возвращает ип адрес контейнера. Далее браузер идет уже по выданному ип адресу на 80 порт как на обычный сайт.
Весь гемор с ДНС встречается только на винде и маке. Там докер не на хосте, в внутри виртуалки работает. И просто так пропинговать контейнера нельзя. Надо сначала докер вывернуть наружу. Об этом собственно и статья. В итоге я для винды рекомендую использовать реверс прокси. Я несколько лет уже на линуксе без перезагрузок под винду, там я использую третий способ с локальным ДНС.
Понял, спасибо.
Я, честно говоря, вообще из статьи не сразу понял зачем рассматриваются остальные варианты, если срабатывает проброс портов. Теперь картинка собралась! Спасибо, буду пробовать сегодня на винде, а через пару дней на centOS.
Оказывается в новых версиях Docker Desktop создатели убрали возможность использовать DockerNAT и соответственно IP-адрес 10.0.75.2 (в некоторых источниках пишут про 10.0.75.1) более не работает. Это породило большую волну обсуждения (например тут https://github.com/docker/for-win/issues/5538).
Но к сожалению из этого обсуждения я не смог вынести нужной мне информации. Я понял только, что у меня есть два варианта:
1) откатывать на более раннюю версию
2) использовать проброс портов с указанием свободного IP адреса типа:
ports:
— 127.55.0.1:80:8000
И не забывать прописать в hosts
127.55.0.1 example.org
Соответственно постоянно писать в хостс новые записи для новых проектов и следить, чтобы адреса не пересекались с предыдущими.
Может быть вы подскажете ещё какое-то решение?
Спасибо за комментарий. Я не в курсе изменений под винду, не отслеживаю на текущий момент.
Так на вскидку тоже ничего не скажу. Надо в тему углубляться. Но буду знать, что под винду что-то кардинально изменилось. Вот уж чего не ожидал, так это ухудшения функционала. Я скорее ожидал того, что контейнеры начнут пинговаться прям сразу после установки докера, без танцев с бубном. Там же всякие WSL (Windows Subsystem Linux) делаются. Были надежды, что оно всё будет из коробки красиво работать.
Попробовал сделать на CentOS. Вроде всё поднялось, но не могу достучаться до сайта извне (из сети интернет).
Т.е. локально с сервера curl показывает, что сайт работает, а с моего компа я достучаться не могу, хотя у себя в хостс я прописал ip-адрес сервера — домен.
Но ни по домену, ни по ip не дохожу.
На сервере пока отключил iptables, т.е. это мешать не должно. Что же мешает? Может что-то надо донастроить в docker-compose.yml?
Разобрался, сделал через проксирующий nginx. Спасибо