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

С одной стороны — про него все знают. С другой — если тебя устраивают небольшие веб-проекты, особенно на какой-то конкретной CMS, то докер очень часто оказывается тем самым «первым лишним», и все сводится к инструкции «возьми готовый докер-файл, запусти в терминале docker run и будет тебе счастье». А когда понимаешь, что этого уже недостаточно — натыкаешься на статьи, написанные по тому же принципу «скачайте-запустите-получите». Кому этого мало — читайте дальше.

Зачем программисту холодильник

Кто сказал: «Не люблю пить теплое»? Убираем емкость подальше и смотрим на принципиальную схему девайса:

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

А теперь представим, что в доме программиста появилось существо противоположного пола, считающее, что в холодильнике можно хранить мясо. Что характерно — замороженное. Но при этом, как несложно догадаться, замерзнут до непотребного состояния и напитки, что нас в некотором смысле не совсем устраивает.

У нас два пути: купить второй холодильник или разделить тот, что у нас есть, на две камеры с датчиками температуры на входе, чтобы управлять клапаном подачи хладагента внутрь. Ура, мы только что изобрели двухкамерный холодильник. А заодно — разобрались с тем, для чего программистам понадобились виртуальные машины и докер. 

Проекты, которые мы делаем, не сильно отличаются от продуктов в холодильнике — каждому нужны свои условия. Для одного — PHP 7.4, база MySQL 7.6, Sphinx и мейлер на Golang. Для другого — нода 12 версии, Angular 7 и база MySQL 8.0. Проектов может быть не один десяток. Установить это все на одну машину — все равно, что запихнуть все продукты в одну камеру холодильника. 

Нужно как-то изолировать один проект (продукт) от другого. На помощь приходит или виртуальная машина (еще один холодильник), или докер (вторая камера со своими настройками). Давайте немного изменим схему нашего устройства:

Включаем воображение и смекалку, поехали!

Итак, у нас есть квартира (компьютер) со своей инфраструктурой, от которой нам требуется электричество (жесткий диск, сетевая плата, процессор, etc). Для установки второго холодильника (виртуальной машины) нам нужен разветвитель розеток (hypervisor). Довольно просто, но мы видим, что для изоляции мяса от напитков нам потребовалось два комплекта оборудования (Guest OS), хотя по факту условия хранения определяет только датчик, управляющий клапаном к капиллярной системе (bins/lib). 

В случае, когда мы физически разделяем холодильную и морозильную камеры (container engine), нам не нужна вторая розетка (hypervisor) и место для второго холодильника (полноценная Guest OS). Мы получили два независимых контейнера — каждый со своими условиями (bins/lib), которые подходят нужному продукту (app).

Что ты, черт возьми, такое, Докер?

Разделим вопрос на две части: «как с этим работать» и «как это работает». Эта статья посвящена ответу на первую часть. Если интересно углубиться в детали, пишите в комментариях — поговорим про изоляцию процессов, пространства имен и прочее. А может быть, напишу еще одну статью.

Из схемы выше очевидно, что ответ на вопрос «что такое докер» спрятан в блоке «Container/Docker engine», иначе говоря — движок контейнеризации. Давайте посмотрим на него внимательней:

Первым делом нам потребуется некий сервис (Docker daemon), который будет управлять всем процессом — набор инструкций, как создавать изолированные пространства. Поселим его на своем компьютере!

Сам процесс установки прост и у вас не должно возникнуть трудностей.

Неважно, какую операционную систему вы выберете — все равно выполнение произойдет на Linux. В Windows и Mac будет запускаться виртуализированное ядро Линукса для докера.  

Для того, чтобы мы могли как-то управлять этим сервисом, воспользуемся REST API, а команды будем выдавать посредством CLI, назовем его для удобства клиентом (Docker client). 

Сами приложения и нужные для их работы библиотеки мы будем хранить в виде файлов-образов (Docker-images). Можете воспринимать их по аналогии с ISO-образами DVD-дисков или как специфический вид архива с данными.

