Сегодня мы продолжим знакомится с контейнерами и поговорим о том, как сделать контейнер для Web приложения. Web сайты и сервисы – это как раз та сфера, где контейнеры способны показать всю свою мощь.
На первом уроке Контейнеры docker – проще некуда мы поговорили немного о теории, посмотрели, как завернуть в контейнер простое Hallo World приложение и прошли полный путь от кода, через образы к контейнеру. Если ты не знаком с основами и не слышал о базовых вещах, то очень рекомендую сделать паузу, перекусить Твикс и прочитать сначала первый урок.
Сегодня теорию будем изучать по мере надобности, а больше делать упор на практику.
Для Web приложения нам нужен не только PHP или Python, но и еще и Web сервер. Я сегодня наверно ограничусь только PHP, хотя начнем мы создавать приложение с простого HTML файла, чего достаточно для простого Web сайта.
Создадим новую папку: phpapache и в ней создаем Dockerfile, в котором опишем нужный нам образ.
Итак, если у вас есть сайт, который работает на PHP, то нам нужен образ, который будет включать и то и другое. Мы можем взять за основу образ PHP и потом в Dockerfile запустить команды установки Apache нужной нам версии и это будет прекрасно работать, но есть способ проще – взять за основу образ, в котором уже будет и то и другое и такое уже есть: php:7-apache.
FROM php:7-apache
После двоеточия стоит номер версии PHP и наличие Apache. Я в прошлый раз говорил, что после двоеточия – это тэг, который могут использовать для указания версии, но это происходит не всегда, тэг может включать и другую информацию, как в данном случае.
Мы создадим таким образом образ и у него будет установлен Apache и теперь его нужно сконфигурировать. Тут есть несколько подходов – мы можем подправить httpd-vhosts.conf файл, но тут нужно быть уверенным, что настроен таким образом, что у него конфигурация разбита на файлы. Apache может хранить все настройки в одном файле httpd.conf, а может хранить в нескольких. Если мы начнем использовать httpd-vhosts.conf, то нужно убедиться, что в файле httpd.conf есть строка:
Include /private/etc/apache2/extra/httpd-vhosts.conf
В принципе это достижимо, но в Apache есть способ проще – создавать конфигурацию сайтов для каждого в своем отдельном файле. Нам достаточно будет сайте по умолчанию, для которого нужно создать файл 000-default.conf. Создайте файл с таким именем и в него помещаем следующий код:
<VirtualHost *:80>
ServerName localhost
DocumentRoot /var/www/public
<Directory /var/www>
Options Indexes FollowSymLinks
AllowOverride All
Require all granted
</Directory>
</VirtualHost>
Один контейнер – один сайт, это отличный вариант, поэтому этого будет достаточно.
Здесь мы используем имя сервера по умолчанию localhost и указываем, что он будет смотреть на папку /var/www/public в поисках файлов. Дальше указываются права на доступ к папке.
Файл готов, его нужно закинуть в наш контейнер, чтобы apache внутри контейнера увидел эту конфигурацию. Файлы мы копируем командой COPY, так что в нашем Dockerfile появляется еще одна строка:
FROM php:7-apache COPY 000-default.conf /etc/apache2/sites-available/000-default.conf
Нужно убедиться, что нужная нам папка для файлов существует, поэтому добавим команду mkdir:
RUN mkdir -p /var/www/public
Теперь мы можем копировать в эту папку файлы нашего сайта и нужно поменять права на папку так, чтобы Web сервер имел доступ к ней:
COPY files /var/www/public RUN chown -R www-data:www-data /var/www/public
Для удобства я создал для файлов Docker отдельную папку files и в ней пока находится один только index.html.

