Файловая система образа: слои и overlay

Откуда берётся «слоёная» файловая система образа и почему контейнеры такие лёгкие на диске.

OverlayFS — это union-файловая система Linux: она «склеивает» несколько каталогов в один, где верхний слой перекрывает нижние. На ней Docker строит образы из read-only слоёв и тонкий writable-слой контейнера.

Зачем это знать на практике

Понимание слоёв напрямую экономит время и место. Оно объясняет, почему изменение одной строки в Dockerfile ломает кеш и пересобирает половину образа; почему десять контейнеров из одного образа занимают на диске почти столько же, сколько один; и почему данные, записанные внутрь контейнера, исчезают после его удаления. Без этой модели оптимизация Dockerfile превращается в шаманство.

Образ — это стопка слоёв

Каждая инструкция в Dockerfile, меняющая файловую систему (RUN, COPY, ADD), порождает новый слой — архив с разницей: какие файлы добавлены, изменены или удалены относительно предыдущего слоя. Образ — это упорядоченная стопка таких слоёв плюс конфигурация. Слои read-only и переиспользуются между образами: если два образа основаны на ubuntu:22.04, базовые слои хранятся на диске один раз.

FROM ubuntu:22.04          # базовые слои
RUN apt-get update         # слой со списком пакетов
RUN apt-get install -y curl # слой с установленным curl
COPY app.py /app/          # слой с вашим файлом

Историю слоёв и их размер показывает docker history:

docker history nginx:latest
# IMAGE  CREATED  CREATED BY                       SIZE
# <...>  ...      /bin/sh -c apt-get install ...   54MB
# <...>  ...      /bin/sh -c #(nop) COPY ...        1.1kB

OverlayFS: как слои становятся одной ФС

Контейнеру нужна обычная единая файловая система, а не стопка архивов. Их объединяет драйвер хранилища overlay2 поверх OverlayFS. В терминах overlay есть нижние слои (lowerdir, read-only слои образа), верхний слой (upperdir, writable-слой контейнера) и результат их объединения (merged) — то, что контейнер видит как /.

         merged (то, что видит контейнер как «/»)
         ▲ объединение
┌────────┴─────────┐
upperdir  (writable-слой контейнера, copy-on-write)
lowerdir  (read-only слои образа: ubuntu + apt + curl + app.py)

Если файл есть только в нижнем слое — контейнер читает его оттуда. Если файл создан или изменён в контейнере — он лежит в upperdir и перекрывает версию из нижних слоёв. Удаление файла из read-only слоя реализуется специальным «whiteout»-файлом в верхнем слое, который прячет нижний.

Copy-on-write: почему запись «дешёвая», но не бесплатная

Главный приём слоистой ФС — copy-on-write (CoW). Пока контейнер только читает файл из образа, никакого копирования нет: данные берутся из общего read-only слоя. Но как только контейнер впервые изменяет файл, overlay копирует его целиком из нижнего слоя в upperdir и правит уже копию.

# большой файл лежит в образе (read-only слой)
docker run -it --name demo someimage bash
echo " " >> /var/big.log   # дозапись 1 байта → весь big.log скопируется в upperdir

Отсюда два практических вывода. Во-первых, запуск десяти контейнеров из одного образа почти ничего не стоит по диску: у каждого лишь свой пустой upperdir, а тяжёлые слои общие. Во-вторых, активная запись и изменение больших файлов внутри контейнера раздувает его writable-слой и медленнее, чем работа на обычной ФС, — поэтому базы данных и часто меняющиеся данные держат в томах (volumes), которые минуют слоистую ФС.

Тот же механизм объясняет и поведение кеша при сборке. Каждый слой адресуется по содержимому, поэтому при повторном docker build Docker переиспользует ранее собранные слои, пока инструкция и её входные данные не изменились. Стоит измениться файлу, который копирует COPY, — и этот слой, а вместе с ним все последующие, пересобираются заново. Поэтому порядок инструкций в Dockerfile — это не косметика, а прямое управление тем, что попадёт в кеш, а что будет пересчитано при каждой правке кода.

Writable-слой и эфемерность данных

Всё, что контейнер пишет в свою ФС, оседает в его writable-слое. Этот слой принадлежит конкретному контейнеру и удаляется вместе с ним. Два контейнера из одного образа имеют независимые writable-слои и не видят изменений друг друга. Что именно изменилось относительно образа, показывает docker diff:

docker diff demo
# A /app/cache       (Added — добавлено)
# C /etc/nginx.conf  (Changed — изменено)
# D /tmp/old.txt     (Deleted — удалено)

Как это работает под капотом

OverlayFS — это модуль ядра Linux, а не выдумка Docker. Драйвер overlay2 при старте контейнера монтирует overlay-точку, указывая ядру список lowerdir (слои образа сверху вниз) и upperdir (writable-слой). Ядро само разрешает, из какого слоя брать каждый файл. Слои на диске лежат в /var/lib/docker/overlay2/ и адресуются по содержимому (хеш), поэтому одинаковые слои дедуплицируются автоматически.

Частые ошибки

  • Хранить важные данные в writable-слое. После docker rm они пропадают. Для постоянных данных — тома.
  • Ломать кеш слоёв сборки. Если COPY . . идёт до установки зависимостей, любая правка кода инвалидирует слой и переустанавливает пакеты заново. Копируйте сначала файл зависимостей, ставьте их, потом копируйте остальной код.
  • Удалять файлы отдельной инструкцией RUN. Удаление в новом слое не уменьшает образ: данные остаются в нижнем слое, а сверху лишь whiteout. Создание и удаление надо делать в одной инструкции.
  • Активно писать большие файлы в CoW-слой. Каждая первая запись копирует файл целиком; для нагруженного I/O используйте том.

Итоги

  • Образ — стопка read-only слоёв; каждая меняющая ФС инструкция Dockerfile добавляет слой.
  • OverlayFS объединяет read-only слои (lowerdir) и writable-слой контейнера (upperdir) в единую ФС.
  • Copy-on-write делает чтение бесплатным, а первую запись файла — копированием его в верхний слой.
  • Writable-слой эфемерен и удаляется с контейнером; docker history показывает слои, docker diff — изменения относительно образа.
Проверьте себя
1. Что происходит, когда контейнер впервые изменяет файл, лежащий в read-only слое образа?
AФайл редактируется прямо в слое образа
BИзменение теряется, файлы образа неизменяемы
CФайл целиком копируется в writable-слой (copy-on-write), правится уже копия
DСоздаётся новый слой образа
2. Почему данные, записанные внутрь контейнера, исчезают после docker rm?
AОни лежат в writable-слое, который принадлежит контейнеру и удаляется вместе с ним
BDocker шифрует их и теряет ключ
CОни пишутся в оперативную память
DИх удаляет сборщик мусора образов
3. Какая команда показывает слои образа и их размер?
Adocker diff
Bdocker history
Cdocker inspect
Ddocker layers