Web приложение в Docker контейнере – от простого к сложному

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

Комментарии

Иван

14 Октября 2021

Не знаю почему, но не работает. Первый урок хотя бы отображал в терминале результаты. А в этом, втором уроке, в браузере ничего нет. Всё прямо четко копировал полные файлы, по видео попробовал повторить - не работает. Может с Win 8.1 не совместимо? (в браузере пишет: "Попытка соединения не удалась")


Иван

14 Октября 2021

Разобался. В общем, localhost работать не будет. При выполнении команды docker run -p 8080:80 --name webtest webapp надо смотреть на указанный ip адрес без домена. Тогда всё откроется. Спасибо!


Михаил Фленов Зарегистрированный пользователь

15 Октября 2021

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


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

О блоге

Программист, автор нескольких книг серии глазами хакера и просто блогер. Интересуюсь безопасностью, хотя хакером себя не считаю

Обратная связь

Без проблем вступаю в неразборчивые разговоры по e-mail. Стараюсь отвечать на письма всех читателей вне зависимости от страны проживания, вероисповедания, на русском или английском языке.

Пишите мне