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

Пишите мне