В комментариях к моей статье Docker: вредные советы было много просьб объяснить, чем так ужасен описанный в ней Dockerfile. Краткое содержание предыдущей серии: два разработчика в жестком дедлайне составляют Dockerfile. В процессе к ним заходит Ops Игорь Иванович. Итоговый Dockerfile плох настолько, что ИИ оказывается на грани инфаркта.  Сейчас разберемся, что не так с этим Dockerfile. Итак, прошла неделя. Dev Петя встречается в столовой за чашкой кофе с Ops Игорем Ивановичем. П: Игорь Иванович, вы сильно заняты? Хотелось бы разобраться, где мы напортачили. ИИ: Это хорошо, не часто встретишь разработчиков, которых интересует эксплуатация.
Для начала давай договоримся о некоторых вещах: П: А почему в одном контейнере должен быть один процесс?  ИИ: Docker при запуске контейнера отслеживает состояние процесса с pid 1. Если процесс умирает, Docker пытается перезапустить контейнер. Допустим, у вас в контейнере запущено несколько приложений или основное приложение запущено не с pid 1. Если процесс умрет, Docker об этом не узнает. Если больше нет вопросов, показывай ваш Dockerfile. И Петя показал: ИИ: Ох, давай разбираться по порядку. Начнем с первой строчки: Вы берете тэг latest. Использование тэга latest приводит к непредсказуемым последствиям. Представьте, мейнтейнер образа собирает новую версию образа с другим списком ПО, этот образ получает тэг latest. И ваш контейнер в лучшем случае перестает собираться, а в худшем вы ловите баги, которых ранее не было. Вы берете образ с полноценной ОС с большим количеством ненужного ПО, что раздувает объем контейнера. И чем больше ПО, тем больше дырок и уязвимостей. Вдобавок чем больше образ, тем больше он занимает места на хосте и в registry (ты же где-то хранишь образы)? П: Да, конечно, у нас registry, вы же его и настраивали. ИИ: Так, о чем это я?.. Ах да, объемы… Так же растет нагрузка на сеть. Для единичного образа это незаметно, но когда идет непрерывная сборка, тесты и деплой, это ощутимо. А если у тебя нет God’s mode на AWS, тебе еще и космический счет прилетит. Поэтому нужно выбирать наиболее подходящий образ, с точной версией и минимумом ПО. Например, возьми: FROM ruby:2.5.5-stretch П: О, понятно. А как и где посмотреть имеющиеся образы? Как понять, какой мне нужен? ИИ: Обычно образы берутся с докерхаба, не путай с порнхабом :). Для образа обычно существует несколько сборок:
Alpine: образы собраны на минималистичном образе Linux, всего 5 Мб. Его минус: он собран с собственной реализацией libc, стандартные пакеты в нем не работают. На поиск и установку нужного пакета уйдет немало времени.
Scratch: базовый образ, не используется для сборки других образов. Он предназначен исключительно для запуска бинарных, подготовленных данных. Идеально подойдет для запуска бинарных приложений, которые включают в себя все необходимое, например go-приложения.
На базе какой-либо ОС, например Ubuntu или Debian. Ну тут, думаю, пояснять не надо. ИИ: Теперь нам нужно поставить все доп. пакеты и почистить за собой кэши. И сразу можно выкинуть apt-get upgrade. Иначе при каждой сборке, несмотря на фиксированный тэг базового имиджа, будут получаться разные образы. Обновление пакетов в образе — это задача мейнтейнера, она сопровождается изменением тэга. П: Да, я пробовал это сделать, у меня получилось так: ИИ: Неплохо, но тут тоже есть, над чем поработать. Смотри, вот эта команда: … не удаляет данные из итогового образа, а лишь создает дополнительный слой без этих данных. Правильно так: Но это еще не все. Что у вас там, Ruby? Тогда не надо в начале копировать весь проект. Достаточно скопировать Gemfile и Gemfile.lock.  При таком подходе bundle install не будет выполняться на каждое изменение исходников, а только если изменился Gemfile или Gemfile.lock. Те же методы работают и для других языков с менеджером зависимостей, таких как npm, pip, composer и других базирующихся на файле со списком зависимостей. Ну и наконец, помнишь, в начале я говорил про идеологию Docker «один контейнер — один процесс»? Это означает, что supervisor не нужен. Так же не стоит устанавливать systemd, по тем же причинам. По сути, Docker сам является supervisor. И когда ты пытаешься запускать в нем несколько процессов, это как в одном процессе supervisor запускать несколько приложений.
При сборке вы сделаете единый образ, а потом запустите нужное количество контейнеров, чтобы в каждом работал один процесс. Но об этом позже. П: Кажется, понял. Смотрите, что получается: А запуск демонов переопределим при запуске контейнера? ИИ: Да, все верно. Кстати, можно использовать как CMD так и ENTRYPOINT. А разобраться, в чем отличие, это тебе домашнее задание. На эту тему на Хабре есть хорошая статья. Так, давай дальше. Ты качаешь файл для установки node, но при этом нет никакой гарантии, что в нем будет то, что тебе нужно. Надо добавить валидацию. Например, так: По контрольной сумме ты сможешь проверить, что скачал верный файл. П: Но если файл изменится, то сборка не пройдет. ИИ: Да, и как ни странно, это тоже плюс. Ты узнаешь, что файл изменился, и сможешь посмотреть, что там поменяли. Мало ли, добавили, скажем, скрипт, который удаляет все, до чего дотянется, или делает бэкдор. П: Спасибо. Получается, итоговый Dockerfile будет выглядеть так: П: Игорь Иванович, спасибо за помощь. Мне уже пора бежать, надо еще 10 коммитов за сегодня сделать. Игорь Иванович, взглядом остановив торопливого коллегу, делает глоток крепкого кофе. Поразмыслив несколько секунд об SLA 99.9% и коде без багов, он задает вопрос. ИИ: А где вы логи храните? П: Конечно, в production.log. Кстати да, а как мы без ssh получим к ним доступ? ИИ: Если вы их оставите в файлах, решение для вас уже придумали. Команда docker exec позволяет исполнить любую команду в контейнере. Например, вы можете сделать cat для логов. А использовав ключ -it и запустив bash (если он установлен в контейнере), вы получите интерактивный доступ к контейнеру. Но хранить логи в файлах не стоит. Как минимум это приводит к бесконтрольному росту контейнера, логи же никто не ротирует. Все логи нужно кидать в stdout. Там их уже можно посмотреть с помощью команды docker logs. П: Игорь Иванович, а может вынести логи в смонтированную директорию, на физическую ноду, как данные пользователей? ИИ: Хорошо, что вы не забыли вынести данные, загруженные на диск ноды. С логами так тоже можно, только не забудь настроить ротирование.
Все, можешь бежать. П: Игорь Иванович, а посоветуйте, что почитать? ИИ: Для начала прочитай рекомендации от разработчиков Docker, вряд ли кто-то знает Docker лучше них. А если хочешь пройти практику, сходи на интенсив. Ведь теория без практики мертва. Ведущий инженер