сеньорчикОткрыть в Telegram
← вся теориятеория к собесу · Docker

Образы и слои 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, остальные разбираются в тренажёре.

  1. #dvo_docker_images1 / 5
    В Dockerfile ключ копируется, используется, а следующей инструкцией удаляется. Почему это дыра?
    A)Удаление сработает лишь при сборке с BuildKit
    B)Слой с ключом остаётся в образе и достаётся
    C)Ключ попадёт в переменные окружения контейнера
    D)rm внутри RUN не выполняется на этапе сборки
    показать ответ и разбор
    +B)Слой с ключом остаётся в образе и достаётся

    // разбор: Слои неизменяемы и складываются стопкой: удаление в верхнем слое лишь помечает файл отсутствующим, а содержимое нижнего никуда не девается — его видно через docker save или разбор слоёв. Секреты в сборку передают через --mount=type=secret (BuildKit) или собирают в промежуточной стадии, из которой в финальный образ копируют только артефакт.

  2. #dvo_docker_images2 / 5
    Скачали второй образ на той же базе, что и первый, — pull прошёл почти мгновенно. Почему?
    A)Второй образ целиком взялся из локального кэша сборки на хосте
    B)Слои общие, адресуются по хешу и повторно не качаются
    C)Docker хранит только разницу между тегами внутри репозитория
    D)Реестр отдал образ сильнее сжатым, потому что это повтор
    показать ответ и разбор
    +B)Слои общие, адресуются по хешу и повторно не качаются

    // разбор: Образ — это стек read-only слоёв, каждый адресуется по content digest. Docker хранит и тянет слои по хешу, поэтому общие с уже скачанным образом слои (например, общий базовый образ) повторно не качаются — pull докачивает только недостающие. Отсюда же выгода тонких общих баз: экономия на диске и на сети.

  3. #dvo_docker_images3 / 5
    docker build идёт долго и гонит по сети сотни мегабайт ещё до первой инструкции. В чём причина?
    A)Базовый образ каждый раз скачивается заново, потому что кэш не работает
    B)Слои образа не сжимаются при передаче их демону docker
    C)docker build пересобирает образ с нуля, без использования кэша
    D)В контекст сборки попал весь каталог; лечит .dockerignore
    показать ответ и разбор
    +D)В контекст сборки попал весь каталог; лечит .dockerignore

    // разбор: Перед сборкой docker упаковывает и отправляет демону весь контекст — каталог, указанный аргументом (обычно .). Если там лежат .git, node_modules, дампы, артефакты — они летят по сети/сокету и раздувают сборку, а иногда и утекают в образ через COPY. Лечит .dockerignore: исключает лишнее из контекста (как .gitignore для git).

  4. #dvo_docker_images4 / 5
    Образ собрали на Apple Silicon (arm64), на amd64-нодах он падает с exec format error. Что не так и как собирать?
    A)Архитектура образа не та; нужен мультиарх через buildx
    B)На нодах не установлен подходящий рантайм для этого образа
    C)Образу не хватило прав, на нодах нужен запуск с --privileged
    D)Тег latest указал на битую сборку, лежащую в реестре
    показать ответ и разбор
    +A)Архитектура образа не та; нужен мультиарх через buildx

    // разбор: exec format error значит, что бинарь в образе собран под другую архитектуру процессора: arm64-образ не запустится на amd64-ноде. Образы привязаны к платформе. Решение — мультиарх-сборка: docker buildx build --platform linux/amd64,linux/arm64 --push собирает и публикует manifest list, из которого каждая нода тянет свой вариант. Так одна и та же ссылка работает на любых нодах.

  5. #dvo_docker_images5 / 5
    Чем образ Docker отличается от контейнера?
    A)Образ — шаблон, контейнер — экземпляр
    B)Образ работает, контейнер лежит в реестре
    C)Это одно и то же, названия из разных версий Docker
    D)Образ хранит данные приложения, контейнер — только код
    показать ответ и разбор
    +A)Образ — шаблон, контейнер — экземпляр

    // разбор: Образ лежит на диске и не меняется, а контейнер это процесс, поднятый из образа со своим слоем записи. Из одного образа поднимают сколько угодно контейнеров, и каждый живёт своей жизнью.

дальше

Теорию прочитали. Навык ставится повторением

В Сеньорчике эта подтема идёт в ежедневных сессиях: движок возвращает её, пока ответы не станут уверенными, и ведёт прогресс отдельно по каждой подтеме. Теория внутри тоже бесплатна, лимит только на количество вопросов в день.