TGViewer
Код в Прод Код в Прод @codeinprod · 238 subscribers
Post #25 160
Код в Прод 👩‍💻 Что такое Docker-образ на самом деле? Откройте любое видео «Docker за 5 минут» — и вам почти наверняка скажут что-то из этого: 🔹 «Образ — это как ISO-образ диска». 🔹 «Dockerfile — это рецепт, а образ — готовое блюдо». 🔹 «Это коробка, в которой лежит…
👩‍💻 Запускаем контейнер вручную — без Docker

В прошлый раз мы вскрыли 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
More from @codeinprod
  1. Jul 27, 2026🐧 Заходишь на сервер, чтобы протестировать новый скрипт, а у chmod кто-то убрал x бит: -r…
  2. Jun 28, 2026👩‍💻 Как проверить сеть, если под рукой нет сетевых утилит? Представьте: ваше приложение…
  3. May 31, 2026👩‍💻 Что такое Docker-образ на самом деле? Откройте любое видео «Docker за 5 минут» — и в…
  4. Apr 15, 2026🐧 Ты создал облачный сервер. Добавил туда SSH-ключ… как тебе казалось. Пытаешься зайти: s…
  5. Mar 6, 2026Post #20
  6. Mar 6, 2026Ставишь Nginx Ingress Controller в Kubernetes — и получаешь два балансировщика. В облаке п…
Threads Profile ViewerView any public Threads profile without an account.Open ThreadLook →Writing with AI? Make it sound human.Metric37 rewrites AI drafts so they read naturally. Free AI detector, 1,500 words free.Try Metric37 →