Привет! Меня зовут Илья Лушкевич, я DevOps и автор образовательных курсов в сфере IT.
Stepik - https://stepik.org/users/DevOps/teach
Post #25
158
Код в Прод 👩💻 Что такое Docker-образ на самом деле? Откройте любое видео «Docker за 5 минут» — и вам почти наверняка скажут что-то из этого: 🔹 «Образ — это как ISO-образ диска». 🔹 «Dockerfile — это рецепт, а образ — готовое блюдо». 🔹 «Это коробка, в которой лежит…
👩💻 Запускаем контейнер вручную — без Docker
В прошлый раз мы вскрыли Docker-образ и увидели, что это просто слои-архивы в формате
Можно ли распаковать их и запустить контейнер самому? Вообще без Docker? 🤔 Давайте попробуем!
1️⃣ Собираем корневую файловую систему
В прошлый раз мы распаковали один слой. Теперь распакуем все — строго по порядку из манифеста:
❗️ Порядок важен: верхние слои перезаписывают файлы нижних. Именно поэтому в
У alpine слой всего один, но у большинства образов их несколько.
Проверяем:
Получилось то, что называется rootfs — корневая файловая система будущего контейнера. Обычная директория с файлами.
2️⃣ Пробуем
Первое, что приходит в голову: у нас есть корень файловой системы, а в Linux есть команда
Проверим:
Мы вроде бы «внутри», а видим все процессы хоста. И можем убить любой из них. Сеть — общая с хостом, лимитов нет, прав тоже никто не урезал.
3️⃣ Пишем инструкцию запуска контейнера
Этот кто-то —
Но
Посмотрим на
Вот они — namespaces. Каждый отвечает за свой вид изоляции: pid даёт своё пространство процессов, network — свой сетевой стек, uts — собственное имя хоста, а mount — свои точки монтирования.
4️⃣ Запускаем контейнер
Два процесса, и наш
🧐 Если хотите проверить, что namespaces реально работают — уберите строку
5️⃣ Смотрим на контейнер снаружи
Самое интересное — открыть второй терминал, пока контейнер работает:
Наш «контейнер» с PID 1 внутри — снаружи обычный процесс с обычным номером. Его видно в общем списке, ему можно послать сигнал и у него есть родитель.
💡 И вот тут — важный момент. То, что мы весь пост называли «контейнером», на самом деле обычный процесс:
А теперь попробуйте закрыть терминал, в котором запущен
В следующий раз разберёмся, кто решает эти проблемы за нас — и почему за
👨💻 Код в Прод
В прошлый раз мы вскрыли Docker-образ и увидели, что это просто слои-архивы в формате
tar.gz. С этим разобрались — но как превратить эти слои в живой контейнер?Можно ли распаковать их и запустить контейнер самому? Вообще без Docker? 🤔 Давайте попробуем!
1️⃣ Собираем корневую файловую систему
В прошлый раз мы распаковали один слой. Теперь распакуем все — строго по порядку из манифеста:
mkdir alpine-image
skopeo copy docker://alpine:3.21 dir:alpine-image
mkdir rootfs
layers=$(jq -r '.layers[].digest' alpine-image/manifest.json | cut -d: -f2)
for layer in $layers; do
tar -xzf alpine-image/$layer -C rootfs
done
❗️ Порядок важен: верхние слои перезаписывают файлы нижних. Именно поэтому в
Dockerfile нельзя «спрятать» секрет, добавив его одним слоем и удалив следующим. RUN rm secret.txt не удаляет файл из образа — сверху лишь ляжет метка «удалено» (whiteout), а сам файл останется в нижнем слое. Кто угодно достанет его, распаковав слои — ровно как мы сейчас.У alpine слой всего один, но у большинства образов их несколько.
Проверяем:
~# ls -1F rootfs/
bin/
dev/
etc/
home/
lib/
media/
mnt/
...
Получилось то, что называется rootfs — корневая файловая система будущего контейнера. Обычная директория с файлами.
2️⃣ Пробуем
chroot — контейнер для бедных?Первое, что приходит в голову: у нас есть корень файловой системы, а в Linux есть команда
chroot, которая умеет делать любую директорию корнем для процесса.Проверим:
mount -t proc proc rootfs/proc
chroot rootfs /bin/sh
/ # ps aux
PID USER TIME COMMAND
1 root 0:02 {systemd} /sbin/init
2 root 0:00 [kthreadd]
3 root 0:00 [rcu_gp]
...
Мы вроде бы «внутри», а видим все процессы хоста. И можем убить любой из них. Сеть — общая с хостом, лимитов нет, прав тоже никто не урезал.
chroot подменил корень файловой системы и больше он ничего не умеет. Изоляции нет. Значит, кто-то должен её создать.exit
umount rootfs/proc
3️⃣ Пишем инструкцию запуска контейнера
Этот кто-то —
runc. Он создаёт namespaces, ставит лимиты через cgroups и урезает права — делает всю низкоуровневую работу по созданию контейнера. Именно его Docker вызывает под капотом на каждый docker run.Но
runc нужно объяснить, что запускать и в какой изоляции. Эта инструкция называется OCI runtime spec и лежит в файле config.json — runc умеет генерировать её шаблон сам:runc spec
~# ls -F
alpine-image/ config.json rootfs/
Посмотрим на
config.json:{
"process": {
"args": ["sh"],
"env": ["PATH=/usr/local/sbin:..."],
"capabilities": { "bounding": ["CAP_AUDIT_WRITE", "CAP_KILL", "CAP_NET_BIND_SERVICE"] }
},
"root": { "path": "rootfs", "readonly": true },
"hostname": "runc",
"linux": {
"namespaces": [
{ "type": "pid" }, { "type": "network" }, { "type": "ipc" },
{ "type": "uts" }, { "type": "mount" }, { "type": "cgroup" }
]
}
}Вот они — namespaces. Каждый отвечает за свой вид изоляции: pid даёт своё пространство процессов, network — свой сетевой стек, uts — собственное имя хоста, а mount — свои точки монтирования.
4️⃣ Запускаем контейнер
runc run mycontainer
/ # ps aux
PID USER TIME COMMAND
1 root 0:00 sh
7 root 0:00 ps aux
Два процесса, и наш
sh — первый в системе. Docker в этой истории не участвовал вообще.🧐 Если хотите проверить, что namespaces реально работают — уберите строку
{"type": "pid"} из config.json и запустите снова. Внутри опять появятся все процессы хоста, как при chroot.5️⃣ Смотрим на контейнер снаружи
Самое интересное — открыть второй терминал, пока контейнер работает:
~# ps -p $(pgrep -x sh) -o pid,ppid,cmd
PID PPID CMD
2323 2312 sh
Наш «контейнер» с PID 1 внутри — снаружи обычный процесс с обычным номером. Его видно в общем списке, ему можно послать сигнал и у него есть родитель.
💡 И вот тут — важный момент. То, что мы весь пост называли «контейнером», на самом деле обычный процесс:
runc просто запустил его в изоляции. Никакой особой сущности «контейнер» в системе нет. Есть процесс, которому подменили корень файловой системы и ограничили видимость мира.А теперь попробуйте закрыть терминал, в котором запущен
runc. Контейнер тут же завершится вместе с ним. А его логи? Их нигде не будет — собирать было некому.В следующий раз разберёмся, кто решает эти проблемы за нас — и почему за
docker run прячется цепочка сразу из пяти программ.👨💻 Код в Прод
- 🔥 7
- 👍 1






