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