Слева в дереве видна структура моего проекта – два файла находятся в корне папки phpapache, потому что один из них это Dockerfile и нам его внутрь образа копировать ненужно, и подпапка files, содержимое которой мы и копируем. Внутри только один файл index, содержимое которого видно на картинке справа.
Почти готово, нам осталось только сообщить в Dockerfile, что образ может открывать 80-й порт наружу, на этом порту как раз и работает Web сервер:
EXPOSE 80
И теперь нужно сообщить докеру, как он может запустить наш контейнер. Так как у нас Apache сервер, его можно запустить выполнив команду apache2-foreground:
CMD ["apache2-foreground"]
Мы запускаем именно в foreground режиме, когда сервер блокирует консоль и докер продолжает выполняться.
Полный файл докера выглядит так:
FROM php:7-apache COPY 000-default.conf /etc/apache2/sites-available/000-default.conf RUN mkdir -p /var/www/public COPY files /var/www/public RUN chown -R www-data:www-data /var/www/public EXPOSE 80 CMD ["apache2-foreground"]
Мы готовы создать образ, выполнив команду docker build и укажем тэг webapp:
docker build -t webapp .
Может показаться, что мы можем запустить этот образ, выполнив команду docker run:
docker run --name webtest webapp
Да, мы можем это сделать, но как потом получить доступ к сайту, который будет выполняться внутри контейнера? Мы говорили о том, что контейнеры работают изолированно и просто так мы не сможем получить к ним доступ. Нужно как-то приоткрыть дверь и показать, что мы хотим получить доступ к 80-му порту, который может быть открыт. Для этого при выполнении команды docker run нужно указать порты – локальный порт, при подключении к которому команды будут отправляться внутрь докера и удаленный порт внутри докера.
С удалённым все ясно, у нас там работает Apache, который по умолчанию использует 80-й порт. А локально я тоже мог бы использовать 80-й, но тут у меня уже есть локальный Apache, который уже занял этот порт и никому не отдает. Мы можем выбрать совершенно любой, обычно жертвой становится порт 8080. То есть нужно указать такую конфигурацию, при которой при обращении к порту 8080 локально (http://localhost:8080) вся информация передавалась на 80-й порт внутрь контейнера. Это можно сделать с помощью ключа -p, которому через двоеточие указывается два порта – локальный:удаленный. Так что наша команда для запуска будет выглядеть так:
docker run -p 8080:80 --name webtest webapp
Выполняем эту команду и в консоли можно увидеть запуск контейнера и сообщения от Apache сервера с предупреждениями, что он что-то использует по умолчанию:
AH00558: apache2: Could not reliably determine the server's fully qualified domain name, using 172.17.0.2. Set the 'ServerName' directive globally to suppress this message
У меня он возмутился, что не указан полный домен сайта, но он мне и не нужен, потому что для данного приложения домен не важен, мы будем обращаться к нему через локальный хост.
Отлично, можно попробовать загрузить в браузере сайт http://localhost:8080 и убедиться, что мы видим на странице сообщение Hello.
Все отлично, но у нас консоль занята. С одной стороны, это удобно, можно просто нажать Ctrl+C, чтобы прервать выполнение Apache и контейнер остановится. Попробуйте нажать и у вас контейнер остановится и сайт перестанет загружаться. Теперь мы можем удалить контейнер и создать новый. Во время тестирования иногда приходиться запускать новые контейнеры, удалять их и чтобы каждый раз не выполнять команду docker rm можно при запуске добавить ключ --rm:
docker run -p 8080:80 --rm --name webtest webapp
Если запустить контейнер с ключом rm, то после остановки он самоуничтожиться.
Когда мы уже отладили контейнер и все устраивает, можно запустить его в фоне, чтобы он не блокировал консоль добавив ключ -d:
docker run -p 8080:80 -d --name webtest webapp
Обратите внимание, я убрал ключ --rm, потому что не хочу больше автоматически самоутичножать контейнер.
В результате мы должны увидеть id нового контейнера в виде очень длинного кода:
66f090ce949277d8d5468e2a82b579ef78d05a2e9fa1eabef0ffc31036e10932
Если выполнить команду docker ps, то
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 66f090ce9492 phpwebapp "docker-php-entrypoi…" 11 seconds ago Up 9 seconds 0.0.0.0:8080->80/tcp phptest
Обратите внимание на Container ID – это в принципе тот же ID, который мы увидели при старте, только чуть сокращенная версия. При выполнении команд над контейнером вполне достаточно указывать сокращенную версию.
Теперь мы можем работать с контейнером указанием имени или ID, который мы увидели после создания или при выполнении команды docker ps.
Теперь наш контейнер работает, и мы можем его перезапустить docker restart, остановить docker stop или запустить заново docker stop. Так как контейнер выполняется в фоне, мы не сможем прервать его работу с помощью Ctrl+C. Теперь для остановки нужно использовать docker stop:
docker stop 66f090ce9492
Заново запустить:
docker start 66f090ce9492
Если вы укажите ключ -rm и установите контейнер stop, то конечно же не получиться запустить заново командой start.
В первом видео я не показывал вам команды stop и start потому что контейнер выполнялся и сразу же останавливался, поэтому запускать его заново смысла особо не было.
Обратите внимание, что контейнер останавливается и запускается очень быстро, на много быстрее, чем можно получить от виртуальной машины.
Теперь у меня есть сайт, который я могу запустить локально или поместить на хостинг, и он будет работать одинаково, мне не нужно ничего конфигурировать. Конечно же в случае с единственным файлом ощутить все прелести этого невозможно, но все же, можно включить воображение и представить что-то серьезное.
Если у вас сейчас мало посетителей, то одного контейнера может быть достаточно. С ростом сайта может потребоваться больше вычислительных ресурсов и если писать под контейнер, то мне сложно себе представить проблемы с масштабируемостью. Скорей всего достаточно будет поднять на хостинге два контейнера, поставить перед ними балансировщик нагрузки, который будет кидать траффик по очереди на каждый из контейнеров и таким образом мы удвоим мощности.
В контейнер можно помещать код и какие-то статичные файлы, которые меняться не будут. Если вы работаете в распределенном окружении, то бывает очень удобно держать файлы уже тут же на сервере.
Когда я работал над сони еще 10 лет назад, то у меня неизменяемый контент копировался на каждый из серверов, а изменяемый подключался через сетевой диск, чтобы все сервера имели доступ к одним и тем же файлам и их не приходилось синхронизировать.
Точно такую же идею взяли на вооружение и разработчики докера. Контейнер неизменяем и содержит статичный контент, а динамический контент можно подключить к контейнеру и тут есть два способа – подключить локальную папку и подключить doker том (volume или можно еще сказать диск). Docker том – это практически как папка, просто она добавлена в Docker.
Точно также мы можем подключать диски для хранения на них файлов баз данных, потому что это тоже изменяемый контент, который скорей всего вы захотите сохранять, а не терять по завершении работы программы.
Давайте посмотрим оба варианта на деле.
Изменим имя файла с index.html на index.php и в нем напишем немного PHP кода, который будет отображать на странице содержимое текстового файла, а если в URL переданы какие-то данные, то сохранять изменений в этот же файл:
<!DOCTYPE html>
<html lang="en">
<head>
<title>Document</title>
</head>
<body>
<h1>Hello from PHP</h1>
<?
if ($_REQUEST['data'] != '') {
file_put_contents("./data/file.txt", $_REQUEST['data']);
}
echo file_get_contents("./data/file.txt");
?>
</body>
</html>
Даже если вы не знаете PHP, код должен быть прост для чтения, пусть он и на самом деле говнокод, но главное достаточно простой для нашей задачи. Если в URL будет параметр data (например, http://localhost:8080/?data=данные), то его содержимое сохраняется в файл ./data/file.txt:
if ($_REQUEST['data'] != '') {
file_put_contents("./data/file.txt", $_REQUEST['data']);
}
После этого мы просто отображаем содержимое этого же файла:
echo file_get_contents("./data/file.txt");
На следующем скриншоте показана структура проекта. Слева у нас есть теперь в папке files подпапка data и в ней файл file.txt. Это как раз файл, к которому мы будем обращаться. Справа на скриншоте видно содержимое файла. Я просто поместил в него какую-то информацию по умолчанию:

Собираем заново контейнер:
docker build -t webapp .
Запускаем приложение:
docker run -p 8080:80 -d --name phptest webapp
Загружаем страницу localhost:8080 и на странице видим:
Hello Default content
Чтобы обновить информацию мы можем загрузить URL:
http://localhost:8080/?data=Updated
И в файле сохраниться Updated. Этот файл храниться в контейнере и только в нем. Если остановить контейнер и запустить снова
docker stop phptest docker start phptest
То содержимое файла все еще будет на месте, потому что контейнер – это слой с возможностью записи вокруг образа, но сам образ не изменился, в нем файл все еще содержит текст Default content.
Попробуем остановить контейнер, создать новый и запустить новый:
docker stop phptest docker rm phptest docker run -p 8080:80 -d --name phptest webapp
Мы вернулись к надписи Default content. И это верно, потому что изменяемый контент должен жить отдельно, и мы будем подключать его отдельно. Давайте подключим папку, к контейнеру. Для этого используем ключ -v и через двоеточие указываем сначала путь к локальной папке и путь к папке внутри контейнера. Так же как мы указывали через двоеточие порт.
Итак, новая команда для запуска контейнера:
docker run -p 8080:80 -d -v /Users/mikhailflenov/Projects/docker/phptestdata:/var/www/public/data --name phptest webapp
После ключа -v идет путь к локальной папке:
/Users/mikhailflenov/Projects/docker/phptestdata
Которая будет примонтирована к папке внутри контейнера:
/var/www/public/data
Именно оттуда мой PHP код читает файл и туда же записывает. То есть теперь он будет писать не файл внутри контейнера, а в файл на моем компьютере.
Запустите сайт и попробуйте загрузить сайт и обновить данные. Обратите внимание, что следующий файл изменился:
/Users/mikhailflenov/Projects/docker/phptestdata/file.txt
Он находиться локально на компьютере и подключался к контейнеру. Это значит, что даже если контейнер умрет и мы создадим новый с указанием этой же папки, то новый контейнер увидит наши изменения.
Отлично, подключение папок работает как надо. Второй способ – это те же папки, просто их назвали томами внутри docker и уже docker как бы управляет этими папками.
Новый том можно создать выполнив команду docker volume create и указав ей имя тома. Давайте создадим том phptestappvolume:
docker volume create phptestappvolume
Теперь просмотреть тома можно с помощью команды volume ls:
docker volume ls
Результат:
DRIVER VOLUME NAME local phptestappvolume
Теперь при запуске контейнера мы также должны указать параметр -v, только вместо локального пути можно указать том web:
docker run -p 8080:80 -d -v web:/var/www/public/data --name phptest webapp
Не знаю почему, но не работает. Первый урок хотя бы отображал в терминале результаты. А в этом, втором уроке, в браузере ничего нет. Всё прямо четко копировал полные файлы, по видео попробовал повторить - не работает. Может с Win 8.1 не совместимо? (в браузере пишет: "Попытка соединения не удалась")
Разобался. В общем, localhost работать не будет. При выполнении команды docker run -p 8080:80 --name webtest webapp надо смотреть на указанный ip адрес без домена. Тогда всё откроется. Спасибо!

А попробуй 127.0.0.1 вместо localhost и ip, просто интересно. У меня Win 8.1 нет, чтобы посмотреть, что там за проблема