Как стать автором
Обновить

Комментарии 33

> Пока скачивается миллион npm-пакетов

А смотрели возможность использовать yarn для установки пакетов? Когда столкнулся с тем что npm i занимает значительную часть pipeline, это позволило заметно оптимизировать процесс по времени.
Вместо `npm install` следует использовать `npm ci` в связке с `package-lock.json`. Отрабатывает гораздо быстрее из-за пропуска этапа анализа зависимостей.
В данном примере я заменил npm на yarn, и время первой сборки уменьшилось до 2m4.134s, что незначительно. Исследования работы с именно нодой не вошли в данную статью, здесь больше речь про работу докера, но тема очень интересная.
yarn не панацея, в разное время он то быстрее, то нет. Если у вас много nodejs — мониторьте изменения в npm и yarn, тестируйте на своих проектах регулярно.
Спасибо за полезный пост. Хорошая выжимка best practices с разъяснениями и иллюстрациями

Хорошая статья! Делал такое для golang, но столкнулся с тем, что при активной разработке go.mod меняется всё равно не редко (особенно если сервисов много). И тут на помощь пришел BuldKit.
С ним становится ещё проще, поскольку можно указать напрямую какие директории можно шарить между разными запусками даже если поменялось что-то из слоёв. Работает как если бы шареная директория была отдельным volume который подключается на этапе сборки, можно указывать политики разделения этой директории.

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


Просто по моим наблюдениям с Go всё и так летает, достаточно кешировать на CI каталоги с родными кешами Go, примерно так (для CircleCI):


- save_cache:
    when: always
    key: v4-{{ checksum "/tmp/tools.ver" }}-{{ checksum "go.mod" }}-{{ .Branch }}
    paths:
        - ~/go/bin/
        - ~/go/pkg/
        - ~/go/src/
        - ~/.cache/go-build/
        - ~/.cache/golangci-lint/

И при наличии этих кешей сборка и тесты Go реально летают, даже на немаленьком проекте.

Да, примерно это и делалось — маунтились кэши в контейнеры.
Из плюсов BuildKit’а — можно явно указать разрешено ли совместное использование и изменение, или нет. Я не рискнул просто маунтить директорию поскольку не уверен в безопасности параллельных сборок в таком случае. С BuildKit можно запускать параллельно, просто результат докачки одного из запусков пропадёт.

НЛО прилетело и опубликовало эту надпись здесь
Спасибо за уточнение!

1.-2. А можно, пожалуйста, ткнуть ссылочками?

3. С учетом п.4 у меня будет папка `node_modules_production` (так ее назовем для понимания). Не совсем понял, что с ней надо сделать. Почему ее просто не оставить в образе (и даже не `COPY` ее, как автор предлагает), она ведь нужна для runtime? И что именно «удалить, чтоб уменьшить слой»?

4. Это понимаю.
Ниже объяснили, что `node_modules_production` не будет копироваться при `COPY`. У нас она в образе не будет и не нужна — всегда чистая `npm ci`.

Остальное актуально.
По пункту №3 — лучше локальный прокси репозитория, Nexus или аналогичный, а не папка с кэшем. Качать с локального же сервера не сильно дольше, зато намного больше гибкость + приватный репозиторий автоматом появляется.

А производительность не просела в результате перехода на alpine?

только сегодня разработчики жаловались, что на сборку проекта уходит полтора часа… полтора часа Карл…
если кто-то Квартус ускорит в сто раз, нужно будет отлить ему памятник в золоте…
сорри… о наболевшем(

Можно ещё вынести процесс сборки Angular-приложения за пределы докера (в команды непосредственно для CI-системы) и кешировать его с помощью механизмов CI-системы; докеру останется только закинуть артефакт в образ Nginx.


Можно не создавать новый образ на основе node:12.16.2-alpine3.11, а использовать его вместе с docker run, чтобы собрать Angular-приложение напрямую в CI-системе. Но обычно CI-системы запускаются в докер-контейнере, и у разработчиков есть возможность выбрать образ контейнера, поэтому нет необходимости запускать отдельно образ с Node.js.


Эти решения выходят за рамки просто докерфайла и реализация может быть завязана на конкретную CI-систему.


P.S. Спасибо за описание поэтапной эволюции решения. Мне как новичку в докере это сильно помогает.

Меняем файл src/index.html — имитируем работу программиста.

Постоянно это делаю)
При добавлении образа с базой, и др необходимых для прода на сколько увеличится сложность сборки?
Спасибо за статью!

