Прод с докером. Фото в цвете
Прод с докером. Фото в цвете
Опытный капитан, прочитай сперва

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

Что такое контейнеризация?

Чтобы понять смысл контейнера сначала стоить обратиться к такой вещи, как образ. 
Образ - это шаблон по которому будет создаваться контейнер. Он может хранить в себе целую операционную систему! И именно образы скачивают с именитого docker-hub. Образы можно создавать(о том, как это делать, будет написано ниже), удалять и даже наслаиваться друг на друга (при создании образов так и делают), но никак не редактировать существующий образ (тут можно привести сравнение с образом диска. Они, по сути, идентичны). Образы хранятся в регистрах и маркируются тегами. Последний образ в регистре автоматически помечается тегом latest. Формат названия образа такой:

регистр/имя_образа:тег

Этих знаний на текущий момент времени будет достаточно, чтобы стать на шаг ближе к технологии контейнеров!

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

А зачем все это?

Стоит начать с того, что контейнер - среда изолированная и оттого безопасная. 
Поэтому вы можете вложить в свой образ любые необходимые вам инструменты и не беспокоится о том, что на сервере, где будет развертываться ваше приложение, node.js версии 8, а не 16.
А ведь приложение в команде должно нормально запускаться как и у всех разработчиков, так и на тестовом и продуктивном стенде. Из этого вытекает еще один плюс - правильно собранный образ запустится везде, где можно запустить докер.

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

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

Установка Docker и первый контейнер

Docker, как и любая другая программа, требует установки. Скачать ее можно с официального сайта. От пользователей windows и macOS требуется лишь установить приложение Docker Desktop. Также на сайте представлены инструкции для пользователей разных дистрибутивов Linux. 

После того, как вы установили docker, он станет доступен как утилита командной строки (У пользователей Windows и macOS также доступен графический интерфейс, но в рамках туториала мы будем рассматривать только работу с CLI. С привычкой к вам придет понимание, что это проще и быстрее)

Теперь можно запустить первый образ! Откроем терминал и введем туда команду:

docker run hello-world 

После этого произойдет магия (пока что) и докер отобразит вам следующее:

Привет мир в докере
Привет мир в докере

А теперь разберем, что здесь произошло (просто перевод того, что на терминале с небольшими дополнениями):

  • Докер клиент подключился к докер-серверу (демону) и передал ему набор инструкций.

  • Демон начал искать образ локально и не нашел. Поэтому он скачал его из регистра под названием library (Это имя пользователя docker hub. В library хранятся официальные образы, которые загружаются по умолчанию).

  • Демон забрал образ из регистра (автоматически за вас выполнил команду pull) с тэгом latest (по умолчанию, скачивается именно он).

  • Демон развернул контейнер из образа, запустил его и присоединил вывод контейнера к клиенту.

Теперь магия не совсем магия, так что давайте попробуем что-нибудь написать!

Собираем свой первый образ

Примеры можно найти в github репе

Для того, чтобы собрать свой образ, нам необходим лишь один файл - Dockerfile. Я покажу как это работает на примере простого сайта на python + flask, который вернет hello world при открытии.

Для начала напишем небольшое приложение. Создайте файл app.py и введите туда следующий код (это курс по Docker и в рамках этого курса не буду углубляться в python - простого копипаста кода должно хватить).

from flask import Flask

app = Flask(__name__)

@app.route('/')
def hello():    
    return '<h1 style="color: #003f8c"> Hello Docker world! </h1>'

Также необходимо указать зависимости. Создайте файл requirements.txt со следующим наполнением:

click==8.0.3
colorama==0.4.4
Flask==2.0.2
gunicorn==20.1.0
itsdangerous==2.0.1
Jinja2==3.0.3
MarkupSafe==2.0.1
Werkzeug==2.0.2

Скорее всего, у вас не получится запустить проект, так как у вас на машине нет python и менеджера пакетов pip. На помощь приходит docker.

Давайте создадим Dockerfile, по которому Docker создаст образ. Любой Dockerfile начинается с директивы FROM <образ>, которая обозначает базовый образ.

