Что такое "операционная система Linux"? Это ядро (Linux kernel, бинарник в несколько мегабайт, хитрая прослойка между железом и софтом) и пользовательское пространство (user space, десяки или сотни мегабайт разных бинарников утилит, помогающих работать с ядром). Ядро всегда одно, отличается лишь версиями, а пользовательских пространств много (Debian, Ubuntu, CentOS, Alpine, busybox, кастомные сборки и т.п.).
Что такое "контейнеризация"? Это способ виртуализации на уровне операционной системы. В частности, это когда на запущенной Linux ОС можно положить разные пользовательские пространства в разные папки и потом запустить их выполняться. В качестве ядра будет использовано ядро родительской ОС, но программы, выполняющиеся в разных контейнерах, будут думать, что каждая из них имеет свою собственную ОСь вокруг. Изменения, сделанные в рамках одного контейнера, могут быть скрыты от остальных контейнеров и родительской ОС. Как это работает? Ядро Linux поддерживает т.н. namespaces, то есть способ изоляции ресурсов. Например, Process ID namespace позволяет иметь несколько деревьев процессов, начинающихся каждое со своего PID 1 и все это в рамках одного и того же экземпляра выполняющегося ядра. Также существуют network, mount, user ID и прочите изолированные пространства. Системный вызов clone(), являющийся основой канонического fork() принимает флаги, позволяющие изолировать новый запускаемый процесс в различных пространствах. Зачем это нужно? Здорово уметь запускать на одной железке как можно больше изолированных окружений, это круто повышает утилизацию ресурсов (альтернативный подход - запускать полностью излоированные ОСи на одной и той же железке - обычно менее эффективен [см. kata containers], правда и более безопасен).
Что такое Docker? Когда-то давно Docker был большим демоном-монолитом (и клиентом, со всеми знакомым нам CLI API), решающим комплексную задачу запуска и обслуживания контейнеров. Что же именно это значит? Когда на диске уже есть папка с набором файлов какого-либо user space, остается лишь создать нужные namespace и запустить бинарник, указанный как entry point в качестве процесса с PID 1. Но после запуска необходимо мониторить состояние контейнера, возможно перезапускать его при падении, очищать ресусры при остановке и т.п. Папку с user space на диске тоже нужно создать умело. Многие контейнеры используют на 100% схожий user space, отличаясь лишь полезной нагрузкой (кто-то хочет запускать nginx в debian:latest, кто-то свой node.js app но тоже в debian:latest и т.п.). Так как речь идет о десятках и сотнях мегабайт, неплохо бы уметь реиспользовать повторяющиеся части. Я могу выделить следующие основные куски Docker-а: 1) обслуживание жизненного цикла контейнеров (запуск, мониторинг состояния, очистка ресурсов после остановки) 2) работа с образами (images), т.е. жонглирование всеми этими большими папками, в частности загрузка их из реестров по сети 3) мощный API для запуска и запроса состояния контейнеров и образов. Так как каждый из этапов вполне себе самодостаточный, а релизить все синхронно - сложно, в какой-то момент Docker решили распилить на runc (запускатор контейнеров, консольная утилита, один запус == один контейнер), containerd (демон, делает все, чтобы runc мог запускать новые контейнеры, в частности работу с образами) и dockerd ("лицевой" демон, общающийся с containerd, чтобы мы могли исползьовать вот эти все удобные плюшки Docker-а в командной строке, через docker client). С момента распила каждый из проектов живет своей отдельной жизнью. Например, runc используется и другими менеджерами контейнеров (см. cri-o), а его интерфейс стандартизирован и он является канонической реалзиацией Container Runtime Specification.
Post #33
263