Docker-основы: образы, слои, Dockerfile
Что нужно знать перед уроком
- M1.02 — Deployment и ReplicaSet — нужно понимать, что такое container image как единица деплоя в Pod
Что нужно знать перед уроком
M1 урок 1 — Pod запускает контейнеры из готовых образов; там мы использовали чужой готовый образ excalidraw/excalidraw. В этом уроке разбираемся, как такой образ появляется вообще — фундамент, без которого непонятен весь дальнейший werf.yaml (M3 уроки 2–7).
Теория
Образ — не файл, а слоёная файловая система
Container image — это не единый архив, а последовательность слоёв (layers), каждый — результат одной инструкции Dockerfile, наложенных друг на друга через union-файловую систему (overlayfs в современном containerd/Docker). Каждый слой — read-only после сборки; при запуске контейнера сверху добавляется единственный read-write слой (thin writable layer), в который пишутся все runtime-изменения. Именно поэтому:
- удаление файла в новом слое не уменьшает физический размер образа — файл просто помечается «удалённым» (
whiteout-файл), нижний слой остаётся на диске; - одинаковые слои между разными образами (например, общий базовый
ubuntu:24.04) физически хранятся на диске один раз — это называется copy-on-write и лежит в основе экономии дискового пространства при множестве образов на одном узле; - порядок инструкций в
Dockerfileопределяет порядок слоёв и, соответственно, эффективность кэша слоёв: Docker/containerd переиспользует уже собранный слой, если инструкция и всё, от чего она зависит (предыдущие слои + контекст, влияющий именно на эту инструкцию), не изменились.
Базовый синтаксис Dockerfile
| |
Порядок COPY package*.json → RUN npm ci → COPY . . — не случайность, а классический паттерн максимизации кэша: файлы зависимостей (package.json/package-lock.json) меняются гораздо реже исходного кода, поэтому дорогая операция установки зависимостей (npm ci) кэшируется отдельно от частых изменений кода. Если бы COPY . . стоял раньше RUN npm ci, любое изменение любого файла кода инвалидировало бы кэш зависимостей — пересборка занимала бы минуты вместо секунд.
CMD vs ENTRYPOINT: ENTRYPOINT — неизменяемая (без переопределения при запуске) часть команды, CMD — аргументы по умолчанию к ней, которые легко переопределить (docker run image другая-команда заменит CMD, но не ENTRYPOINT). Частый паттерн: ENTRYPOINT ["node"] + CMD ["server.js"] — по умолчанию запускается node server.js, но docker run image debug.js запустит node debug.js без переопределения самого интерпретатора.
Multi-stage builds — сборка и финальный образ разделены
| |
FROM ... AS builder именует промежуточную стадию; COPY --from=builder копирует только нужный артефакт (скомпилированный бинарник) в финальный образ, не копируя весь тяжёлый инструментарий сборки (компилятор Go, кэш модулей). Итоговый образ содержит только бинарник и минимальный рантайм — этот же принцип лежит в основе паттерна Stapel-импорта из M3 урока 5 и продакшен-минимизации через distroless из M8 урока 6.
docker build/docker run вручную — что происходит «под капотом» werf
Хотя весь курс использует werf как оболочку над сборкой, важно понимать, что делает сама Docker/containerd-инфраструктура под капотом:
| |
werf (со стартового M0 урока 1) — не замена Docker/containerd, а надстройка с двумя билдерами (Dockerfile-builder и Stapel-builder, разбираем в M3 уроке 2), решающая задачи, которые голый docker build не решает из коробки: детерминированное тегирование по содержимому, giterminism, встроенная очистка registry, линковка нескольких образов в одном файле конфигурации.
Частые ошибки и подводные камни
- Ставить
COPY . .раньшеRUN npm ci/RUN go mod download. Инвалидирует кэш зависимостей при каждом изменении любого файла кода — самая частая причина «почему у меня сборка так долго идёт». - Путать
EXPOSEс публикацией порта.EXPOSE— это только документация/метаданные образа, реальная публикация происходит черезdocker run -p/K8sService(M1 урок 3), без этого флага/ресурсаEXPOSEне открывает никакого порта наружу. - Не использовать multi-stage и тащить в продакшен-образ весь инструментарий сборки. Финальный образ раздувается на сотни МБ компилятором/кэшем модулей, которые физически не нужны в рантайме и увеличивают площадь атаки.
Linux / Сети / Docker
Слои образа физически хранятся как обычные tar-архивы (плюс манифест config.json с метаданными) в специальном локальном хранилище (/var/lib/docker/overlay2/ для Docker, аналогично для containerd). Каждый слой идентифицируется хэшем своего содержимого (content-addressable storage) — именно на этом принципе позже будет построено content-based тегирование werf (M8 урок 3): тег образа werf вычисляется от хэша итогового содержимого, а не назначается вручную.
| |
Практика в кластере
Практики в кластере в этом уроке нет — материал полностью локальный (сборка на своей машине), первый деплой собранного образа Vikunja в кластер — с M3 урока 2 (после появления werf.yaml).
Практика разработки
Vikunja — self-hosted менеджер задач с Go-бэкендом (компилируемый бинарник) и Vue-фронтендом (статические файлы после сборки) — двухкомпонентная структура делает его удобным сквозным примером для всего модуля M3 (несколько образов в одном werf.yaml, линковка между ними).
- Клонируйте репозиторий и посмотрите на структуру:
| |
- Соберите минимальный пробный multi-stage
Dockerfileдля Go-бэкенда самостоятельно (упражнение, не финальное решение — финальное появится в M3 уроке 3–4):
| |
| |
- Убедитесь, что размер финального образа заметно меньше, чем если бы вы оставили промежуточную стадию
golang:1.26как единственныйFROM.
Шпаргалка команд урока
| |
Вопросы для самопроверки
Слои — read-only после сборки; удаление в новом слое — это whiteout-маркер поверх, а не реальное удаление данных из нижних слоёв.
Источник: Рекомендуемая литература
Порядок инструкций определяет порядок слоёв и, соответственно, границы кэша — разделение зависимостей и кода по разным слоям максимизирует переиспользование кэша.
Источник: Docker — документация
ENTRYPOINT задаёт неизменяемый исполняемый файл, CMD — аргументы по умолчанию к нему, которые docker run легко переопределяет без изменения ENTRYPOINT.
Источник: Docker — документация
Multi-stage разделяет сборочную среду и финальный рантайм-образ, оставляя в итоговом образе только необходимые артефакты — тот же принцип, что лежит в основе Stapel-импорта и distroless.
Источник: Docker — документация
EXPOSE не публикует порт сам по себе — нужен явный -p при docker run или Service в Kubernetes; это чисто декларативная метка в образе.
Источник: Docker — документация
Рекомендуемая литература
Официальная документация
- docs.docker.com — Dockerfile reference.
- docs.docker.com — Multi-stage builds.
- docs.docker.com — Build cache.
Статьи и блоги
- werf v1.2 — стабильный релиз утилиты для доставки приложений в Kubernetes (Stapel) — Flant на Habr.
- Первые шаги с werf: собираем и деплоим простое приложение — Flant на Habr.
- GitLab + K8s + Werf — Habr.
Книги
- Nigel Poulton. Docker Deep Dive. 2023. ISBN 978-1916585206.
Связанные материалы
- Предыдущий урок: M1.11 — RBAC.
- Следующий урок: M3.02 — werf.yaml и два билдера.
- Продвинутая минимизация образов: M8 урок 6 — Distroless.
✓ Урок пройден — все вопросы самопроверки отвечены верно