После термина "базовый образ" может возникнуть вопрос: а точно ли образы неизменямы? Точно! При создании образа на каждом этапе докер создает контейнер из того, что получилось на предыдущей директиве, выполняет в нем команду и сохраняет новый образ... И так до тех пор, пока не выйдет готовый образ - это один из методов кеширования сборки. Если вы изменяете Dockerfile, выполненные директивы до измененных строк будут взяты из кэша. Поэтому хорошей практикой будет делать копирование, загрузку чего-либо как можно позже, а новые директивы добавлять в конец файла.

Я хочу использовать образ python с версией 3.8, работающий на Alpine Linux. В docker hub есть такой образ и называется он python:3.8-alpine. В нем уже есть все, что мне требуется - интерпретатор python нужной мне версии и пакетный менеджер pip. Пишем первую директиву:

FROM python:3.8-alpine

После этого, я загружу файл с зависимостями директивой COPY <локальный путь> <путь внутри контейнера>, которая перенесет его в контейнер (точка, по классике, обозначает текущую директорию). Чуть выше описано, почему сейчас скачиваются только зависимости, но повторюсь: все дело в кешировании. Если вы захотите что-то поменять в коде, или добавить что-то в Dockerfile под строку с копированием и установкой зависимостей (следующая строка), то вам не придется ждать установки зависимостей, так как это уже закешированный шаг сборки.

COPY ./requirements.txt .

Теперь нужно установить зависимости с помощью pip. Для этих задач прекрасно подходит директива RUN <shell команда>, которая исполнит любую вашу прихоть команду в шелле контейнера (если она доступна). Если при вызове команды возникнут ошибки (например, вызов RUN npm build в контейнере из python:3.8-alpine зафейлиться, так как внутри нет этого самого npm и его сначала нужно установить), то они отобразятся в логе сборки.

RUN pip install -r ./requirements.txt

И только теперь копируем все файлы в сборку

COPY . .

Теперь нам стоит объяснить докеру, как ему взаимодействовать с контейнером. Давайте заставим его запускать веб-сервер при запуске контейнера при помощи директивы CMD ["команда", "аргумент1", "аргумент2"], которая подскажет докеру, что именно нужно запустить после запуска контейнера.

CMD ["gunicorn", "--bind", "0.0.0.0", "app:app"]
Бесполезно, но полезно

Директиву CMD можно опустить. Тогда команду, которая исполнится в контейнере, придется указать вручную.

docker run -it my-first-image python
# Запустит интерпретатор Python в контейнере

Кстати, если задать команду вручную, она переопределит ту, что указана в CMD. А если не указать CMD вообще, то Docker возьмет ее из базового образа

На этом все - наш Dockerfile готов к тому, чтобы из него получился образ! Давайте его соберем:

docker build . -t my-first-image

Команда docker build предназначена для сборки образов. Точка после нее соответствует пути сборки (все, что в ней находится является контекстом сборки). При ее запуске клиент docker передает контекст демону, который по инструкциям-директивам начинает собирать образ. Аргумент -t определяет имя образа. Можно обойтись и без него, но тогда запускать образ придется по id (который можно найти введя команду docker images - docker выведет список образов).

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

docker run -it -p 8000:8000 my-first-image

С docker run мы уже знакомы - эта команда запускает образы, но появились новые аргументы. Давайте их разберем:

  • -it на самом деле являются двумя аргументами -i и -t. Docker позволяет задавать последовательно идущие аргументы без параметров опустив пробел и тире.

  • Аргумент -i обозначает то, что клиент docker подключит ваш ввод к контейнеру.

  • Аргумент -t обозначает то, что для исполнения контейнера будет выделен псевдотерминал.

  • Аргумент -p <локальный порт>:<порт контейнера>/<протокол> пробрасывает(размечает) порты. Если не указывать протокол, будет использоваться TCP. Если нужно разметить несколько портов, нужно указать аргумент -p несколько раз. Например:

docker run -it -p "80:8000/tcp" -p "5000:5000/udp" my-first-image
  • Разметит порт контейнера 8000 к локальному порту 80 по протоколу tcp и порт 5000 к 5000 по протоколу udp. Также, если у вас несколько сетевых адресов и вы хотите разметить порт для конкретного, можно дополнительно указать IP адрес перед локальным портом в формате <IP адрес>:<локальный порт>:<порт контейнера>/<протокол>

docker run -it -p "127.0.0.1:80:8000/tcp" my-first-image

Вот мы и запустили контейнер. Давайте перейдем на localhost:8000 и полюбуемся на результат в терминале).

Вот и гуникорн! Радуги только не хватает
Вот и гуникорн! Радуги только не хватает

Работает! Значит все сделано правильно и можем попробовать перейти по localhost:8000/

Да сколькож можно-то хенло вордов-то ваших...
Да сколькож можно-то хенло вордов-то ваших...

Открылось! А что это значит? А это значит то, что ты, читатель, молодец, потому что у тебя получилось освоить эту статью, пройти по всем шагам и развернуть свое первое приложение внутри докера!

Команды докера, которые мы освоили

  • docker pull <имя образа> - загрузить образ из регистра;

  • docker build - собрать образ;

  • docker tag - переименовать образ;

  • docker run - запустить образ;

  • docker images - список образов, доступных локально.

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


  1. ZiggiPop
    16.02.2022 02:34
    +8

    Допустим, я — юнга.

    >Директиву CMD можно опустить.

    И что в этом случае произойдет?

    >Тогда команду, которая исполнится в контейнере, придется указать вручную.

    «Вручную» это как? Что нужно сделать, чтобы «указать вручную»?

    Из статьи не понятно, чем отличается запуск RUN от CMD. Вроде ж и то, и другое выполняют команду, только у CMD синтаксис какой-то непривычный, не так ли?

    >а RUN <shell команда>, которая исполнит любую вашу прихоть команду в контейнере (если она доступна).

    А почему она может быть доступна в одном случае, и недоступна в другом? И как ее сделать доступной, если она мне внезапно нужна в конкретном случае, когда она недоступна? Статья на эти вопросы не отвечает.

    На мой взгляд на Хабре уже есть более удачные варианты «для юнги».


    1. momoru_kun Автор
      16.02.2022 03:31
      +1

      Спасибо, что прочитали статью и прокомментировали ее. Указанные косяки я исправил. И чуть дополнил моменты с кэшированием.

      На тему более удачных вариантов на Хабре и не только. Отразить могу только статью от селектела (https://selectel.ru/blog/what-is-docker/) которой, кстати, здесь нет, и "Понимая docker" (https://habr.com/ru/post/253877/) в которой объясняется принцип работы: namespaces, cgroups и почему на винде для докера все-таки нужна виртуализация (что для джуна, согласитесь, не самая важная информация. Особенно, когда его кидают на проект, где есть какой-то не понятный Dockerfile). Я же попытался сделать выжимку большого количества статей, видосиков по докеру, пропущенную через призму своего восприятия которую, в случае чего, можно юзать, как шпоргалку, поэтому и опубликовал и считаю, что статья имеет место для жизни.


      1. mSnus
        16.02.2022 05:33

        Например, вот: https://habr.com/ru/post/310460/


  1. pilot114
    16.02.2022 08:07
    +5

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


  1. TDen
    16.02.2022 08:51

    " Если бы мне в школе вот так вот... доходчиво... "(с)


  1. Koolio_Tiglesias
    16.02.2022 09:06

    "После этого, я загружу файл с зависимостями директивой"\\

    Попробуйте упростить статью 


  1. mrtippler
    16.02.2022 09:22
    +2

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

    Для меня это новое определение клиент-серверной архитектуры. Я всегда всегда имел о ней несколько иное представление.

    А то, что вы имеете в виду, скорее можно называть кроссплатформенной разработкой. Хоть и с натяжкой, но все же точнее.


    1. momoru_kun Автор
      16.02.2022 10:44

      Спасибо большое!
      Сегодня перепишу. Действительно звучит как-то неоднозначно (в моей голове явно звучало лучше)


    1. pilot114
      16.02.2022 10:52
      +1

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


      1. mrtippler
        16.02.2022 13:18
        +1

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

        Ок. Я просто придрался к точности формулировки. )


        1. momoru_kun Автор
          16.02.2022 14:24

          Да, я тут с вами согласен. Сейчас думаю, как лучше конкретизировать)


  1. videoNik
    16.02.2022 11:07
    +2

    Хорошая статья, надо попробовать

    Мне не хватило описания, что такое dockerfile, физически. Я конечно загуглил и посмотрел в другой статье, но пара предложений в тексте не повредили бы.

    ps совсем юнга


  1. sYB-Tyumen
    16.02.2022 12:53

    К тому же докер легко скалировать - достаточно запустить несколько контейнеров

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


  1. Lezenford
    16.02.2022 12:56
    +3

    Аргумент -p <порт контейнера>/<протокол>:<локальный порт>/<протокол>

    Если мне не именяет память, то наоборот. сначала протокол и порт локальной машины, потом - контейнера


    1. gekt0r
      16.02.2022 15:25

      Не изменяет. Это же и из примера в статье видно. Думаю, автор просто опечатался


      1. Lezenford
        16.02.2022 15:53
        +1

        Нет, в примере он пишет, что хочет пробросить порт сервера 80 на порт 8080 контейнера. последовательность в примере должна быть обратная, т.е.

        -p 80:8080


        1. momoru_kun Автор
          16.02.2022 19:40

          Да, спасибо! Постоянно путаю и в реальной жизни и здесь.
          Забавно, но перепроверил этот момент несколько раз и все-равно напутал xD


          UPD: Поправил и дополнил инфой по бинду на конкретный IP


  1. Stroy71
    16.02.2022 13:57
    +2

    Вопрос, что такое регистр в интерпретации docker?


    1. Lezenford
      16.02.2022 15:31
      +1

      Не самый удачный перевод Registry с английского. Это хранилище образов


  1. HOMPAIN
    17.02.2022 13:08

    Вы создали программу для питона app.py. В каком месте вы её поместили в образ докера? И где вы описали момент запуска программы в докере? Почему нигде нет подобной строки для запуска "python3 app.py"?


    1. momoru_kun Автор
      17.02.2022 14:55

      CMD ["gunicorn", "--bind", "0.0.0.0", "app:app"]

      Потому что код запускается не напрямую через интерпретатор питона, а передается на исполнение gunicorn (WSGI сервер)

      А код был помещен директивой COPY . .


  1. nickolaym
    18.02.2022 07:03

    "Докер - это не виртуальная машина"... и хоба, мы берём образ, содержащий минимальную установку Alpine Linux. Ну и где же этот линукс будет работать? Уж не в виртуальной машине ли?

    Ладно бы, если мы гоняли докер на линуксовом хосте. Но на винде и маке нет ядра линукса!


    1. momoru_kun Автор
      18.02.2022 09:50

      Понятия того на чем строится контейнеризация в containerd (контрольные группы и нэймспейсы) были упущено намеренно в страхе сильно усложнить нагрузить текст. Но вы правы и стоило бы хоть где-то отписать о том, что не на linux-based системах, докер прибегает к виртуализации. Сделаю, спасибо!


  1. Oleg_Med
    18.02.2022 09:50

    Давайте создадим Dockerfile

    Это для меня было не понятно. Из вашего текста я понял что мы будем создавать Dockerfile не как имя файла а как файл для Docker. И далее я ожидал увидеть имя для этого файла, для того чтобы прописать строки ниже, но ничего не нашёл.
    Это немного ввело в ступор и пришлось гуглить чтобы понять что эти строки пишутся в файл с именем "Dockerfile" в той же директории что и созданные ранее файлы.

    Я думаю понятнее будет написать:
    Давайте создадим файл с именем "Dockerfile"

    Я как раз тот полный юнга по Docker для кого эта статья.


    1. momoru_kun Автор
      18.02.2022 09:53

      Спасибо! Дополню текст тем, что Dockerfile - это именно название файла.

      P.S. Кстати, к статье приложен git-репозиторий с проектом и косвенно это объяснено там