Офіційні найкращі практики для Dockerfile містять багато чудового матеріалу про те, як покращити свої Dockerfile.
Головно варто оптимізувати саме швидкодію (особливо для тест-раннерів). Це гарантує, що наш інструментарій працюватиме якомога швидше і не вичерпає ліміт часу.
Часте вимірювання часу виконання - чудовий спосіб відчути швидкодію інструментарію. Візьмімо за звичку вимірювати час виконання і після і перед зміною. Навіть коли здається, що зміна «точно» покращить швидкодію, час виконання все одно варто виміряти.
Коли можливо, створюймо скрипти для автоматичного вимірювання швидкодії (це ще називають бенчмаркінгом). Дуже корисний інструмент командного рядка - це hyperfine, але вільно використовуймо той, що найкраще пасує для нашого інструментарію.
Новіші репозиторії інструментарію треків матимуть доступ до таких двох скриптів:
./bin/benchmark.sh: вимірювання швидкодії коду інструментарію треку (джерельний код)./bin/benchmark-in-docker.sh: вимірювання швидкодії образу Docker інструментарію треку (джерельний код)Якщо ми працюємо над репозиторієм інструментарію треку, у якому цих файлів немає, їх можна вільно скопіювати до себе в репозиторій за наведеними вище посиланнями на джерельний код.
Скрипти бенчмаркінгу допомагають оцінити швидкодію інструментарію. Але маймо на увазі, що швидкодія на продакшн-серверах Exercism часто нижча.
Спробуймо поекспериментувати з різними базовими образами (наприклад, Alpine замість Ubuntu), щоб побачити, чи один (суттєво) перевершує інший. Якщо швидкодія приблизно однакова, берімо найменший образ.
Перевірмо, чи використання мережі internal замість none покращує швидкодію.
Більше інформації в документації про мережі.
Інструментарій треку запускає одноразовий, недовговічний контейнер Docker, який виконує такі кроки.
Тому код, який виконується на кроці 2, виконується під час кожного запуску інструментарію. Тож скорочення обсягу коду, який виконується на кроці 2, - чудовий спосіб покращити швидкодію. Один зі способів це зробити - перенести код із часу виконання до часу збірки. Тоді як код часу виконання виконується під час кожного запуску інструментарію, код часу збірки виконується лише один раз (коли збирається образ Docker).
Код часу збірки виконується один раз у складі робочого процесу GitHub Actions. Тож не страшно, якщо код, який виконується під час збірки, буде (відносно) повільним.
Коли тест-раннер для Haskell запускає тести, потрібно, щоб деякі базові бібліотеки були скомпільовані. Оскільки кожен запуск тестів відбувається в новому контейнері, це означає, що компіляція виконувалася під час кожного запуску тестів! Щоб обійти це, Dockerfile тест-раннера для Haskell містить такі дві команди:
COPY pre-compiled/ .
RUN stack build --resolver lts-20.18 --no-terminal --test --no-run-tests
Спочатку каталог pre-compiled копіюється в образ.
Цей каталог налаштовано як тестову вправу, і він залежить від тих самих базових бібліотек, від яких залежить справжня вправа.
Потім ми запускаємо тести для цього каталогу, що схоже на запуск тестів для справжньої вправи.
Запуск тестів призведе до компіляції базових бібліотек, але різниця в тому, що це відбувається під час збірки.
Отже, в отриманому образі Docker базові бібліотеки вже скомпільовані.
Це означає, що компіляція не потрібна під час виконання, що дає (значно) швидше виконання.
Деякі мови дають змогу компілювати код заздалегідь або на льоту. Це компроміс між часом збірки та часом виконання, і знову ж, з міркувань швидкодії ми віддаємо перевагу виконанню під час збірки.
Dockerfile тест-раннера для C# використовує цей підхід: тест-раннер компілюється в бінарний файл заздалегідь (під час збірки), а не компілює код на льоту (під час виконання). Це означає, що під час виконання залишається менше роботи, що має допомогти підвищити швидкодію.
Варто намагатися зменшити розмір образу, завдяки чому він:
Різні образи дистрибутивів матимуть різний розмір.
Наприклад, образ alpine:3.20.2 у десять разів менший за образ ubuntu:24.10:
REPOSITORY TAG SIZE
alpine 3.20.2 8.83MB
ubuntu 24.10 101MB
Загалом образи на основі Alpine - серед найменших, тож багато образів інструментарію базуються на Alpine.
Деякі образи мають спеціальні «slim»-варіанти, у яких частину можливостей прибрано, що дає менший розмір образу.
Наприклад, образ node:20.16.0-slim у пʼять разів менший за образ node:20.16.0:
REPOSITORY TAG SIZE
node 20.16.0 1.09GB
node 20.16.0-slim 219MB
«Slim»-варіанти менші, бо мають менше можливостей. Можливо, образу не потрібні додаткові можливості, і якщо так, розгляньмо «slim»-варіант.
Очевидний, але чудовий спосіб зменшити розмір образу - вилучити все, що не потрібне. Це може бути, наприклад:
Більшості образів Docker потрібно встановлювати додаткові пакети, що зазвичай робиться через менеджер пакетів. Ці пакети потрібно встановлювати під час збірки (бо під час виконання немає доступу до інтернету). Тож будь-які кешувальні чи службові файли менеджера пакетів слід вилучати після встановлення додаткових пакетів.
Дистрибутиви, які використовують менеджер пакетів apk (наприклад, Alpine), мають застосовувати прапорець --no-cache під час встановлення пакетів командою apk add:
RUN apk add --no-cache curl
Дистрибутиви, які використовують менеджер пакетів apt-get/apk (наприклад, Ubuntu), мають виконати команди apt-get autoremove -y та rm -rf /var/lib/apt/lists/* після встановлення пакетів і в тій самій команді RUN:
RUN apt-get update && \
apt-get install curl -y && \
apt-get autoremove -y && \
rm -rf /var/lib/apt/lists/*
У Docker є можливість під назвою багатоетапне збирання. Вони дають змогу розбити Dockerfile на окремі етапи, причому до отриманого образу Docker потрапляє лише останній етап (решта існує тільки для підтримки збирання останнього етапу). Кожен етап можна уявляти як власний міні-Dockerfile; етапи можуть використовувати різні базові образи.
Багатоетапне збирання особливо корисне, коли Dockerfile потребує встановлення пакетів, потрібних лише під час збірки. У цій ситуації загальна структура Dockerfile має такий вигляд:
За такого налаштування додаткові пакети встановлюються лише в етапі «build», а не в етапі «runtime», тож вони не потраплять до образу Docker, який отримується.
Тест-раннеру для Fortran потрібен curl, щоб завантажити деякі файли.
Однак його образ часу виконання не потребує curl, що робить це чудовим прикладом для багатоетапного збирання.
Спочатку його Dockerfile визначає етап (з назвою «build»), у якому встановлюється пакет curl.
Потім він за допомогою curl завантажує файли в цей етап.
FROM alpine:3.15 AS build
RUN apk add --no-cache curl
WORKDIR /opt/test-runner
COPY bust_cache .
WORKDIR /opt/test-runner/testlib
RUN curl -R -O https://raw.githubusercontent.com/exercism/fortran/main/testlib/CMakeLists.txt
RUN curl -R -O https://raw.githubusercontent.com/exercism/fortran/main/testlib/TesterMain.f90
WORKDIR /opt/test-runner
RUN curl -R -O https://raw.githubusercontent.com/exercism/fortran/main/config/CMakeLists.txt
Друга частина Dockerfile визначає новий етап і копіює завантажені файли з етапу «build» до власного етапу командою COPY:
FROM alpine:3.15
RUN apk add --no-cache coreutils jq gfortran libc-dev cmake make
WORKDIR /opt/test-runner
COPY --from=build /opt/test-runner/ .
COPY . .
ENTRYPOINT ["/opt/test-runner/bin/run.sh"]
Тест-раннеру для Ruby потрібно, щоб перед встановленням його необхідних бібліотек (gem-ів) були встановлені пакети git, openssh, build-base, gcc і wget.
Його Dockerfile починається з етапу (з назвою build), який встановлює ці пакети (через apk add), а потім встановлює залежності (через bundle install):
FROM ruby:3.2.2-alpine3.18 AS build
RUN apk update && apk upgrade && \
apk add --no-cache git openssh build-base gcc wget git
COPY Gemfile Gemfile.lock .
RUN gem install bundler:2.4.18 && \
bundle config set without 'development test' && \
bundle install
Потім він визначає етап, який сформує отриманий образ Docker.
Цей етап не встановлює залежності, які встановлював попередній етап: натомість він командою COPY копіює встановлені бібліотеки з етапу збірки до власного етапу:
FROM ruby:3.2.2-alpine3.18
RUN apk add --no-cache bash
WORKDIR /opt/test-runner
COPY --from=build /usr/local/bundle /usr/local/bundle
COPY . .
ENTRYPOINT [ "sh", "/opt/test-runner/bin/run.sh" ]
Dockerfile тест-раннера для C# робить щось подібне, лише тут етап збірки може скористатися наявним образом Docker, у якому вже встановлено додаткові пакети, потрібні для встановлення бібліотек.
Модульні тести бувають дуже корисними, але радимо зосередитися на написанні інтеграційних тестів. Їхня головна перевага в тому, що вони краще перевіряють, як інструментарій працює в продакшені, і тим самим підвищують довіру до реалізації інструментарію.
Щоб якнайкраще відтворити продакшн-середовище, інтеграційні тести мають запускати інструментарій так само, як у продакшн-середовищі. Це означає зібрати образ Docker, а потім запустити зібраний образ на рішенні, щоб перевірити його вихідні дані.
Інтеграційні тести слід визначати як golden-тести, тобто тести, у яких очікувані вихідні дані зберігаються у файлі. Це ідеально підходить для інтеграційних тестів інструментарію треку, адже вихідні дані інструментарію - це теж файли.
Коли тест-раннер запускається на рішенні, на виході отримуємо файл results.json.
Тоді ми можемо порівняти цей файл із «еталонним» (тобто «очікуваним») файлом вихідних даних (з назвою expected_results.json), щоб перевірити, чи тест-раннер працює як задумано.
Безпека - головна причина, чому ми використовуємо контейнери Docker для запуску інструментарію.
На Docker Hub є багато образів Docker, але намагаймося використовувати офіційні. Ці образи ретельно відібрані, і ймовірність, що вони небезпечні, (набагато) менша.
Щоб збирання було стабільним (тобто не ламалося раптово), базові образи завжди варто фіксувати на конкретних тегах. Це означає, що замість:
FROM alpine:latest
варто використовувати:
FROM alpine:3.20.2
З останнім варіантом збирання завжди використовуватиме ту саму версію.
Типово багато образів запускаються від імені користувача з правами root. Варто розглянути запуск як непривілейований користувач.
FROM alpine
RUN groupadd -r myuser && useradd -r -g myuser myuser
# RUN <COMMANDS THAT REQUIRE ROOT USER, E.G. INSTALLING PACKAGES>
USER myuser
(Майже) завжди добре встановлювати найновіші версії
RUN apt-get update && \
apt-get install curl
Радимо писати Dockerfile-и з розрахунку на файлову систему лише для читання. Єдині каталоги, які можна вважати доступними для запису:
/tmp
Наше продакшн-середовище наразі не вимагає файлової системи лише для читання, але в майбутньому може. Тому базовий шаблон нового тест-раннера, аналізатора чи репрезентера починається з файлової системи лише для читання. Якщо не вдається змусити все працювати з файловою системою лише для читання, можна (поки що) вважати файлову систему доступною для запису.