Якщо ви хочете зменшити розмір докер-образу, використовуйте найкращі стандартні практики створення докер-образів.
У цій статті ми розповімо про різні методи оптимізації, які ви можете швидко застосувати, щоб створити якомога менший образ докера. Ми також розглянемо деякі з найкращих інструментів для оптимізації.
Docker як контейнерний рушій дозволяє легко взяти шматок коду і запустити його всередині контейнера. Це допомагає інженерам зібрати всі залежності коду та файли в одному місці, звідки їх можна запустити будь-де, досить швидко та легко.
Вся концепція образів, які можна запускати будь-де, починається з простого конфігураційного файлу, який називається Dockerfile. Спочатку ми додаємо всі інструкції зі збирання, такі як залежності коду, команди та деталі базового образу, у Dockerfile.
Потреба в оптимізації образів Docker
Незважаючи на те, що процес збирання Docker є простим, багато організацій припускаються помилки, створюючи роздуті та не оптимізовані образи.
При типовій розробці програмного забезпечення кожен сервіс має декілька версій/випусків, і кожна версія вимагає більше залежностей, команд та конфігурацій. Це створює проблеми при створенні образів, оскільки той самий код потребує більше часу та ресурсів для створення, перш ніж його можна буде відправити у вигляді контейнера.
Я бачив випадки, коли початковий образ програми починався з 350 МБ, а з часом він збільшувався до більш ніж 1,5 ГБ.
Крім того, встановлюючи небажані бібліотеки, ми збільшуємо ймовірність потенційного ризику для безпеки, збільшуючи простір для атак.
Тому інженери DevOps повинні оптимізувати образи, щоб запобігти роздуванню докерів після збірок програми або майбутніх випусків. Не тільки для робочих середовищ, але й на кожному етапі процесу CI/CD ви повинні здійснювати оптимізацію.
Крім того, з використанням інструментів оркестрування контейнерів, таких як Kubernetes, краще мати образи невеликого розміру, щоб скоротити час на передачу та розгортання.
Як зменшити розмір образу докера?
Якщо ми візьмемо образ контейнера типової програми, він містить базовий образ, залежності/файли/конфіги та сміття (небажане програмне забезпечення).
Отже, все зводиться до того, наскільки ефективно ми можемо керувати цими ресурсами всередині образу контейнера.
Розгляньмо різні усталені методи оптимізації образів Docker. Крім того, ми навели практичні приклади для розуміння оптимізації образів докера в реальному часі.
Ви можете скористатися наведеними в статті прикладами або спробувати методи оптимізації на існуючих Docker-файлах.
Нижче наведено методи, використовуючи які ми можемо оптимізувати образи.
- Використання бездистрибутивних / мінімальних базових образів
- Багатоетапні збірки
- Мінімізація кількості шарів
- Розуміння кешування
- Використання Dockerignore
- Зберігання даних програми поза межами контейнера
Docker-файли для вправ: Весь код програми, Docker-файли та конфіги, використані в цій статті, розміщені в цьому репозиторії Github. Ви можете клонувати його і слідувати інструкціям.
Спосіб 1: Використовуйте мінімальні базові образи
Перш за все, необхідно зосередитися на виборі правильного базового образу з мінімальним розміром.
Одним з таких прикладів є альпійські базові образи alpine. Ці образи можуть мати розмір всього 5.59 МБ. Це означає, що він не лише невеликий, але й максимально безпечний.
alpine latest c059bfaa849c 5.59MB
Базовий образ Nginx alpine займає лише 22 МБ.
За замовчуванням, він постачається з командним рядком sh, що дозволяє під'єднатися до контейнера для налагодження.
Ви можете ще більше зменшити розмір базового образу за допомогою бездистрибутивних образів. Це урізана версія операційної системи. Бездистрибутивні базові образи доступні для java, nodejs, python, Rust тощо.
Бездистрибутивні образи настільки мінімізовані, що в них навіть немає оболонки. Ви можете запитати, як же тоді налагоджувати програми? Для налагодження є версія того ж самого образу, яка постачається з busybox.
Крім того, зараз більшість дистрибутивів мають мінімальні базові образи.
Примітка: Базові образи, що є у відкритому доступі, не можна безпосередньо використовувати у проєктних середовищах. Вам потрібно отримати дозвіл від служби безпеки підприємства на використання базового образу. У деяких організаціях служба безпеки сама публікує базові збірки щомісяця після тестування і сканування на безпеку. Ці образи будуть доступні у загальному приватному сховищі організації.
Спосіб 2: Використання багатоетапних збірок Docker
Шаблон багатоетапної збірки розвинувся з концепції шаблону білдера, де ми використовуємо різні Docker-файли для збірки та пакування коду програми. Хоча цей патерн допомагає зменшити розмір образу, він створює невеликі накладні витрати, коли справа доходить до побудови конвеєрів.
При багатоетапній збірці ми отримуємо ті ж переваги, що і при використанні патерну білдера. У цьому підході ми використовуємо проміжні образи (етапи збірки) для компіляції коду, встановлення залежностей та пакувальних файлів. Ідея полягає в тому, щоб усунути непотрібні шари в образі.
Після цього на інший образ копіюються лише необхідні для запуску програми файли, які містять лише необхідні бібліотеки, тобто полегшують запуск програми. Розглянемо це на практичному прикладі, де ми створимо простий застосунок на Nodejs та оптимізуємо його Docker-файл.
Спочатку створимо код. У нас буде наступна структура каталогів.
├── Dockerfile1
├── Dockerfile2
├── env
├── index.js
└── package.json
Збережіть наступний код як index.js
.
const dotenv=require('dotenv');
dotenv.config({ path: './env' });
dotenv.config();
const express=require("express");
const app=express();
app.get('/',(req,res)=>{
res.send(`Learning to Optimize Docker Images with DevOpsCube!`);
});
app.listen(process.env.PORT,(err)=>{
if(err){
console.log(`Error: ${err.message}`);
}else{
console.log(`Listening on port ${process.env.PORT}`);
}
}
)
Збережіть наступне як package.json
.
{
"name": "nodejs",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"dotenv": "^10.0.0",
"express": "^4.17.2"
}
}
Збережіть наступну змінну з портом у файлі з назвою env
.
PORT=8080
Простий Dockerfile
для цієї програми буде виглядати так - Збережіть його як Dockerfile1
.
FROM node:16
COPY . .
RUN npm install
EXPOSE 3000
CMD [ "node", "index.js" ]
Перевіримо скільки необхідно місця для його збірки.
docker build -t devopscube/node-app:1.0 --no-cache -f Dockerfile1 .
Після завершення збірки. Перевіримо його розмір за допомогою команди:
docker image ls
Ось що ми отримуємо.
devopscube/node-app 1.0 b15397d01cca 22 seconds ago 910MB
Отже, розмір складає 910 МБ.
Тепер використаймо цей метод для створення багатоетапної збірки.
Ми використаємо node:16
як базовий образ, тобто образ для встановлення усіх залежностей та модулів, після чого перемістимо його вміст до мінімального та легшого "alpine" образу. "Alpine" образ має мінімальний набір утиліт, а отже є дуже легким.
Ось графічне зображення багатоетапної збірки Docker.
Крім того, в одному Dockerfile
ви можете мати декілька етапів з різними базовими образами. Наприклад, ви можете мати різні етапи для збірки, тестування, статичного аналізу та па кування з різними базовими образами.
Розглянемо, як може виглядати новий Docker-файл. Ми просто скопіюємо необхідні файли з базового образу в основний.
Збережемо наступний файл як Dockerfile2
.
FROM node:16 as build
WORKDIR /app
COPY package.json index.js env ./
RUN npm install
FROM node:alpine as main
COPY --from=build /app /
EXPOSE 8080
CMD ["index.js"]
Розглянемо, скільки місця для зберігання він потребує. Збираємо командою:
docker build -t devopscube/node-app:2.0 --no-cache -f Dockerfile2 .
Після завершення збірки. Перевіримо його розмір за допомогою
docker image ls
Ось що ми отримуємо.
devopscube/node-app 2.0 fa6ae75da252 32 seconds ago 171MB
Таким чином, новий зменшений розмір образу становить 171 МБ у порівнянні з образом з усіма залежностями.
Тобто оптимізація становить понад 80%!
Втім, якби ми використали той самий базовий образ, що і на етапі збирання, то не побачили б великої різниці.
Ви можете ще сильніше зменшити розмір образу, використовуючи бездистрибутивні образи. Ось той самий Docker-файл з багатоетапною збіркою, в якому замість alpine використовується бездистрибутивний образ Google для nodeJS
.
FROM node:16 as build
WORKDIR /app
COPY package.json index.js env ./
RUN npm install
FROM gcr.io/distroless/nodejs
COPY --from=build /app /
EXPOSE 3000
CMD ["index.js"]
Якщо ви зберете наведений вище Docker-файл, розмір вашого образу становитиме 118 МБ,
devopscube/distroless-node 1.0 302990bc5e76 118MB
Спосіб 3: Мінімізуйте кількість шарів
Образи докера працюють наступним чином - кожна команда RUN, COPY, FROM
в докер-файлі додає новий шар, і кожен шар збільшує час виконання збірки та збільшує вимоги до місця для зберігання образу.
Розгляньмо це на практичному прикладі: створимо образ ubuntu з оновленими та модернізованими бібліотеками, а також деякими необхідними пакетами, такими як vim, net-tools, dnsutils.
Docker-файл для цього буде наступним - Збережіть його як Dockerfile3
.
FROM ubuntu:latest
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update -y
RUN apt-get upgrade -y
RUN apt-get install vim -y
RUN apt-get install net-tools -y
RUN apt-get install dnsutils -y
Ми також хочемо дізнатися час збирання цього образу.
Демон Docker має вбудовану можливість показувати загальний час виконання Docker-файлу.
Щоб увімкнути цю функцію, виконайте наступні кроки
- Створіть файл
daemon.json
з таким вмістом у каталозі/etc/docker/
{
"experimental": true
}
- Щоб увімкнути цю функцію, виконайте наступну команду.
export DOCKER_BUILDKIT=1
- Створюємо його та перевіряємо розмір та час збирання
time docker build -t devopscube/optimize:3.0 --no-cache -f Dockerfile3 .
- Команда
time
виведе час виконання
time docker build -t devopscube/optimize:3.0 --no-cache -f Dockerfile3 .
[+] Building 117.1s (10/10) FINISHED
=> [internal] load build definition from Dockerfile
.
.
.
.
=> => writing image sha256:9601bcac010062c656dacacbc7c554b8ba552c7174f32fdcbd24ff9c7482a805 0.0s
=> => naming to docker.io/devopscube/optimize:3.0 0.0s
real 1m57.219s
user 0m1.062s
sys 0m0.911
Після завершення збірки час виконання складає 117.1 секунди.
Перевіримо його розмір за допомогою команди
docker image ls
Ось що ми отримали.
devopscube/optimize 3.0 9601bcac0100 About a minute ago 227MB
Отже, розмір становить 227 МБ.
Об'єднаємо команди RUN
в один шар і збережемо його як Dockerfile4
.
FROM ubuntu:latest
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update -y && apt-get upgrade -y && apt-get install --no-install-recommends vim net-tools dnsutils -y
У наведеній вище команді RUN
ми використали прапорець --no-install-recommends
для вимкнення рекомендованих пакунків. Це рекомендується робити, коли ви використовуєте install
у ваших Docker-файлах.
Тепер подивимося, який обсяг пам'яті та час потрібно для збірки.
time docker build -t devopscube/optimize:4.0 --no-cache -f Dockerfile4 .
Він буде відображати час виконання в терміналі.
time docker build -t devopscube/optimize:0.4 --no-cache -f Dockerfile4 .
[+] Building 91.7s (6/6) FINISHED
=> [internal] load build definition from Dockerfile2 0.4s
.
.
.
=> => naming to docker.io/devopscube/optimize:4.0 0.0s
real 1m31.874s
user 0m0.884s
sys 0m0.679s
Після завершення збірки - час виконання становить 91.7 секунд.
Перевіримо його розмір за допомогою
docker image ls
Ось що ми отримуємо.
devopscube/optimize 4.0 37d746b976e3 42 seconds ago 216MB
Таким чином, розмір файлу становить 216 МБ.
Використовуючи цю техніку оптимізації, час виконання зменшився з 117,1 с до 91,7 с, а загальний розмір файлу зменшився з 227 МБ до 216 МБ.
Спосіб 4: Розуміння кешування
Часто один і той самий образ доводиться збирати знову і знову з невеликими змінами у коді.
У таких випадках Docker може допомогти, зберігаючи кеш кожного шару збірки, сподіваючись, що він може знадобитися у майбутньому.
Згідно з цією концепцією, рекомендується додавати рядки, які використовуються для встановлення залежностей та пакунків, на початку Docker-файлу - перед командами COPY
.
Причиною цього є те, що докер зможе кешувати образ з необхідними залежностями, і цей кеш можна буде використовувати у наступних збірках, коли код буде змінено.
Для прикладу, розглянемо наступні два докер-файли.
Dockerfile 5
FROM ubuntu:latest
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update -y && \
apt-get upgrade -y && \
apt-get install -y vim net-tools dnsutils
COPY . .
Dockerfile 6
FROM ubuntu:latest
COPY . .
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update -y && \
apt-get upgrade -y && \
apt-get install -y vim net-tools dnsutils
Docker зможе краще використовувати функціонал кешу у Dockerfile5
, ніж у Dockerfile6
, завдяки кращому розташуванню команди COPY
.
Спосіб 5: Використання Dockerignore
Як правило, до образу докера потрібно копіювати лише необхідні файли.
Docker може ігнорувати файли, наявні у робочому каталозі, якщо це налаштовано у файлі .dockerignore
.
Про цю особливість слід пам'ятати під час оптимізації образу докера.
Спосіб 6: Зберігайте дані програми в іншому місці
Зберігання даних програми в образі призведе до надмірного збільшення розміру образів.
Наполегливо рекомендується використовувати функцію volume, щоб відокремити дані від образу контейнера.
Інструменти оптимізації образів Docker
Нижче наведено деякі інструменти з відкритим вихідним кодом, які допоможуть вам оптимізувати образи Docker. Ви можете вибрати інструмент і зробити його частиною конвеєра образів Docker, щоб забезпечити створення лише оптимізованих образів для розгортання програм.
- Dive: Це інструмент для аналізу образів, який допоможе вам виявити шари в образах контейнерів Docker і OCI. Використовуючи Dive, ви можете знайти способи оптимізації ваших образів Docker. Перегляньте репозиторій Dive на Github для більш детальної інформації.
- Docker Slim: допомагає оптимізувати ваші образи Docker для безпеки та розміру. Дізнайтеся більше про Docker Slim у репозиторії Github. За допомогою Slim ви можете зменшити розмір образу докера до 30 разів.
- Docker Squash: Ця утиліта допомагає зменшити розмір образу завдяки стисненню шарів. Функція стиснення також доступна у Docker CLI за допомогою прапорця squash.
Підсумок
Наведені вище методи допоможуть вам створювати оптимізовані образи докерів і писати кращі докер-файли.
Крім того, якщо ви будете дотримуватися всіх стандартних практик роботи з контейнерами, ви зможете зменшити розмір докер-образу, що дасть змогу розгортати легші образи.
Крім того, з погляду DevSecOps, ви можете використовувати інструменти сканування вразливостей з відкритим вихідним кодом, такі як Trivy, для сканування образів Docker
Ще немає коментарів