Сначала были физические серверы - дорогие и неэффективные. Затем пришли виртуальные машины, которые позволили запускать несколько изолированных ОС на одном железе. Но цена изоляции оставалась высокой: полная копия ОС, гигабайты диска, минуты на запуск.

Контейнеры - следующий шаг эволюции. Зачем виртуализировать целое железо и запускать полноценную ОС, если можно изолировать только сам процесс, используя встроенные механизмы ядра? Этот подход на порядок легче, быстрее и эффективнее.

Расставил приоритеты
Расставил приоритеты

Образ и Контейнер

  • Образ (Image) - шаблон или чертеж. Это неизменяемый набор файлов, инструкций и зависимостей для вашего приложения, доступных только для чтения.

  • Контейнер (Container) - запущенный экземпляр образа. Это изолированная среда, где ваше приложение фактически работает. При создании контейнера на основе образа добавляется тонкий слой для записи (writable layer). Все изменения, которые происходят во время работы приложения сохраняются именно в этом слое.

Но что же на самом деле скрывается внутри этой коробки? Давай заглянем под капот.

Часть 1: Главный секрет контейнеров…Их не существует

Правда в том, что с точки зрения ядра Linux не существует такой сущности, как «контейнер». Есть всего лишь обычный Linux-процесс. Но процесс в особых условиях.

Эти условия создаются двумя механизмами ядра:

1.    Namespaces (Пространства имён)
Namespaces ограничивают то, какие ресурсы и процессы видны изнутри контейнера. По сути, они создают для каждой группы процессов иллюзию, что у них есть собственный экземпляр глобального ресурса, будь то сетевой стек, дерево процессов или файловая система.

  •  PID namespace: Процесс внутри контейнера думает, что он первый и главный (PID 1), и не видит процессы снаружи.

  • Network namespace: Контейнер получает свою собственную виртуальную сетевую карту, таблицу маршрутизации и iptables.

  • Mount namespace: У контейнера своё собственное дерево файловой системы, изолированное от хост-системы.

  •  ...и другие (UTS, IPC, User).

2.    Cgroups (Control Groups)
Они не изолируют, а ограничивают и контролируют ресурсы. Cgroups отвечают на вопрос «Сколько?».

  • Ограничивают использование CPU, памяти, дискового I/O.

  • Не дают одному «прожорливому» контейнеру утянуть за собой всю систему.

Вывод: Контейнер - самый обычный процесс (например, nginx или bash), просто изолированный от других процессов при помощи namespaces и с лимитами, установленными через cgroups.

Часть 2: Анатомия образа

Если контейнер - это процесс, то образ - это его инструкция по запуску.

Образ контейнера состоит из двух ключевых частей:

1.    Root Filesystem (rootfs) - дерево каталогов, содержащее ровно то программное обеспечение, которое нужно для запуска вашего приложения: бинарные файлы, библиотеки, настройки. Ни больше, ни меньше.

2.    JSON-файл с метаданными - инструкция. В нем описано, какой процесс запустить (CMD), какие переменные окружения установить, какие порты открыть и куда подключить тома.

Как это хранится и передается? Всё просто! 

Утилита tar упаковывает rootfs и JSON-файл в единый архив. Именно в таком виде образы лежат в Docker Registry (например, Docker Hub). Каждый слой образа - тоже tar-архив с изменениями файловой системы.

Вот как это выглядит на практике - манифест образа nginx:

[
  {
    "RepoTags": ["nginx:latest"],
    "Layers": [
      "blobs/sha256/eb5f13bce993...",  // Базовый слой (77.8 МБ)
      "blobs/sha256/dab69e9f41e9...",  // Следующий слой (118.6 МБ)
      // ... остальные слои
    ],
    "LayerSources": {
      "sha256:129b375526fc...": {
        "mediaType": "application/vnd.oci.image.layer.v1.tar",  // ? Вот он - tar!
        "size": 5120,
        "digest": "sha256:129b375526fc..."
      }
    }
  }
]

Когда вы делаете docker pull, вы просто скачиваете и распаковываете эти архивы, а Docker Engine собирает из них готовую файловую систему контейнера.

Часть 3: Как это всё работает вместе: Архитектура Docker

Когда вы вводите команду docker run nginx, под капотом запускается целый оркестр из четырех компонентов.

1.    Docker Engine 

  •  Это высокоуровневый демон, с которым вы общаетесь через CLI (docker run, docker build).

  • Он обрабатывает ваши команды, управляет сетями, томами, но сам контейнеры не запускает. Он передает эту задачу вниз.

2.    containerd 

  • Это основной runtime, который управляет жизненным циклом контейнеров.

  • Он отвечает за скачивание образов, управление хранилищем и за передачу инструкций по запуску следующему звену

3.    runc 

  • Это и есть тот самый инструмент, который обращается к ядру linux для изоляции процессов, файловой системы, сети и других ресурсов контейнера от хостовой системы.

  • Получив от containerd инструкцию (config.json и rootfs), runc напрямую взаимодействует с ядром Linux.

  • Он создает новые namespaces, настраивает cgroups и, наконец, запускает в этой изоляции наш целевой процесс (например, nginx).

    Важно: После запуска процесса runc свою работу считает выполненной и завершается.

4.    containerd-shim 

  • А что же происходит с процессом контейнера, когда его родитель (runc) завершился? За ним присматривает containerd-shim.

  • Это легковесный процесс, который становится новым родителем для процесса контейнера.

    Зачем это нужно?

    • Containerd-shim позволяет перезапустить или обновить containerd, не останавливая работающие контейнеры.

    • Перенаправляет стандартные потоки ввода/вывода (stdin, stdout, stderr) обратно в Docker Engine.

    • Отслеживает статус процесса и сообщает о его завершении. 

Итог

Ваша команда docker run проходит долгий путь: 

чтобы в итоге на вашей машине появился всего лишь один изолированный Linux-процесс.

Теперь, когда вы знаете этот фокус, Docker становится мощной инженерной системой. Удачи в контейнеризации :-)

Комментарии (3)


  1. iamkisly
    06.11.2025 11:50

    Простите я не удержался
    Простите я не удержался


    1. Up4Soft Автор
      06.11.2025 11:50

      хахаахааха


  1. iwram
    06.11.2025 11:50

    Нужно больше статей про контейнеры..... Но пока лучшая https://habr.com/ru/articles/935178/