Картофельные лонгриды

Назад, к списку

Бухта Docker

Привет, Картофельный Олимпийский!

Решил начать развеивать туман Frontend-войны с, пожалуй, самого популярного и часто используемого инструмента — докера. Для прочтения нужно уметь хотя бы чуть-чуть пользоваться терминалом. Если вы запускали хоть раз npm start — поздравляю, этого хватит.

Какую проблему решает?

Делает развертывание нашего приложения простым и декларативным. Для того, чтобы поднять мини-линукс с нужными файлами для запуска чего угодно, нужно написать несколько строчек практически английским языком и написать пару команд в консоль. А ещё запускаемое приложение работает одинаково везде, где можно запустить Docker.

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

Как решает?

Давайте возведем проблему в абсолют: представьте, что вы написали программу на C, которая адекватно собирается и работает только под Arch Linux. Я с MacOS скорее забью на поиск способов запустить его. Если не знаю про докер.

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

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

Из чего состоит?

Для того, чтобы ПОСТИЧЬ докер, нужно знать как минимум про три вещи:

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

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

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

Как работать?

Для начала докер надо установить. В результате мы добавляем на свой ПК две вещи: ДЕМОН ДОКЕРА (docker daemon), который на самом деле и не злой, а просто фоновый процесс, умееющий наши контейнеры собирать и запускать (и не только); и клиент, который с этим демоном общается. Про то, как работать с ним из командной строки, узнаем по ходу дела, а пока давайте начнем с докерфайла.

Я тут накидал совсем простенькое node+express приложение, в котором всё должно быть понятно:

Мы здесь просто слушаем 3000 порт и отвечаем на каждый запрос приветом. Как бы мы запускали этот проект руками?

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

В целом, читая его как английский текст, уже можно ухватить, что он примерно делает 👀 Но всё же давайте разберем каждую строчку поподробнее:

FROM node:16

Говорит, что мы начинаем ИЗ (FROM) базового образа ноды. Да, я немного недоговорил, что мы можем собирать образ не только из «чистой» ОС, но и на основе другого, уже собранного образа.

Посмотреть все доступные образы, на которых мы можем базировать свой, можно на Docker Hub. В данном случае в качестве отправной точки мы используем ОС Debian с уже установленной нодой.

COPY . .

КОПИРУЕМ (COPY) директорию, в которой мы находимся на нашем ПК, в директорию, в которой мы находимся в образе. Обычно это home (~) директория, но мы можем её сменить с помощью WORKDIR.

Если мы уже выполняли npm i вне докера, то этой командой мы перенесем внутрь и node_modules. Чтобы делать совсем-совсем по честному повторяемые окружения, стоит такого избегать. Для этого в файл .dockerignore рядом с нашим Dockerfile пишем всё, что копировать не нужно, прямо как в gitignore. В нашем случае положим туда одну строчку: /node_modules .

RUN npm ci

Тут всё ясно: ВЫПОЛНЯЕМ (RUN) внутри образа команду npm ci и устанавливаем зависимости.

CMD [”node”, “index.js”]

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

Вместо CMD можно использовать ENTRYPOINT, а зачем и как — можно почитать в этой статье.

Собираем образ

Окей, мы описали образ, который нам нужен, давайте теперь его соберем! Для этого в терминале запустим команду docker build . Точка здесь — это папка, где докер будет искать докерфайл. В правильных терминах её называют контекстом сборки.

В конце мы должны получить сообщение writing image sha256:1f495ea2c5ce76888b8c2362ce41736c0c63241b3c0a09b69d861abbbb7a95a0 . Так из докерфайлов и рождаются образы.

Правда, по умолчанию докер никак их не называет — чтобы их запустить, мы должны постоянно упоминать огромный sha256 хеш. Эту проблему можно решить, присвоив образу тег. Задается он при помощи флага -t: docker build -t hello-docker . . Теперь последней строчкой логов мы должны увидеть naming to docker.io/library/hello-docker.

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

Запускаем контейнер

Запустить контейнер ещё проще — просто пишем docker run hello-docker, видим что приложение запустилось, идём дергать localhost:3000 в браузере иии....

По умолчанию докер не пробрасывает порты от контейнера к нашему ПК — небезопасно это, да и не ясно, какой из портов нужен. Чтобы это исправить, нужно вручную опубликовать нужный порт: docker run -p 3000:3000 hello-docker

Сработало!

Теперь мы знаем, как заворачивать в контейнеры наши приложения, но при чем тут фронтенд? Развернутое фронтендовое приложение — это что-то, что отдает собранные статические файлы. Мы можем запускать внутри контейнера, например, react/angular dev server буквально тем же докерфайлом, которым мы собирали наш простенький бекенд. Но dev-сервера на то и dev-, что хорошо подходят для разработчика и плохо для прода — слишком они медленные и небезопасные.

Хорошо для отдачи статики подходят специальные сервера, например, nginx. И вот их изолировать в контейнеры одно удовольствие: из-за хорошей безопасности докера можно не переживать, что в результате взлома nginx злоумышленники получат доступ ко всему, что есть на вашем ПК. Их, вместе с nginx, докер изолирует в контейнере. Как раз об этом расскажу в следующей части.

Рецепты

Бонусом расскажу ещё о нескольких часто используемых командах Docker клиента:

docker run -d ...

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

docker ps

Показывает все контейнеры, которые запущены в данный момент, их занятые порты и названия.

Ещё его можно запустить с флагом -a и посмотреть на все мёртвые контейнеры, которые у вас остались:

docker logs

Позволяет посмотреть последние логи из контейнера по его имени:

docker stop

Останавливает запущенный контейнер

docker exec -it /bin/sh

Позволяет зайти внутрь контейнера и выполнять команды внутри него

[sudo] docker system prune

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