Кілька RUN проти одноланцюгових RUN в Dockerfile, що краще?


132

Dockerfile.1виконує кілька RUN:

FROM busybox
RUN echo This is the A > a
RUN echo This is the B > b
RUN echo This is the C > c

Dockerfile.2 приєднується до них:

FROM busybox
RUN echo This is the A > a &&\
    echo This is the B > b &&\
    echo This is the C > c

Кожен RUNстворює шар, тому я завжди вважав, що менше шарів краще і, отже Dockerfile.2, краще.

Це, очевидно, вірно, коли RUNвидалення чогось додано попереднім RUN(тобто yum install nano && yum clean all), але у випадках, коли кожен RUNдодає щось, ми повинні враховувати кілька моментів:

  1. Шари повинні просто додати діфф порівняно з попереднім, тому якщо пізній шар не видалить щось додане в попередньому, між обома методами не повинно бути великої переваги дискового простору ...

  2. Шари витягуються паралельно від Docker Hub, тому Dockerfile.1, хоча вони, мабуть, трохи більші, теоретично завантажуються швидше.

  3. Якщо додати 4-те речення (тобто echo This is the D > d) та перебудувати локально, Dockerfile.1побудував би швидше завдяки кешу, але Dockerfile.2доведеться запустити всі 4 команди заново.

Отже, питання: який кращий спосіб зробити Dockerfile?


1
Не можна відповісти загалом, оскільки це залежить від ситуації та використання зображення (оптимізуйте розмір, швидкість завантаження чи швидкість побудови)
Генрі

Відповіді:


99

Коли це можливо, я завжди зливаю разом команди, які створюють файли, з командами, які видаляють ті самі файли в один RUNрядок. Це пояснюється тим, що кожен RUNрядок додає шару образу, вихід - це буквально зміна файлової системи, яку ви могли переглянути з docker diffтимчасовим контейнером, який він створює. Якщо ви видалите файл, створений в іншому шарі, всі файлові системи об'єднання - це зареєструвати зміну файлової системи на новому шарі, файл все ще існує в попередньому шарі і надсилається через мережевий і зберігається на диску. Отже, якщо ви завантажуєте вихідний код, витягуєте його, компілюєте у двійковий файл, а потім видаляєте файли tgz та вихідні файли наприкінці, ви дійсно хочете, щоб це все було зроблено в один шар, щоб зменшити розмір зображення.

Далі я особисто розбила шари, виходячи з їх потенціалу для повторного використання інших зображень та очікуваного використання кешування. Якщо у мене є 4 зображення, всі з однаковим базовим зображенням (наприклад, debian), я можу перенести колекцію загальних утиліт для більшості цих зображень у команду першого запуску, щоб інші зображення виграли від кешування.

Порядок в Dockerfile важливий при повторному використанні кешу зображень. Я переглядаю будь-які компоненти, які оновлюватимуться дуже рідко, можливо лише тоді, коли базове зображення оновлюється та розміщуватиметься високо в Dockerfile. Під кінець Dockerfile я включаю будь-які команди, які запускаються швидко і можуть часто змінюватися, наприклад, додавання користувача із специфічним для UID користувачам або створення папок та зміна дозволів. Якщо контейнер включає інтерпретований код (наприклад, JavaScript), який активно розробляється, він додається якомога пізніше, щоб відбудова виконувала лише одну зміну.

У кожній із цих груп змін я консолідую якнайкраще, щоб мінімізувати шари. Отже, якщо є 4 різні папки вихідного коду, вони розміщуються всередині однієї папки, щоб вона могла бути додана однією командою. Будь-яка установка пакету з чогось типу apt-get об'єднується в єдиний RUN, коли це можливо, щоб мінімізувати кількість накладних витрат менеджера пакунків (оновлення та очищення).


Оновлення для багатоетапних збірок:

Я набагато менше хвилююся щодо зменшення розміру зображення на не завершальних етапах багатоетапної збірки. Коли ці етапи не позначені тегами та не надсилаються до інших вузлів, ви можете збільшити ймовірність повторного використання кешу, розділивши кожну команду на окремий RUNрядок.

Однак це не ідеальне рішення для розшарування шарів, оскільки все, що ви копіюєте між етапами, - це файли, а не решта метаданих зображень, як-от налаштування змінної середовища, точка входу та команда. І коли ви встановлюєте пакети в дистрибутиві Linux, бібліотеки та інші залежності можуть бути розкидані по всій файловій системі, що ускладнює копіювання всіх залежностей.

Через це я використовую багатоетапні збірки як заміну для створення бінарних файлів на сервері CI / CD, так що мій сервер CI / CD повинен мати лише інструменти для запуску docker build, а не мати jdk, nodejs, go і будь-який інший встановлений інструмент компіляції.


30

