Docker получил широкое распространение благодаря своим возможностям по работе с контейнерами и прежде всего, благодаря тому функционалу, который он предоставляет для работы с приложениями в контейнерах. В этой статье мы рассмотрим работу с приложениями и сервисами в контейнерах Docker, поговорим о том, чем сервисы отличаются от приложений и как с ними работать.

Контейнеры и образы

Иногда у начинающих пользователей при работе с Docker возникает некоторая путаница, связанная с понятиями контейнера и образа. Итак, попробуем разобраться. Образ — это файл, хранящий всю конфигурацию запуска программы в контейнере Docker. Образ не изменяется в процессе работы контейнера.

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

Работа с приложениями

Итак, что же происходит, когда мы запускаем контейнер. В качестве классического примера рассмотрим запуск hello-world:

docker run hello-world

Команда docker run указывает Docker запустить приложение в контейнере. После запуска Docker ищет в кэше нужный образ с названием hello-world. Если такого образа нет, то сервер Docker связывается с хабом Docker, где скачивает образ с указанным именем. Хаб Docker получает запрос от сервера Docker на загрузку конкретного файла образа из общедоступного хранилища. В результате Docker загружает нужный образ и, при необходимости другие образы. И согласно первоначальным установкам из образа, в контейнере запускается простая программа hello-world. Как видно здесь все достаточно просто.

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

Для создания собственного контейнера на основе готового образа нам необходимо подготовить специальный Dockerfile, который будет содержать все необходимые команды для работы нашего контейнера. Dockerfile — это текстовый документ, в котором содержатся все команды, которые пользователь может вызывать в командной строке для сборки образа.

Вот пример такого файла для hello-world:

FROM busybox:latest

CMD echo Hello World!

Для сборки и запуска контейнера выполним в том каталоге, где мы создали Dockerfile следующие команды:

docker image build -t hello-world .

docker run hello-world

С помощью ключа -t мы назначаем контейнеру тег. В итоги получим следующее:

В первой строке мы запускаем образ busybox, а во второй указываем Docker выполнить команду echo Hello World! Таким образом, мы видим, что Docker может собирать образы автоматически, считывая инструкции из Dockerfile.

Любая инструкция выполняет конкретное действие в новом слое поверх текущего образа и создает новый слой, который доступен для последующих шагов в Dockerfile. Посмотрим еще несколько примеров немного сложнее.

В следующем примере мы используем образ busybox, в котором объявляем переменную окружения FOO=/bar, далее объявляем рабочий каталог со значением переменной FOO,  и копируем из $FOO в /quux.

FROM busybox

ENV FOO=/bar

WORKDIR ${FOO}   # WORKDIR /bar

COPY \$FOO /quux # COPY $FOO /quux

Как видно из этого примера, мы можем выполнять операции с каталогами при работе с контейнерами.

В следующем примере мы не ограничиваемся только операциями с каталогами, хотя здесь они тоже присутствуют. В этом примере мы используем команды RUN и CMD для запуска команд. Инструкция RUN позволяет выполнять любые команды, в то время, как CMD (и аналогичная ей команда ENTRYPOINT)  определяют, какая команда будет выполняться при запуске контейнера.

FROM python:3.7.2-alpine3.8

RUN apk update && apk upgrade && apk add bash

COPY . ./app

ADD https://content_file \

/my_app_directory

RUN ["mkdir", "/a_directory"]

CMD ["python", "./my_script.py"]

И CMD и RUN имеют две формы exec и shell. В приведенном примере RUN использует обе формы, а CMD только exec форму.

ENTRYPOINT ["executable", "param1", "param2"]

ENTRYPOINT command param1 param2

CMD ["executable","param1","param2"]

CMD command param1 param2

И еще один пример, полезный с практической точки зрения это создание контейнера с веб-сервером Nginx, который при запуске копирует контент и файлы конфигурации из внешних каталогов и сохраняет журналы событий также на внешний, подмонтированный к контейнеру ресурс (инструкция VOLUME).

FROM nginx

COPY content /usr/share/nginx/html

COPY conf /etc/nginx

VOLUME /var/log/nginx/log

После предварительной подготовки всех необходимых ресурсов мы можем собрать данный контейнер с помощью уже знакомых нам команд:

docker image build -t nginx .

docker run -p 8080:80 nginx

Здесь при запуске контейнера мы пробросили локальный порт 80 на внешний порт 8080.

Когда нужен демон

В примере с nginx мы создали и запустили контейнер, но он будет работать только пока он запущен в консоли, что согласитесь, не слишком удобно, особенно в продуктивной среде. Гораздо удобнее было бы запускать контейнер в режиме сервиса, по аналогии с демонами в Linux. Реализовать такой режим можно с помощью ключа -d при запуске контейнера.

docker run -d -p 8080:80 nginx

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

Политика перезапуска

В Docker есть политика перезапуска с помощью которой можно указать следует или не следует перезапускать контейнер при выходе. Возможны следующие 4 значения:

  • no – не перезапускать контейнер автоматически (вариант по умолчанию)

  • on-failure - перезапустить контейнер, если выполняется выход из-за ошибки (ненулевой выход)

  • always - всегда перезапускать контейнер, если он останавливается. Если контейнер остановлен вручную, он перезапускается только при перезапуске демона Docker.

  • unless-stopped - аналогично always, за исключением того, что, когда контейнер остановлен, он не перезапускается даже после перезапуска демона Docker

Как видно, мы можем выбрать какой именно вариант перезапуска нам наиболее подходит. Для примера с nginx предлагаю использовать вариант always. Политика задается при запуске контейнера с помощью ключа –restart.

docker run -d --restart always -p 8080:80 nginx

Конечно в Kubernetes “живучесть” контейнеров обеспечивается гораздо более мощными средствами, но наша статья посвящена исключительно Docker.

Заключение

В этой статье мы поговорили о том, как создавать собственные контейнеры и как обеспечивать отказоустойчивость их работы. В следующей статье мы поговорим о такой довольно экзотической вещи, как Docker Inside Docker.

В заключение рекомендую к посещению открытый урок «Kubernetes: pods, контроллеры репликации и службы», который скоро пройдет в OTUS. На этой встрече вы:

  • поймете базовых сущностей в K8s;

  • разберетесь, как связаны с собой контроллер репликации, под и служба;

  • научитесь предоставлять единую точку входа к pods и поддержание их в необходимом количестве для стабильной работы.

Записаться на открытый урок

Комментарии (2)