Начала Docker для юнги
Опытный капитан, прочитай сперва
Эта статья предназначена для начинающих и является попыткой максимально простым языком ответить на те вопросы, которые либо неочевидно гуглятся, либо описаны более сложным языком. Поэтому прошу оценить статью на простоту восприятия, как если бы вы только начали осваивать докер. Но вообще я буду рад любой критике!
Что такое контейнеризация?
Чтобы понять смысл контейнера сначала стоить обратиться к такой вещи, как образ.
Образ - это шаблон по которому будет создаваться контейнер. Он может хранить в себе целую операционную систему! И именно образы скачивают с именитого 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 - список образов, доступных локально.