Для этого положим в проект файл `.dockerignore` и укажем, что не нужно для сборки: `/node_modules`.

Вот тут я не совсем понял. В сборке есть `npm ci`, `npm run`, неужели им не нужен `/node_modules`? Или он нужен, но мы его копируем с помощью `COPY. .`?
Кажется, я понял. Я не знал, что такое `npm ci`. Полагал, это «Continuous Integration», а не «Clean Install». И `/node_modules` нам был не нужен для сборки на первых шагах статьи потому, что, во-первых, это первые шаги, а, во-вторых, install у нас «clean». Каюсь, надо было сначала прочитать еще раз и подумать.

Но, пока что, остается такой вопрос: почему `/node_modules` надо `COPY`, а не просто оставлять в кэше? Мол, такое копирование быстрее?

Привет! Спасибо за статью! Для новичков она будет полезна. Но стоит и рассказать о подводных камнях. Попробую систематизировать


  1. alpine — уже обсуждали тут на хабре риски связанные с этим.
    https://habr.com/ru/post/486202/
    https://habr.com/ru/post/415513/


  2. docker build --from-cache регулярно ломается, да и кэширование слоев докера не очень надежно — я слышал, что бывают конфликты хэшей.


  3. в вашем пайлплайне, если изменился базовый образ, то эти изменения не будут замечены. Это актуально для процесса разработки, когда ты наследуешься от какого-нибудь неуточненного тега типа python:3 В проде же наоборот лучше стабильность и это ок.


  4. не рассмотрен вопрос, когда COPY стопицот файлов на overlay2 работает медленно.


  5. докер проверяет изменился ли результат сравнением команды. Т.е. строка в докерфайле apk --no-cache --update --virtual build-dependencies add python make g++ не изменяется и докер не увидит изменений, если в репозитории ОС появились новые пакеты, и об этом нужно думать отдельно.



без оптимизации каждая сборка бэкенда занимала 14 минут.

соглашусь, что ДЛЯ РАЗРАБОТКИ это непозволительно долго. Очень долго ждешь изменений. Для выката на продакшен — это ОТЛИЧНО. 14 минут? Да ну — там пока кубернетес прочухается, пока образы зальются… Ну, вы поняли )))

3. Не понял, почему. Это про случай, когда Python задеплоит 3.8.3 вместо 3.8.2? Но почему «эти изменения не будут замечены», разве образ `python:3` не изменится (его хэш и, следовательно, он сам), Docker это не проверит?

4. Спросил у автора, спрошу и у Вас, на всякий случай :) А какой вообще резон включать `node_modules` в `.dockerignore`, а потом — его `COPY`?

Спасибо.
  1. Не понял, почему. Это про случай, когда Python задеплоит 3.8.3 вместо 3.8.2? Но почему «эти изменения не будут замечены», разве образ python:3 не изменится (его хэш и, следовательно, он сам), Docker это не проверит?

Объясню как это работает.


  1. Кэш докера пустой. Он качает с регистри python:3 последней версии. Сборка идет с 3.8.3.
  2. В кэше докера есть python:3 (с 3.8.2), в репозитории уже он изменен (3.8.3). Докер не ИЩЕТ в регистри новую версию. Собирает с python:3 (с питоном 3.8.2).
  3. Собираем с ключом

      --pull                    Always attempt to pull a newer version of the image

Тогда идет проверка новой версии. И скачивается она.


Очень очевидное и понятное поведение, ога.


  1. Спросил у автора, спрошу и у Вас, на всякий случай :) А какой вообще резон включать node_modules в .dockerignore, а потом — его COPY?

Не знаю такого смысла — потому что если что-то включено в .dockerignore, его потом в COPY скопировать уже нельзя )))

3. А, для этого отдельный ключ, `--pull`, спасибо.

