Образы и слои Docker
Запустил на сервере контейнер с командой sleep. Внутри он видит себя первым процессом системы, номер 1. А на хосте, то есть на самой машине, где всё это крутится, я нахожу его в обычном списке процессов: номер 3278386, команда «sleep 300», родитель - служебный процесс containerd-shim, занято памяти 920 килобайт. Никакой второй операционной системы рядом не появилось. Пять таких контейнеров подряд запустились за 2078 миллисекунд, то есть 415 миллисекунд на штуку вместе с самой командой docker.
Стержень: контейнер это процесс хоста, которому ядро подменило вид на мир, а образ - стопка неизменяемых слоёв, из которой ничего нельзя вычеркнуть задним числом.
// Формулировки: «чем контейнер отличается от виртуальной машины?», «зачем образу слои?», «удалил секрет следующей строкой - он ушёл из образа?»
Контейнер это процесс с подменённым видом на мир
Ядро - это программа, которая распоряжается процессором, памятью и железом и раздаёт их всем процессам машины. Ядро на сервере одно, и контейнер его не поднимает. Он получает от ядра две вещи. Первая - пространства имён: отдельный список процессов, отдельная сеть, отдельное дерево каталогов. Вторая - контрольные группы (cgroups): потолок и счётчик по памяти и процессору.
Пространство имён видно числом. У процесса на хосте список процессов помечен как 4026531836, у процесса в контейнере - 4026532630. Разные метки означают разные списки, поэтому внутри контейнера я насчитал 4 процесса, а на хосте в ту же секунду - 154. То же самое с сетью, с деревом каталогов, с именем машины. А если запустить контейнер с флагом --network host, метка сети совпадает с хостовой до цифры - и никакой сетевой изоляции больше нет, хотя список процессов остаётся своим.
// Отсюда и цена вопроса. Процесс sleep внутри контейнера занял 920 килобайт - это всё, что контейнер тратит на себя. Виртуальная машина поднимает собственное ядро поверх гипервизора (прослойки, которая делит железо между машинами), и платит за это секундами загрузки и сотнями мегабайт памяти. Зато граница у неё крепче: у контейнеров ядро общее, и дыра в ядре достаёт до всех сразу.
- ядро
- программа, раздающая процессам процессор, память и железо; на машине одна
- пространство имён
- отдельный список процессов, сети или каталогов, выданный ядром
- контрольные группы
- механизм ядра, который считает и ограничивает ресурсы процесса
- гипервизор
- прослойка, которая делит железо между виртуальными машинами
Из слоя нельзя вычеркнуть, можно только накрыть
Каждая строка сборки добавляет слой - неизменяемый набор изменений в файлах. Слои лежат стопкой, и то, что попало в нижний, там и остаётся. Я это проверил буквально. Собрал образ, где сначала копируется файл с ключом, а следующей же строкой он удаляется. В запущенном контейнере файла честно нет: «cat: can't open '/tmp/token.env'».
Дальше я сохранил образ в архив и разобрал его по слоям. Слой базового alpine (маленький образ Linux, с которого обычно начинают сборку): 87 файлов, 3,6 мегабайта. Слой копирования: один файл на 175 байт, и внутри лежит мой ключ целиком. Слой удаления: тоже один файл, но с именем «.wh.token.env» - это файл-пометка. Он ничего не стирает, он говорит верхним уровням «считайте, что token.env тут нет». Данные остались этажом ниже, и любой, кто скачал образ, достаёт их той же распаковкой за минуту.
// Правильный способ - секрет сборки: значение подкладывается шагу на время его работы и в слой не попадает. Я собрал тот же образ с флагом --mount=type=secret и проверил все слои получившегося образа: слоёв с ключом ноль, а результат работы шага на месте. Второй способ - собирать в промежуточной стадии, а в финальный образ копировать только готовый файл.
- слой
- неизменяемый набор изменений файлов, добавленный одной строкой сборки
- файл-пометка
- запись в верхнем слое «этого файла нет»; данные ниже остаются
- секрет сборки
- значение, доступное шагу сборки, но не попадающее в слой
Тег переставляют, отпечаток - нет
Тег - это подвижная бумажка с именем. Я собрал два разных образа, повесил на первый имя demo:v1 (он был c006fde2...), а потом повесил то же самое имя на второй (81c8b91e...). Запуск demo:v1 до этого печатал «приложение А», после - «приложение Б». Имя не изменилось ни на букву.
Отпечаток содержимого так не переставить: это хэш - короткая строка, посчитанная по самому содержимому, вида alpine@sha256:d9e853e8... Изменилось содержимое - изменился отпечаток. Поэтому на проде либо прибивают отпечаток, либо дают каждой сборке свой неповторяющийся тег вида app:2026.08.11-a1b2c3. Плавающее имя latest на нескольких машинах разъезжается: где-то стоит вчерашняя сборка, где-то позавчерашняя, а откатиться некуда, потому что откатываться не к чему.
// Обратная сторона неизменяемости приятная. Я собрал два образа на одной базе: у каждого по 3 слоя и по 54,1 мегабайта на бумаге, а общих слоёв - 2. Хранилище держит их в одном экземпляре, и по сети едет только разница. На этом сервере лежит 41 образ общим весом 11,68 гигабайта, и 78% этого веса - переиспользуемое.
- тег
- подвижное имя образа; его можно перевесить на другой образ
- отпечаток содержимого
- хэш образа; при смене содержимого меняется, поэтому ссылка на него надёжна
- реестр
- хранилище образов, откуда машины их скачивают
Как отвечать: «Чем контейнер отличается от виртуальной машины?»
Контейнер это обычный процесс на ядре хоста, которому ядро подменило вид на мир: свой список процессов, своя сеть, своё дерево каталогов, а сверху потолок по памяти и процессору. Я это проверял: контейнер с командой sleep виден на хосте обычным процессом и занимает меньше мегабайта, а запуск занимает доли секунды. Виртуальная машина поднимает собственное ядро поверх гипервизора, поэтому стартует секундами и тратит на себя сотни мегабайт. Практический вывод про безопасность: у контейнеров ядро общее, дыра в нём достаёт до всех сразу, а пользователь с полными правами внутри контейнера без отдельной настройки равен такому же пользователю на самой машине. Поэтому для чужого кода или жёсткой границы между клиентами берут виртуалки или отдельные машины, а не надеются на контейнер.
Ответ даёт механику, подкрепляет её порядком величин и заканчивается решением о том, когда контейнера мало. Это разговор эксплуатации, а не пересказ статьи.
На чём валятся
- −Называют контейнер «лёгкой виртуалкой со своей операционной системой». Ядро общее, своего ядра нет.
- −Удаляют секрет следующей строкой сборки и считают образ чистым. Слой с секретом остаётся, его достают распаковкой.
- −Катят latest на прод и потом не могут сказать, какая именно сборка сейчас работает на каждой машине.
- −Считают контейнер полноценной границей безопасности для чужого кода.
- −Путают вес образа на бумаге с занятым местом: слои общие, и одинаковая база хранится один раз.
Проверьте себя
Пять вопросов из банка по этой подтеме. Всего их 7, остальные разбираются в тренажёре.
- В Dockerfile ключ копируется, используется, а следующей инструкцией удаляется. Почему это дыра?A)Удаление сработает лишь при сборке с BuildKitB)Слой с ключом остаётся в образе и достаётсяC)Ключ попадёт в переменные окружения контейнераD)rm внутри RUN не выполняется на этапе сборки
показать ответ и разбор
+B)Слой с ключом остаётся в образе и достаётся// разбор: Слои неизменяемы и складываются стопкой: удаление в верхнем слое лишь помечает файл отсутствующим, а содержимое нижнего никуда не девается — его видно через docker save или разбор слоёв. Секреты в сборку передают через --mount=type=secret (BuildKit) или собирают в промежуточной стадии, из которой в финальный образ копируют только артефакт.
- Скачали второй образ на той же базе, что и первый, — pull прошёл почти мгновенно. Почему?A)Второй образ целиком взялся из локального кэша сборки на хостеB)Слои общие, адресуются по хешу и повторно не качаютсяC)Docker хранит только разницу между тегами внутри репозиторияD)Реестр отдал образ сильнее сжатым, потому что это повтор
показать ответ и разбор
+B)Слои общие, адресуются по хешу и повторно не качаются// разбор: Образ — это стек read-only слоёв, каждый адресуется по content digest. Docker хранит и тянет слои по хешу, поэтому общие с уже скачанным образом слои (например, общий базовый образ) повторно не качаются — pull докачивает только недостающие. Отсюда же выгода тонких общих баз: экономия на диске и на сети.
- docker build идёт долго и гонит по сети сотни мегабайт ещё до первой инструкции. В чём причина?A)Базовый образ каждый раз скачивается заново, потому что кэш не работаетB)Слои образа не сжимаются при передаче их демону dockerC)docker build пересобирает образ с нуля, без использования кэшаD)В контекст сборки попал весь каталог; лечит .dockerignore
показать ответ и разбор
+D)В контекст сборки попал весь каталог; лечит .dockerignore// разбор: Перед сборкой docker упаковывает и отправляет демону весь контекст — каталог, указанный аргументом (обычно .). Если там лежат .git, node_modules, дампы, артефакты — они летят по сети/сокету и раздувают сборку, а иногда и утекают в образ через COPY. Лечит .dockerignore: исключает лишнее из контекста (как .gitignore для git).
- Образ собрали на Apple Silicon (arm64), на amd64-нодах он падает с exec format error. Что не так и как собирать?A)Архитектура образа не та; нужен мультиарх через buildxB)На нодах не установлен подходящий рантайм для этого образаC)Образу не хватило прав, на нодах нужен запуск с --privilegedD)Тег latest указал на битую сборку, лежащую в реестре
показать ответ и разбор
+A)Архитектура образа не та; нужен мультиарх через buildx// разбор: exec format error значит, что бинарь в образе собран под другую архитектуру процессора: arm64-образ не запустится на amd64-ноде. Образы привязаны к платформе. Решение — мультиарх-сборка: docker buildx build --platform linux/amd64,linux/arm64 --push собирает и публикует manifest list, из которого каждая нода тянет свой вариант. Так одна и та же ссылка работает на любых нодах.
- Чем образ Docker отличается от контейнера?A)Образ — шаблон, контейнер — экземплярB)Образ работает, контейнер лежит в реестреC)Это одно и то же, названия из разных версий DockerD)Образ хранит данные приложения, контейнер — только код
показать ответ и разбор
+A)Образ — шаблон, контейнер — экземпляр// разбор: Образ лежит на диске и не меняется, а контейнер это процесс, поднятый из образа со своим слоем записи. Из одного образа поднимают сколько угодно контейнеров, и каждый живёт своей жизнью.
дальше
Теорию прочитали. Навык ставится повторением
В Сеньорчике эта подтема идёт в ежедневных сессиях: движок возвращает её, пока ответы не станут уверенными, и ведёт прогресс отдельно по каждой подтеме. Теория внутри тоже бесплатна, лимит только на количество вопросов в день.