Learning Platform
Глоссарий Troubleshooting
Урок 06.02 · 22 мин
Начальный
dockercontainerscopy-on-writeoverlayfscommit

Образ vs контейнер: template и runtime

В предыдущем уроке мы провели аналогию: образ это класс, контейнер это экземпляр. Эта метафора полезна на 80% случаев, но за ней скрывается важная техническая деталь — copy-on-write. Понимание этого механизма объясняет, почему контейнеры стартуют за миллисекунды, почему пять одинаковых контейнеров не занимают пятикратно места и почему docker commit существует, но является антипаттерном.

В этом уроке погружаемся в то, что физически происходит между docker run и появлением процесса в ps.


Что делает docker run

Когда ты пишешь docker run nginx:1.27, последовательность примерно такая:

  1. Resolve образа. Docker daemon смотрит в локальный кэш. Нет — обращается к registry, скачивает манифест и все слои.
  2. Создание контейнера. Внутри /var/lib/docker/containers/<id>/ создаётся метадата (config.v2.json, hostconfig.json).
  3. Подготовка rootfs. Это самая интересная часть. Docker создаёт новый writable layer в /var/lib/docker/overlay2/<container-id>/diff/ — изначально пустую директорию. Затем настраивает overlayfs: lowerdirs = read-only слои образа, upperdir = только что созданный writable layer, merged = что увидит процесс.
  4. Создание namespaces. clone() с флагами CLONE_NEWPID | CLONE_NEWNS | CLONE_NEWNET и т.д. — новый процесс рождается в изолированном PID/network/mount-неймспейсе.
  5. Запуск процесса. В новом неймспейсе выполняется chroot (точнее pivot_root) в merged-директорию, затем exec команды из image config (CMD / ENTRYPOINT).

Шаг 3 это копирование? Нет. Никакие файлы не копируются. Создаётся только пустая директория upperdir. Это и есть copy-on-write: данные копируются только когда процесс реально пишет в файл из read-only слоя.

docker run: что физически создаётся
Read-only слои образа остаются на месте; добавляется лишь пустой writable layer
Layer 0 (RO)Базовый слой образа (например, debian:12-slim). Лежит в /var/lib/docker/overlay2/{layer-id}/diff/. Read-only, шарится между всеми контейнерами, использующими этот образ.
Layer 1 (RO)Слой 1: apt-installed пакеты. Read-only.
Layer 2 (RO)Слой 2: приложение. Read-only.
Writable Layerupperdir, ~emptyWritable Layer контейнера. Изначально пустая директория. Все изменения, которые делает процесс (новые файлы, модификации существующих) сохраняются здесь. При удалении контейнера этот слой исчезает.
Merged Viewчто видит процессТо, что видит процесс внутри контейнера как корневую файловую систему. Это overlayfs merged: верхний слой накрывает нижние, при чтении используются нижние, при записи -- copy-up в верхний.

open(), read(), write() — системные вызовы для файлов

Copy-on-write шаг за шагом

Допустим, в образе есть файл /etc/nginx/nginx.conf (200 байт), который лежит в read-only слое 2. Контейнер запущен. Происходит четыре сценария:

Сценарий 1: процесс читает файл. cat /etc/nginx/nginx.conf. Overlayfs смотрит upperdir — файла нет. Смотрит lowerdirs сверху вниз — находит в слое 2. Возвращает содержимое read-only слоя. Никаких записей на диск, файл не копируется.

Сценарий 2: процесс пишет в файл. echo "client_max_body_size 100M;" >> /etc/nginx/nginx.conf. Overlayfs делает copy-up: копирует файл из lowerdir в upperdir (теперь занимает 200 байт в writable layer), затем дописывает изменения в копию. Read-only слой остался нетронутым.

Сценарий 3: процесс создаёт новый файл. echo "test" > /tmp/foo.txt. Файла нет ни в одном lowerdir. Просто создаётся в upperdir. Никакого copy-up.

Сценарий 4: процесс удаляет файл из образа. rm /etc/nginx/nginx.conf. В upperdir создаётся whiteout-маркер — специальный character device (или файл с особым префиксом, зависит от драйвера) с тем же именем. Overlayfs, видя whiteout, считает что файл удалён, даже если он есть в lowerdirs. Read-only слой опять не тронут.

WARNING

Copy-on-write имеет цену: первая запись в большой файл из read-only слоя триггерит копирование целиком. Если у тебя 1GB-файл базы данных в образе и контейнер делает dd if=/dev/zero of=/var/lib/mysql/data.db bs=1M count=1, overlayfs скопирует весь гигабайт в writable layer перед записью. Поэтому изменяемые данные надо держать в volumes, а не внутри контейнера.


Что происходит при остановке и удалении

docker stop <id> посылает процессу SIGTERM (если процесс не реагирует за --stop-timeout секунд, далее SIGKILL). Процесс умирает, неймспейсы освобождаются. Но writable layer остаётся — он лежит на диске, занимает место. Контейнер в состоянии Exited можно перезапустить через docker start <id> — те же самые изменения, которые процесс наделал в файловой системе, будут на месте.

docker rm <id> удаляет writable layer (а заодно метадату контейнера). После этого восстановить состояние невозможно.

docker run --rm — флаг, который автоматически делает rm после exit процесса. В CI это must-have: интеграционные тесты должны быть stateless, после прогона ничего не остаётся.

$ docker run -d --name n1 nginx:1.27
$ docker exec n1 sh -c "echo 'changed' > /tmp/marker"
$ docker stop n1
$ docker start n1
$ docker exec n1 cat /tmp/marker
changed                       # writable layer сохранился через stop/start

