Многие разработчики привыкли использовать Docker, Podman и другие контейнерные инструменты как нечто само собой разумеющееся. Мы пишем Dockerfile, запускаем docker build и получаем готовый образ. Но что происходит под капотом этой команды? Как из нескольких строчек конфигурации получается полноценный контейнер с собственной файловой системой?
Оказывается, собрать образ можно даже вручную, буквально «из ничего». Именно такой подход описал датский разработчик Дэниш Пракаш, решив продемонстрировать, как именно устроен контейнерный образ и что происходит при его создании.
🔍 Что на самом деле такое контейнерный образ?
Начнём с небольшой предыстории. Раньше не было единого стандарта для контейнеров. Каждый инструмент работал по своим правилам, и это создавало проблемы совместимости. В 2015 году возникла Инициатива открытых контейнеров (Open Containers Initiative - OCI), которая стандартизировала форматы контейнеров и образов. Теперь большинство инструментов, таких как Docker и Podman, следуют OCI-спецификации.
OCI-образ состоит из четырёх основных компонентов:
📦 Слои (layers) – изменения в файловой системе, представленные в виде архивов tar.gz. Каждый слой хранит только разницу с предыдущим слоем, что позволяет экономить место и ускорять загрузку образов.
⚙️ Конфигурация (config.json) – настройки запуска контейнера: точка входа (entrypoint), переменные окружения, открытые порты и другие параметры.
📑 Манифест (manifest.json) – карта, которая связывает слои и конфигурацию между собой, используя хеши (SHA256).
🗂️ Индекс (index.json) – верхнеуровневая структура, описывающая несколько манифестов (например, для поддержки разных архитектур).
✨ Создание образа «Hello World» вручную
Чтобы понять внутреннее устройство контейнера, Дэниш предлагает сделать собственный простой образ на основе пустого образа (scratch):
FROM scratch
COPY ./hello /
ENTRYPOINT ["./hello"]
Здесь:
- 📍 scratch – полностью пустой базовый образ.
- 📍 COPY добавляет бинарный файл hello.
- 📍 ENTRYPOINT указывает, что нужно выполнить этот бинарник при запуске контейнера.
🧩 Как формируются слои?
Слои – это просто архивы tar с изменениями файловой системы. Например, если мы хотим удалить файл /bin/ash и заменить его на /bin/bash, создаётся архив с файлами:
- /bin/bash – новый файл
- /bin/.wh.ash – специальный файл («whiteout»), показывающий удаление старого файла ash.
Эти архивы накладываются друг на друга, формируя единую файловую систему контейнера.
📂 Пошаговая структура образа OCI
Для нашего примера с «Hello World» необходимо:
🔨 Шаг 1: Скомпилировать простой статический бинарник на C:
#include <stdio.h>
int main(int argc, char *argv[]) {
if (argc < 2) {
printf("Usage: %s <name>\n", argv[0]);
return 1;
}
printf("Hello, %s!\n", argv[1]);
return 0;
}
Компиляция:
gcc -o hello hello.c -static
tar -czvf layer.tar.gz hello
sha256sum layer.tar.gz
mv layer.tar.gz <полученный-хеш>
🔨 Шаг 2: Создание конфигурационного файла (config.json):
{
"architecture": "amd64",
"os": "linux",
"config": {
"Entrypoint": ["./hello"]
}
}
Затем также делаем этот файл адресуемым по содержимому (SHA256):
sha256sum config.json
mv config.json <полученный-хеш>
🔨 Шаг 3: Создание манифеста (manifest.json), связывающего слой и конфигурацию по хешам:
{
"schemaVersion": 2,
"config": {
"mediaType": "application/vnd.oci.image.config.v1+json",
"digest": "sha256:<хеш-config>",
"size": <размер-конфигурации>
},
"layers": [{
"mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
"digest": "sha256:<хеш-layer>",
"size": <размер-слоя>
}]
}
🔨 Шаг 4: Создание индекса (index.json), который ссылается на манифест:
{
"schemaVersion": 2,
"manifests": [{
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"digest": "sha256:<хеш-манифеста>",
"size": <размер-манифеста>,
"annotations": {
"org.opencontainers.image.ref.name": "hello:scratch"
}
}]
}
🖥️ Тестирование созданного вручную образа
Собираем всё вместе в архив и загружаем через Podman:
tar -cf hello.tar blobs index.json
podman load < hello.tar
podman run localhost/hello:scratch world
Вывод:
Hello, world!
🔥 Личное мнение автора: зачем нам это нужно?
Казалось бы, зачем разбираться так подробно? Однако понимание того, как именно устроен образ контейнера, помогает лучше осознать, какие слои можно оптимизировать и как уменьшить размер итогового образа. Это особенно полезно:
✅ Для оптимизации ресурсов (экономия памяти, диска).
✅ Для безопасности (контроль содержимого каждого слоя).
✅ Для гибкости в CI/CD-процессах (кастомные сборки, проверки).
Такие знания делают разработчика не просто пользователем Docker, а инженером, способным эффективно управлять контейнеризацией и понимать её слабые и сильные стороны.
🚀 Итог и рекомендации
Изучение внутреннего устройства контейнеров – не просто интересный технический эксперимент, это ещё и путь к профессиональному росту. Разбираясь в OCI-образах, вы получите реальный контроль над тем, что происходит в ваших контейнерах, а значит, и в ваших приложениях.
Не бойтесь копать глубже – именно в таких деталях кроется истинное мастерство.
🔗 Оригинальная новость