Чтобы все заработало, мы должны с помощью клиента попросить Docker daemon взять конкретный образ и развернуть его в работающий контейнер. Но откуда он его возьмет? Добавим немного логики и инфраструктуры — пусть Docker daemon создаст на нашем компьютере реестр образов и при запросе находит нужный. А если не нашел — отправляется в сеть, находит Docker Hub (сетевой реестр), находит там нужный образ и копирует к нам на локальный компьютер. 

Указать, какой конкретно образ нам нужен, мы можем или при запуске контейнера из готового образа (docker run), или при создании нового образа (docker build), или просто запросив скачивание (docker pull).

Получив нужный образ, докер-демон запустит на его основе контейнер, и мы получим работающее приложение. Заодно докер пробросит внутрь изолированного контейнера сеть (network), чтобы мы смогли увидеть результаты работы приложения, и при необходимости «прикрутит» к нему хранилище для сохранения данных (data volumes).

Повторение — мать учения

Теперь все вышеизложенное — но на примере демонстрационного приложения Hello World. Считаем, что демона мы на своем компе уже поселили, ссылка на установку — чуть выше по тексту.

Выполняем команду docker run hello-world и видим следующий результат:

Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
b8dfde127a29: Pull complete 
Digest: sha256:308866a43596e83578c7dfa15e27a73011bdd402185a84c5cd7f32a88b501a24
Status: Downloaded newer image for hello-world:latest

Hello from Docker!
This message shows that your installation appears to be working
correctly.

To generate this message, Docker took the following steps:
 1. The Docker client contacted the Docker daemon.
 2. The Docker daemon pulled the "hello-world" image from the Docker
 Hub.
    (amd64)
 3. The Docker daemon created a new container from that image
 which runs the
    executable that produces the output you are currently reading.
 4. The Docker daemon streamed that output to the Docker client, which
 sent it
    to your terminal.

To try something more ambitious, you can run an Ubuntu container with:
 $ docker run -it ubuntu bash

Share images, automate workflows, and more with a free Docker ID:
 https://hub.docker.com/

For more examples and ideas, visit:
 https://docs.docker.com/get-started/

Давайте разбираться.

Первое что произошло после команды «докер, запусти образ с именем hello-world», это попытка найти его локально и запустить.

Unable to find image 'hello-world:latest' locally

Попытка не увенчалась успехом (у меня была чистая установка докера), причем, обратите внимание, искался образ не hello-world, а hello-world:latest.
Через двоеточие указывается тег — что-то вроде версии или модификации образа. Если его не указать, будет искаться самая свежая версия с общепринятым тегом latest.

latest: Pulling from library/hello-world