Офіційна відповідь, перелічена в їх найкращих практиках (офіційні зображення ОБОВ'ЯЗКОВО дотримуватися цих)

Мінімізуйте кількість шару

Вам потрібно знайти баланс між читабельністю (і, таким чином, довгостроковою ремонтопридатністю) Dockerfile та мінімізацією кількості шарів, які він використовує. Будьте стратегічні та обережні щодо кількості використовуваних шарів.

Оскільки докер 1.10 COPY, ADDі RUNзаяви додають новий шар вашому зображенню. Будьте обережні, використовуючи ці твердження. Спробуйте об'єднати команди в одне RUNтвердження. Розділіть це, лише якщо це потрібно для читабельності.

Більше інформації: https://docs.docker.com/engine/userguide/eng-image/dockerfile_best-practices/#/minimize-the-number-of-layers

Оновлення: Багатоетапність в докер> 17.05

За допомогою багатоетапних збірок ви можете використовувати кілька FROMзаяв у своєму Dockerfile. Кожне FROMтвердження є етапом і може мати власне базове зображення. На останньому етапі ви використовуєте мінімальне базове зображення, наприклад альпійське, копіюєте артефакти збірки з попередніх етапів та встановлюєте вимоги до виконання. Кінцевим результатом цього етапу є ваш образ. Тож тут ви турбуєтесь про шари, як описано раніше.

Як завжди, докер має чудові документи на багатоетапних складах. Ось короткий уривок:

При багатоетапних збірках ви використовуєте декілька операторів FROM у своєму Dockerfile. Кожна інструкція FROM може використовувати іншу базу, і кожна з них починає новий етап складання. Ви можете вибірково копіювати артефакти з однієї стадії на іншу, залишаючи після себе все, чого ви не хочете, в остаточному зображенні.

Відмінна публікація про це можна знайти тут: https://blog.alexellis.io/mutli-stage-docker-builds/

Щоб відповісти на ваші моменти:

  1. Так, шари начебто відрізняються. Я не думаю, що додаються шари, якщо є абсолютно нульові зміни. Проблема полягає в тому, що після встановлення / завантаження чогось у шарі №2 ви не зможете видалити його в шарі №3. Отже, як тільки щось записано в шарі, розмір зображення більше не можна зменшувати, видаляючи його.

  2. Хоча шари можна витягувати паралельно, що робить його потенційно швидшим, кожен шар, безсумнівно, збільшує розмір зображення, навіть якщо вони видаляють файли.

  3. Так, кешування корисно, якщо ви оновлюєте файл докера. Але це працює в одному напрямку. Якщо у вас є 10 шарів, а ви змінюєте шар №6, вам все одно доведеться відновлювати все з шару № 6- # 10. Тож не дуже часто це прискорить процес збирання, але це гарантовано зайве збільшення розміру вашого зображення.


Дякую @Mohan, що нагадав мені оновити цю відповідь.


1
Зараз це застаріло - див. Відповідь нижче.
Мохан

1
@Mohan дякую за нагадування! Я оновив публікацію, щоб допомогти користувачам.
Menzo Wijmenga

19

Здається, відповіді вище застаріли. Примітка Документів:

До Docker 17.05 і навіть більше, перед Docker 1.10, важливо було мінімізувати кількість шарів у вашому зображенні. Наступні вдосконалення зменшили цю потребу:

[...]

Docker 17.05 і новіші версії додають підтримку багатоетапних збірок, які дозволяють копіювати лише необхідні вам артефакти на остаточне зображення. Це дозволяє включати інструменти та інформацію про налагодження у проміжні етапи збирання, не збільшуючи розмір кінцевого зображення.

https://docs.docker.com/engine/userguide/eng-image/dockerfile_best-practices/#minimize-the-number-of-layers

і

Зауважте, що цей приклад також штучно стискає дві команди RUN разом за допомогою оператора Bash &&, щоб уникнути створення додаткового шару в зображенні. Це схильний до відмов і важко підтримувати.

https://docs.docker.com/engine/userguide/eng-image/multistage-build/

Здається, найкраща практика змінилася на використання багатоетапних збірок та збереження Dockerfileчитабельності.


Хоча багатоступеневі побудови здаються хорошим варіантом для збереження рівноваги, фактичне виправлення цього питання настане, коли docker image build --squashваріант вийде за межі експериментального.
Яджо

2
@Yajo - Я скептично ставлюсь до проходження squashекспериментального. Він має багато трюків і має сенс лише перед багатоступеневими побудовами. При багатоетапних побудовах вам потрібно лише оптимізувати завершальний етап, що дуже легко.
Менцо Віджменга

1
@Yajo Щоб розширити це, лише шари на останньому етапі змінюють розмір остаточного зображення. Отже, якщо ви ставите всі свої gubbins для будівельників на більш ранніх етапах і маєте завершальний етап, просто встановіть пакети та скопіюйте файли з попередніх етапів, все прекрасно працює і сквош не потрібен.
Мохан

3

Це залежить від того, який ви будете включати у свої шари зображень.

Ключовий момент - це поділитися якомога більше шарів:

Поганий приклад:

Докерфайл.1

RUN yum install big-package && yum install package1

Докерфайл.2

RUN yum install big-package && yum install package2

Хороший приклад:

Докерфайл.1

RUN yum install big-package
RUN yum install package1

Докерфайл.2

RUN yum install big-package
RUN yum install package2

Ще одна пропозиція - видалення не настільки корисно, лише якщо це відбувається на тому ж самому шарі, що і дія додавання / встановлення.


Чи дійсно ці 2 поділяться RUN yum install big-packageз кеша?
Yajo

Так, вони поділять один і той же шар за умови, що вони починають з однієї бази.
Ондра Жижка
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.