$ docker rm -f n1
$ docker run -d --name n1 nginx:1.27
$ docker exec n1 cat /tmp/marker
cat: can't open '/tmp/marker': No such file or directory   # новый контейнер -- свой пустой layer

docker commit: образ из контейнера

Допустим, ты запустил контейнер, поставил внутри несколько пакетов, что-то отредактировал, и захотел “заморозить” этот state в новый образ. Для этого существует docker commit:

$ docker run -it --name lab1 ubuntu:24.04 bash
root@a1b2c3:/# apt update && apt install -y curl jq
root@a1b2c3:/# echo "custom config" > /etc/myapp.conf
root@a1b2c3:/# exit

$ docker commit lab1 my-ubuntu-tools:v1
sha256:8f9a2b3c4d5e...

$ docker images
REPOSITORY        TAG    IMAGE ID       CREATED          SIZE
my-ubuntu-tools   v1     8f9a2b3c4d5e   3 seconds ago    142MB
ubuntu            24.04  af1f4f...      4 weeks ago      78MB

Что произошло:

  1. Docker взял writable layer контейнера lab1
  2. Упаковал его в tar
  3. Сделал из этого новый read-only слой
  4. Создал новый image manifest, где layers = layers исходного ubuntu:24.04 + один новый слой
  5. Сохранил в локальный кэш с именем my-ubuntu-tools:v1

Теперь docker run my-ubuntu-tools:v1 запустит новый контейнер с уже установленными curl и jq и файлом /etc/myapp.conf.

DANGER

docker commit это инструмент для отладки и snapshot’ов, не для production. Образ, полученный через commit, не имеет описания того, как он был собран — нет Dockerfile, нет истории команд (точнее история есть, но без deterministic mapping на команды). Воспроизвести такой образ на другой машине невозможно. В продуктовых пайплайнах ВСЕГДА используй Dockerfile. Commit полезен только когда нужно зафиксировать промежуточное состояние для дебага.


Сколько ресурсов занимает контейнер

Слои образа на диске лежат один раз, независимо от количества контейнеров. Что добавляет каждый контейнер:

  • Writable layer: изначально 0 байт, растёт по мере записей. Для типичного “stateless” контейнера (Postgres с volume для данных) — единицы МБ.
  • Container metadata: ~10-50 KB в /var/lib/docker/containers/<id>/.
  • Network namespace и veth-pair: ~несколько KB ядерных структур.
  • Process memory: зависит от приложения, ничего общего со слоями.

Запустить 50 контейнеров одного образа на dev-машине — нормально и недорого. Запустить 50 разных образов — будет тяжелее, потому что каждый образ это отдельный набор слоёв.

Что добавляет каждый контейнер на диске
Image: 200MBодин разСлои образа: лежат на диске один раз, шарятся между всеми контейнерами этого образа.
Container 1+5MB writableПервый контейнер: 5MB diff в upperdir.
Container 2+3MB writableВторой контейнер из того же образа: свой writable layer. Образ не дублируется.
Container 3+10MB writableТретий контейнер: больше записей -- больше writable layer.
Total disk218MBСуммарно: 200MB образ + 18MB writable. Если бы каждый контейнер копировал образ целиком -- было бы 600MB+. Слои экономят место.

Попробуй сам

Понаблюдай за overlayfs изнутри:

# Запусти контейнер с подсчётом diff'ов
docker run -d --name probe alpine:3.21 sh -c "sleep 3600"

# Узнай ID и путь к merged
docker inspect probe --format '{{.GraphDriver.Data.MergedDir}}'
# /var/lib/docker/overlay2/abc123.../merged

# Изменим файл внутри контейнера
docker exec probe sh -c "echo 'hello' > /etc/hostname"

# Посмотрим, что появилось в upperdir
docker inspect probe --format '{{.GraphDriver.Data.UpperDir}}'
# /var/lib/docker/overlay2/abc123.../diff

# (на Linux) sudo ls -la /var/lib/docker/overlay2/abc123.../diff/etc/
# hostname -- новый файл, скопированный из lowerdir и изменённый

# Удаляем контейнер
docker rm -f probe
# upperdir исчезает вместе с контейнером

Если ты на macOS с Docker Desktop или OrbStack — всё это лежит внутри Linux VM, и достать оттуда напрямую можно только через docker run --privileged -v /var/lib/docker:/host alpine sh, что мы делать не будем. Но команды docker inspect работают и на mac.


Проверка знанийKnowledge check
Контейнер был запущен из образа python:3.13-slim. Процесс внутри установил несколько pip-пакетов и создал /data/cache в 500MB. Контейнер остановлен через docker stop. Через час пользователь делает docker start того же контейнера. Что произойдёт с /data/cache и pip-пакетами?
ОтветAnswer
Они останутся на месте. docker stop посылает SIGTERM процессу, но writable layer контейнера на диске не удаляется -- он лежит в /var/lib/docker/overlay2/. При docker start процесс перезапускается в тех же неймспейсах с тем же overlayfs-mount, видит все ранее созданные файлы. Writable layer удаляется только при docker rm. Замечание: 500MB в writable layer -- плохая идея, такие данные надо держать в volume, чтобы переживать docker rm и удобно бэкапиться.

Проверьте понимание

Результат: 0 из 0
Концептуальный
Вопрос 1 из 4. Что Docker создаёт при выполнении docker run образ:тег, относительно файловой системы?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 5