Тогда докер решает поискать этот образ на docker hub`е и скачать его оттуда.
https://hub.docker.com/_/hello-world

b8dfde127a29: Pulling fs layer
b8dfde127a29: Downloading 488B/977B
b8dfde127a29: Extracting 977B/977B
b8dfde127a29: Pull complete

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

А далее видим следующие строки:

b8dfde127a29: Pull complete 
Digest: sha256:308866a43596e83578c7dfa15e27a73011bdd402185a84c5cd7f32a88b501a24
Status: Downloaded newer image for hello-world:latest

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

А именно — из только что скачанного образа был создан контейнер. Если перевести на язык ООП, создался объект sleepy_antonelli (контейнер) экземпляр класса hello-world (образа). Sleepy_antonelli — это рандомно сгенерированное имя контейнера, поскольку мы не указали его явно.

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

Hello from Docker!
This message shows that your installation appears to be working
correctly.

To generate this message, Docker took the following steps:
 1. The Docker client contacted the Docker daemon.
 2. The Docker daemon pulled the "hello-world" image from the Docker
 Hub.
    (amd64)
 3. The Docker daemon created a new container from that image which
 runs the
    executable that produces the output you are currently reading.
 4. The Docker daemon streamed that output to the Docker client, which
 sent it
    to your terminal.

To try something more ambitious, you can run an Ubuntu container with:
 $ docker run -it ubuntu bash

Share images, automate workflows, and more with a free Docker ID:
 https://hub.docker.com/

For more examples and ideas, visit:
 https://docs.docker.com/get-started/

Собственно, это тут и написано (вместе с призывом не останавливаться на достигнутом и ссылкой на документацию). Но давайте проверим сами.

Если выполнить команду docker images, мы увидим скачанный образ.

REPOSITORY    TAG       IMAGE ID       CREATED       SIZE
hello-world   latest    d1165f221234   3 weeks ago   13.3kB

А если выполнить команду docker ps -a флаг «a» показывает все образы, даже те, что остановлены в данный момент — то увидим и сам контейнер, как раз с именем  sleepy_antonelli.

CONTAINER ID   IMAGE         COMMAND    CREATED          STATUS          PORTS     NAMES
01d1bc18db71   hello-world   "/hello"  18 seconds ago   Exited (0) 17    seconds ago             sleepy_antonelli

Имена можно задавать и самим, но нам пока это не нужно, поэтому докер сам генерирует для имени два рандомных слова. Также тут видны его ID, образ, с которого он был сделан, команду, ради которой был запущен (в данном случае — написать приветствие), когда был создан, код завершения работы (0 — это штатное завершение) со временем, порты (в данном случае они никак не пробрасывались, потому и пусто) и, наконец, имя.

Из одного образа можно создать много контейнеров. Также их можно останавливать, запускать и удалять. Но важно помнить, что образ — это шаблон. Он создается один раз и не меняется, а контейнеры можно модифицировать индивидуально. Это то же самое, что получить с завода (образа) 10 одинаковых контейнеров для перевозок грузов, но перекрасить каждый в разный цвет, повесить разные замки, и по-разному назвать.

«Людоеды — как лук, многослойные!»

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

Нет, для этого придуманы слои. Каждое изменение образа можно выносить в отдельный слой. Это позволяет комбинировать их в разные итоговые образы. И вдобавок, уже готовый образ можно взять за основу и «наслоить» что-то свое.

Это можно представить в виде стопки блинов — тех, что железные. Мы надеваем на гриф блины, чтобы получить нужную нам конфигурацию (вес). Но при этом мы не можем вынуть или заменить какой-то блин в середине — придется пересобрать конфигурацию или довесить новые поверх имеющихся.

Если выполнить предложенную команду из hello world:

To try something more ambitious, you can run an Ubuntu container with:
 $ docker run -it ubuntu bash

То мы увидим, что там несколько слоев:

Unable to find image 'ubuntu:latest' locally
latest: Pulling from library/ubuntu
04a5f4cda3ee: Pull complete 
ff496a88c8ed: Pull complete 
0ce83f459fe7: Pull complete 
Digest: sha256:a15789d24a386e7487a407274b80095c329f89b1f830e8ac6a9323aa61803964
Status: Downloaded newer image for ubuntu:latest
root@61f60707bdbe:/#

Подведем итоги

Что же дает нам докер? Во-первых, это изоляция — мы можем запускать что угодно на своем компьютере, не опасаясь за целостность как системы, так и приложения. Они просто не пересекаются. 

Во-вторых — чистота. Образ на диске — это самые обычные «инертные» файлы, которые «оживают» только при создании контейнера. Контейнеры же изолированы от внешнего мира.  

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

Наконец, это предсказуемость. Контейнеру все равно, что находится снаружи. Поэтому если ваше приложение работало в контейнере на вашем локальном компьютере, оно заработает на любом другом. Забудьте про dependencies hell — эта фраза дорогого стоит! 

Можно, конечно, вспомнить еще и  о том, что тот же Kubernetes является развитием идеи контейнеров, перенесенной на уровень DevOps, но достаточно и того, что изложено выше.  

Вопросы, уточнения и комментарии можно оставлять прямо под текстом — буду рад пообщаться.

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


  1. tdemin
    22.10.2021 10:57

    Третья иллюстрация (под спойлером), по моему мнению, не самая удачная:

    Иллюстрация
    1. В схеме полностью опущен контейнерный OCI-рантайм, в случае Docker это containerd.

    2. Непонятно, почему с ресурсами хостовой машины, на которой запущен dockerd (сети, тома, контейнеры/образы, etc), соединен клиент, а не сервер, и почему он вообще оборачивает собой REST API. Для вводного материала для новичков (это же он, да, исходя из вступительных ремарок?) не очень удачно?

    К слову, факт существования контейнерного рантайма в принципе опущен в статье, по ней можно подумать, что dockerd сам по себе занимается обслуживанием контейнеров.


    1. ZeBrains_team Автор
      22.10.2021 11:51

      Спасибо за хорошее уточнение. Поясню, почему выбран именно такой вариант схемы. В начале раздела специально сделана ремарка — мы рассматриваем "как с этим работать", а не "как это работает/устроено". В заявленной парадигме мы работаем через клиент, командами которого обращаемся к демону, просим скачать образ, запустить контейнер, прикрутить том с данными или сеть... Именно поэтому "внешним" на схеме является клиент, а ресурсов машины на ней нет вообще. Вы правы, если смотреть на схему с точки зрения "как это происходит" и взаимодействия с ресурсами — ее надо видоизменять. Но в логике объяснения на уровне холодильников и штанг — добавлять про рантайм и углубляться — только усложнять восприятие, имхо.


  1. amarao
    22.10.2021 12:37
    +1

    Объяснение простых концепций с помощью странных аналогий. Почему холодильник? Почему докер? Если приложению нужны свои лимиты, они тривиально выставляются в ulimit, или в cgroups, без привлечения докера.


    1. ZeBrains_team Автор
      22.10.2021 13:08
      -1

      Почему сковородка, почему плита? Пожарить мясо элементарно можно на костре. Логично, но абсолютно не уместно. Статья про докер, а не про лимиты или ручную изоляцию "без привлечения докера".


      1. amarao
        22.10.2021 14:51

        Именно, статья должна была быть про докер, а вместо этого пачка аналогий, которые не объясняют причину популярности докера. Вы зачем-то делаете упор на изоляцию, хотя docker - плохое средство изоляции (в сравнении, например, с VM). Ключевая особенность докера - унификация.


        1. 13werwolf13
          22.10.2021 15:00

          я бы сказал что ключевая особенность докера вообще не касается самого докера

          ключевая его особенность это возникшая вокруг него экосистема с выбором из 100500 инструментов и интерфейсов

          а всё что умеет докер до него умел lxc, и даже больше lxc может сильно больше и вариативнее

          сейчас ещё есть systemd-nspawn который скорее всего так же являлся бы хорошей альтернативой если бы не отсутствие экосистемы


          1. amarao
            22.10.2021 15:07

            У LXC не было унификации delivery. Чтобы запустить что-то с LXC его нужно построить, а процесс построения не определён для LXC (сравнить с `docker build`), не было адекватного аналога hub'а чтобы быстро получить mpv и т.д. Вы правы про экосистемы, но она не "возникла", а было создано достаточно условий, чтобы она могла развиваться.

            Ещё у LXC ужасная совместимость между версиями. Смена формата конфигов для subuserid в своё время была просто душераздирающей - новая версия не понимала старого формата, старая не понимала новый, и никакого transitional path (именно в этом месте я lxc и закопал).

            systemd-nsspawn - это вообще не об этом. Запускалок в namespace'ах много (и firejail туда же). Ближайшее у systemd - это machinectl с image'ами сервисов


  1. uhf
    22.10.2021 15:05

    Я считаю аналогию со штангой неудачной.
    Каждый слой образа — это фиксация изменений (коммит) файловой системы. Суммарный вес штанги — это лишь итоговый вес образа, его объем в байтах. Конфигурация образа — это его содержимое на последнем слое, и вес тут никакой роли не играет.
    Да, из соображений экономии диска, вес образа желательно сокращать, но это уже ньюансы сборки.