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

Docker для локальной php разработки

добавил Шубин Александр 12 Июль, 2017


В этой статье я расскажу несколько возможных вариантов настройки локального окружения для 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.
И наконец в четвертых, все это должно быстро подниматься для новых разработчиков которые подключаются к проекту.

Поехали. Попробуем это все реализовать 🙂

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. И действительно обычному программисту незачем собирать свое окружение. Однако же объяснить программисту порядок развертки окружения на докер не так и сложно. Если вы собрали собственное окружение под проект, то новому программисту для развертки достаточно три шага

  1. Один раз для всех проектов поставить докер
  2. Клонировать репозиторий проекта через git clone
  3. Выполнить в консоли docker-compose up -d —build

Возможно понадобится еще и четвертый шаг, развернуть базу данных сайта + докачать файлы которые не хранятся под гитом. Например для битрикса достаточно скачать резервную копию сайта и развернуть ее в localdev/html.

После этих трех-четырех шагов, новый программист уже видит работающий сайт. И ему не нужно особо задумываться как оно там устроено внутри и почему собственно работает. Он уже может писать код и отправлять коммиты.

Итого

Возможно статью я еще немного дополню, есть дополнительные примеры. Однако основное что я хотел рассказать, в статье уже есть. Мы посмотрели пример настройки конфигурации для compose, где я обозначил несколько вариантов контейнеров — контейнеры собирающиеся из готовых образов с настройкой через переменные окружения типа phpmyadmin, контейнеры создающиеся билдом на примере апача с модулем php, контейнеры с конфигурацией через маунт (через volume), где конфиги подтягиваются с хоста, контейнеры для хранения данных на примере mysql.

Мы посмотрели три основных метода взаимодействия с контейнерами для разработки сайтов, это проброс портов, доступ к контейнерам по IP и работа с контейнерами через nginx reverse proxy. Лично я рекомендую пользоваться именно reverse proxy. Этот вариант мне кажется самым красивым, стабильным, удобным, быстрым и производительным.

Надеюсь статья оказалась вам полезна. Если это действительно так, то скажите спасибо в комментариях и тогда я возможно буду что-нибудь писать чаще чем раз в год 😀


добавил Шубин Александр 12 Июль, 2017
Рубрика: devops


40 комментариев

  • Jawdrop Express:

    Спасибо! 😀

  • exio:

    Спасибо! 🙂 А как прикрутить к докеру почтовый сервер и настроить его в нем? Допустим, на сайте, который делается в докере, есть регистрация с подтверждением по почте. Что делать, чтоб проверить работу этого скрипта?? Нигде нет простой и понятной инструкции по этому поводу 🙁

    • Прошу прощения за «быстрый» ответ 🙂 Кратко некрасиво было бы ответить, а на длинный ответ все времени не было. Меня на самом деле уже спрашивали про почту, наверное стоит отдельный раздел прям в статье под это выделить, может напишу как время будет.

      Почта в 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.

  • bad4iz:

    Спасибо! за статью почти единственая стоящая что я нашел. только чет я никак не могу настроить phpmyadmin.conf иза этого не работает pma.example.com. не мог ли ты дать настройку или скинуть архив от локальной папки с настройками.

    • Там конфиг один в один такой же как и для основного сайта можно использовать. Отличий не много, во первых субдомен
      server_name pma.*;
      и во вторых
      proxy_pass не apache:80, а phpmyadmin:80

      server {
          listen       80;
          server_name  pma.*;
      
          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;
      			expires 6h;
      		}
      		location / {
      			try_files /does_not_exists @fallback;
      		}
      	}
      	location @fallback {
      		proxy_pass http://phpmyadmin:80;
              proxy_redirect http://phpmyadmin: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;
      
          proxy_connect_timeout       60;
          proxy_send_timeout          60;
          proxy_read_timeout          60;
          send_timeout                60;
      }
  • Иван:

    Добрый день. Благодарю за столь полезное и подробное руководство!
    А вы можете ваш труд выложить где-нибудь в 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
        Иначе возникают проблемы с доступом к реальным сайтам в интернете.

  • RUdi:

    Дебагер не заработал, пробовал в 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 итд) не загружаются.
    Признаюсь, пока особо сильно не копал в эту сторону, но может уже кто-то сталкивался?

  • Zudwa:

    networks:
    front:
    external:
    name: front
    backend:

    Мне кажется или здесь что-то не дописано?

    • backend это просто именованная сеть. Чтобы не folder_name_default сеть создавалась, а с заданным именем. Именовать сеть не обязательно, я это сделал чтобы явно подчеркнуть взаимосвязь контейнеров, чтобы выделить кто кого видит

  • Zudwa:

    Подскажите как исправить?
    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

    • Zudwa:

      Так, ладно. С этим разобрался.
      Нужно было вынести раздел 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

  • Вячеслав:

    Спасибо. Понравилась статья. Одна из немногих которая подробно показывает как начать использовать докер в разработке. Давно искал что-то подобное =) Буду ждать других статей на тему докера.

  • Алексей:

    Спасибо! Самая крутая статья по теме из всех что удалось найти в интернетах

  • Roman:

    $ 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?

  • egolege:

    Спасибо за подробную статью! И не только а эту. Есть у Вас талант подавать материал :).
    По теме статьи
    Помогите пожалуйста понять где и что надо поправить.
    При билде такие ошибки:

    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

    • Вадим:

      Аналогичная проблема

      • Grizzlecaller:

        Это из-за попытки установить «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

  • egolege:

    Добрый день! Пробую собрать 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

  • Спасибо за статью!
    Начинаю изучать докер, хотелось установить контейнер с чем-то знакомым (например с php + phpextensions + mysql + apache), поэтому она мне очень полезна.

    Но в целом… Раз уж вам в windows необходимо сначала поставить виртуалку с linux, почему бы просто не установить потом на этой виртуалке xampp? Там есть всё, что вам надо.

    • Нет в xampp не всё что мне надо. Разные версии PHP, разный набор расширений, разные базы данных (реально нужны postgres, mysql, redis), разные дополнительные контейнеры, например тот же rabbitmq. В общем xampp совсем не то же самое.

      А так, докер и правда не всем нужен. Про xampp цитата из самого начала статьи:

      Кроме того желательно понимать зачем вам вообще локальное окружение именно на докер. Для большинства людей хватит виртуалок, обычного удаленного хостинга или любых других уже готовых сборок типа denver, xampp, open-server и т.д. Их достаточно установить обычным инсталлером винды и у вас будут работать локальные сайты. Для разработки большинства проектов ничего больше и не нужно. Пробовать организовать локальное окружение на докере стоит только если вы понимаете зачем это делаете, например вы хотите построить процесс continous integration, или вам нужны программы определенных версий, ну либо если вам нравится сам докер и принципы его работы, как по мне тоже существенная причина

  • Алексей:

    Добрый день, с помощью этой статьи завёл сайт + 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. Спасибо

Добавить комментарий

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