Docker: образи, Dockerfile і безпека
20 питань · ~20 хв · Версія v3.0
Увійдіть, щоб продовжити
Інструкції Dockerfile, шари й кеш збирання, багатоетапні образи, розмір і базові образи, секрети й сканування вразливостей - питання всіх рівнів, від junior до senior.
- За спробу
- 20
- У пулі
- 54
- Проходжень
- 0
- Середній бал
- -
- Пройшли на 70%+
- -
Питання для підготовки
17 питаньКожна інструкція Dockerfile (RUN, COPY, ADD...) створює шар. Під час повторного збирання Docker перевіряє, чи змінилася інструкція та файли, що в неї входять. Якщо ні - бере шар з кешу. Щойно один шар змінився, усі наступні збираються заново.
Погано:
COPY . /var/www
RUN composer install --no-dev
Будь-яка зміна в коді (навіть README) інвалідує COPY ., і composer install виконується щоразу - хвилини на кожне збирання.
Добре: спершу те, що змінюється рідко, потім - часто:
COPY composer.json composer.lock /var/www/
RUN composer install --no-dev --no-scripts --no-autoloader
COPY . /var/www
RUN composer dump-autoload --optimize
Тепер залежності перевстановлюються лише при зміні composer.json/composer.lock, а зміна коду перебудовує тільки останні шари.
Ще правила:
.dockerignore- щоб у контекст збирання не потраплялиvendor/,node_modules/,.git/,.env, логи. ІнакшеCOPY .інвалідується через файли, що до застосунку не мають стосунку, а секрети можуть потрапити в образ.- Встановлення пакетів ОС - на початку, одним
RUN, разом з очищенням кешу менеджера пакетів. - Кеш-монтування BuildKit зберігає кеш Composer чи npm між збираннями навіть після інвалідації шару:
RUN --mount=type=cache,target=/root/.composer/cache composer install
- У CI кеш зникає з кожною новою машиною - його експортують у реєстр (
--cache-to/--cache-from).
Обидві інструкції додають файли в образ, але ADD робить більше, і саме це робить його менш передбачуваним.
COPY - просто копіює файли й каталоги з контексту збирання (або з іншої стадії --from=):
COPY composer.json composer.lock ./
COPY --chown=www-data:www-data . /var/www/html
ADD уміє ще й:
- завантажувати файли за URL:
ADD https://example.com/app.tar.gz /tmp/; - автоматично розпаковувати локальні архіви (
.tar,.tar.gz,.tar.xz) у каталог призначення; - клонувати Git-репозиторії:
ADD https://github.com/org/repo.git#v1.2 /src.
Чому за замовчуванням - COPY:
- передбачуваність:
COPY archive.tar.gz /app/копіює архів як файл.ADDз тим самим рядком розпакує його - поведінка залежить від типу файлу; - кеш: для URL Docker мусить завантажити файл, щоб перевірити, чи він змінився;
- безпека: завантаження з інтернету без перевірки вмісту - ризик ланцюжка постачання.
Коли ADD доречний:
- розпакувати локальний архів одним кроком (наприклад, бінарник з архіву поруч із Dockerfile);
- завантажити файл із перевіркою контрольної суми - сучасний
ADDпідтримує--checksum=sha256:..., тож вміст гарантовано той, що очікувався; - Git-репозиторій як джерело без окремого
git cloneв образі.
Корисні параметри обох інструкцій:
--chown=user:group- власник файлів одразу при копіюванні. ОкремийRUN chown -RпісляCOPYстворює ще один шар з копією всіх файлів - образ подвоюється в розмірі;--chmod=755- права доступу;--link- копіювання незалежним шаром, який не інвалідується при зміні попередніх шарів (корисно для фінальних стадій).
Що потрапляє в контекст визначає .dockerignore - без нього COPY . . скопіює .git, node_modules, .env і локальні логи.
Правило Docker Best Practices: COPY для локальних файлів; ADD - лише коли потрібні його додаткові можливості, і то свідомо.
- Образ (image) - незмінний шаблон: файлова система з усім потрібним (ОС-шар, PHP, розширення, код, залежності) і метадані - яку команду запускати, які порти слухати. Збирається з
Dockerfile, зберігається в реєстрі (Docker Hub, GHCR). - Контейнер - запущений екземпляр образу: ізольований процес з власною файловою системою, мережею й обмеженнями ресурсів.
Аналогія: образ - як клас, контейнер - як об'єкт. З одного образу можна запустити скільки завгодно контейнерів.
docker build -t myapp:1.4 . # зібрати образ
docker run -d --name web myapp:1.4 # запустити контейнер
docker ps # запущені контейнери
docker images # локальні образи
Шари. Образ складається з шарів тільки для читання - по одному на інструкцію Dockerfile, що змінює файли. Контейнер додає зверху тонкий записуваний шар. Тому:
- десять контейнерів з одного образу ділять його шари й не займають десятикратно більше місця;
- зміни, зроблені всередині контейнера, зникають разом із ним. Дані, що мають жити довше (база, завантажені файли), зберігають у томах (volumes).
Практичне правило: контейнер має бути «одноразовим» - його можна знищити й створити заново з того ж образу без втрати даних. Змінити застосунок - це зібрати новий образ, а не зайти в контейнер і поправити файл.
Multi-stage build - кілька етапів FROM в одному Dockerfile. Проміжні етапи містять інструменти для збирання, а фінальний образ копіює з них лише результат.
# Етап 1: фронтенд
FROM node:22-alpine AS assets
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
# Етап 2: залежності PHP
FROM composer:2 AS vendor
WORKDIR /app
COPY composer.json composer.lock ./
RUN composer install --no-dev --no-scripts --prefer-dist
# Етап 3: фінальний образ
FROM php:8.4-fpm-alpine
WORKDIR /var/www
COPY --from=vendor /app/vendor ./vendor
COPY --from=assets /app/public/build ./public/build
COPY . .
Що це дає:
- Менший образ. У фінальному немає Node.js,
node_modules, Composer, компіляторів - лише те, що потрібно для роботи. Різниця буває в сотні мегабайтів. - Менша поверхня атаки. Менше програм - менше вразливостей і менше інструментів для нападника, який потрапив у контейнер.
- Паралельне збирання. BuildKit збирає незалежні етапи одночасно, а невикористані пропускає.
- Один Dockerfile для всього: окремий етап
devз Xdebug і інструментами,testдля CI,production- збирати потрібний через--target.
docker build --target production -t myapp .
Пастка: копіюючи з етапу збирання, легко забути щось потрібне під час роботи (розширення PHP, системну бібліотеку). Перевіряйте фінальний образ запуском тестів чи smoke-тестом саме в ньому.
Обидві інструкції задають, що запускається в контейнері, але по-різному поводяться з аргументами docker run:
CMD- команда за замовчуванням. Якщо при запуску передати команду, вона повністю замінитьCMD.ENTRYPOINT- основний виконуваний файл. Аргументиdocker run(абоCMD) дописуються до нього як параметри.
ENTRYPOINT ["php", "artisan"]
CMD ["serve", "--host=0.0.0.0"]
docker run myapp # php artisan serve --host=0.0.0.0
docker run myapp queue:work # php artisan queue:work
docker run myapp migrate --force # php artisan migrate --force
Типовий сценарій - скрипт-обгортка як ENTRYPOINT, що готує середовище й передає керування команді:
#!/bin/sh
set -e
php artisan config:cache
exec "$@" # запустити CMD - і зробити його PID 1
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
CMD ["php-fpm"]
Exec-форма проти shell-форми:
CMD ["php-fpm"](JSON-масив, exec-форма) - процес запускається напряму й отримує сигнали.CMD php-fpm(shell-форма) - запускається через/bin/sh -c, і PID 1 - це оболонка. СигналSIGTERMприdocker stopотримує shell, а не застосунок, тож контейнер не завершується коректно і через 10 секунд його вбиваютьSIGKILL.
Тому в обох інструкціях - exec-форма, а в скрипті-обгортці - exec "$@".
ENTRYPOINT можна перевизначити при запуску: docker run --entrypoint sh myapp.
Спершу почитати
Прочитати - ще не значить знати
20 питань, по одному на екран, ~20 хв. Після завершення - розбір кожної помилки з посиланням на питання.