4. То есть `COPY. .` вовсе не копирует `node_modules`? (Не ругайтесь, пожалуйста, — я новичок. Да и замысел автора… Там и если копирует — мне логика не до конца ясна, и если нет.) `.dockerignore` влияет не только на набор файлов, с которых считается хэш-сумма, но и на `COPY`?
  1. То есть COPY. . вовсе не копирует node_modules? (Не ругайтесь, пожалуйста, — я новичок. Да и замысел автора… Там и если копирует — мне логика не до конца ясна, и если нет.)

Это не так работает. Осознайте концепцию контекста докера. Как это работает? Докер берет каталог из команды docker build <directory>, исключает из него файлы при помощи .dockerignore и кладет во временный каталог. Далее из временного каталога идет сборка. Соответственно, COPY выбирает файлы из этого временного каталога. COPY . . копирует, получается, все файлы из временного каталога.

docker build --from-cache регулярно ломается

А зачем вообще нужен --from-cache? Ведь если мы сделали docker pull и образы с кешами уже доступны локально, то докер же сам их найдёт… в чём смысл явно ему указывать в каком образе их искать?

Ведь если мы сделали docker pull и образы с кешами уже доступны локально, то докер же сам их найдёт…

потому что это так не работает. Без --from-cache у вас слои не подтягиваются из старой версии образа.

В смысле? Он же при сборке пишет, что берёт их из кеша. Вот, прямо в статье пример:


$ time docker build -t app .
Sending build context to Docker daemon 409MB
Step 1/5 : FROM node:12.16.2
Step 2/5 : WORKDIR /app
 ---> Using cache

Это зависит от фаз луны. Допускаю, что это работает в случае, если


а) изначальный базовый образ остался тот же самый и не изменялся в рамках того же тега
б) окончательный образ имеет тот же самый тег, что и собираемый


Короче — с --from-cache вероятность кэш-промаха СУЩЕСТВЕННО ниже

Судя по официальной документации, дела обстоят несколько иначе. Никакой "вероятности промаха" не существует — по крайней мере я никогда ничего подобного не замечал, если образ есть локально, то он будет использован в качестве кеша всегда автоматически. А --cache-from нужен для того, чтобы не делать docker pull в надежде, что скачанный образ пригодится, вместо этого мы указываем аргументом для --cache-from имя образа в удалённом репозитории, и он на ходу сам выясняет, содержит ли этот образ нужные для кеша слои и скачивает их по необходимости. https://docs.docker.com/engine/reference/commandline/build/#specifying-external-cache-sources


P.S. Помимо экономии на возможно бесполезном docker pull перед сборкой --cache-from должен дать дополнительную экономию на том, что скачает не весь образ с кешем, а только те его слои, которые реально нужны для кеша.

он на ходу сам выясняет, содержит ли этот образ нужные для кеша слои и скачивает их по необходимости.

Ой, как я боюсь этой уличной магии…
Допускаю, что Вы и правы, но я наблюдал странные граничные случаи. Поэтому мне проще сразу docker pull старый образ и не ломать голову )

Следующий этап: начать пользоваться фичами гитлаба и задолбаться с докерфайлом.
Кэш докера — неуправляемое чудовище. Лучше кэшировать зависимости гитлабом. Делается в две строчки, зато есть контроль.
А еще гитлаб позволяет удобно смотреть на этапы сборки. И видеть что упал job с тестами, или job с линтером, или… Но если вы все пихаете в докерфайл, вы отказываетесь от этих фич. Решение — gitkab docker runner. Указываем ему, в каком образе запускать команды, и забываем про докер до самого конца, когда надо именно образ собирать.

Пацанам пофиг — у них все собирается в докерах и в докерах же уезжает в продакшен. Никаких других способов доставки приложения у них нет. Если это какой-нибудь веб, то это ок. Но для "коробочного" софта это не годится — там придется и deb, и rpm билдить и под разные операционные системы и все такое. И тогда я полностью согласен — начинает выгоднее быть пользование кэшем гитлаба, а не все эти сумасшедшие мультистадийные сборки… А докер… А докер в этом случае хорошо подходит для задач "заморозки" окружения для билда и для тестов.

Зарегистрируйтесь на Хабре , чтобы оставить комментарий