Как уже известно каждый Docker образ состоит из слоёв. Чем больше команд (инструкций) в Dockerfile тем больше слоёв, но не все команды создают слои.

Для примера возьмём Dockerfile который использует ubuntu и ставит nginx, при этом копируя файлы внутрь образа.

FROM ubuntu:22.04
RUN apt-get update && apt-get install -y nginx
COPY index.html /usr/share/nginx/html/
CMD ["nginx", "-g", "daemon off;"]

Если не понимаете что происходит:

Инструкция Что делает Создаёт слой?
1 FROM ubuntu:22.04 скачивает базовый слой (родительский образ) да
2 RUN apt-get install -y nginx установка nginx да
3 COPY копирование файла да
4 CMD команда запуска нет (метаданные)

Получается, что при сборке образа будет создано 3 слоя и сейчас я это проверю, выполнив команду сборки образа.

docker image layers

docker build . -t mybuild:1.0
=> [1/3] FROM docker.io/library/ubuntu:22.04@sha256:09506232a8004baa32c47d68f1e5c307d648fdd59f5e7eaa42aaf87914100db3
=> [2/3] RUN apt-get update && apt-get install -y nginx
=> [3/3] COPY index.html /usr/share/nginx/html/
 => => writing image sha256:17a8b92dd8073184d570a69167c3862f633c69c90e49fbb5d745c56e870fd6f7 

Каждая инструкция (RUN, COPY, ADD, и т.д.) в Dockerfile создаёт слой.

Как хранятся слои в Docker

Каждый слой представляет из себя директорию со сжатыми данными в ОС, где стоит Docker. Путь к этой директории /var/lib/docker/overlay2/.

docker image folders in os

Но если заглянуть в эту директорию, не зная, что и где искать ничего особенно там не найти. Ну а для того чтобы понять в какой директории какой слой необходимо:

  1. Вывести слои образа
    docker inspect --format='' mybuild:1.0 | jq
    
    ["sha256:767e56ba346ae714b6e6b816baa839051145ed78cfa0e4524a86cc287b0c4b00",
      "sha256:b3e9f88953eff8bf59b0042d340cbc16daf139665697ebf2ee741edd3c8b3b18",
      "sha256:a04b6207eb080660102f534250f1e473474068e2f41baa0f2624279036ee75b5"]
    
  2. Выполняем поиск по хэшу слоя
    udo grep -rnw /var/lib/docker/image/overlay2/layerdb/sha256/ -e b3e9f88953eff8bf59b0042d340cbc16daf139665697ebf2ee741edd3c8b3b18
    
    /var/lib/docker/image/overlay2/layerdb/sha256/3cf6bc112ebdd32ca7b08b5c40a279e54ec48ae101ccae6e78493293e5862640/diff:
    

Скорее всего необходимости лезть вручную в эти директории и не будет, но знать где лежат слои физически лишним не будет.

Как работает кэш в Docker

При сборке Docker образа (docker build) Docker сравнивает каждую инструкцию с ранее построенными образами, точнее с их слоями. Если слой не изменился, то используется кэш, т.е. уже существующий слой. Так как у каждого слоя есть хэш, Docker при сборке вычисляет хэш всех изменений инструкции (SHA256) и сравнивает с существующими слоями. Т.е. если другой образ использует тот же слой (т.е. ту же комбинацию базового слоя + команды + контента) **, Docker **возьмёт этот слой из кэша.

Docker считает, что два слоя эквивалентны, если совпадают:

  • хэш предыдущего слоя (родителя);
  • команда (RUN, COPY, ADD и её аргументы);
  • содержимое добавляемых файлов (если есть).

Если всё это совпадает — Docker просто переиспользует существующий слой из другого образа. Т.е. благодаря кэшированию происходит оптимизация использования дискового пространства. Также при наличии кэша нет необходимости скачивать одни и и те же слои с Docker hub или создавать их заново что ускоряет процесс сборки образа.

Для примера я создам два Dockerfile с парочкой одинаковых инструкций и соберу два разных образа.

📜 Dockerfile.1

FROM python:3.11-slim
RUN pip install flask==3.0.0

📜 Dockerfile.2

FROM python:3.11-slim
RUN pip install flask==3.0.0     # <-- точно та же команда
COPY app/ /app/
CMD ["python", "/app/main.py"]

Выполню сборку первого образа с именем cache1:1.0.

docker build --progress=plain -f Dockerfile.1 -t cache1:1.0 .
[1/2] FROM docker.io/library/python:3.11-slim@sha256:8eb5fc663972b871c528fef04be4eaa9ab8ab4539a5316c4b8c133771214a61 DONE 6.3s
[2/2] RUN pip install flask==3.0.0 DONE 5.3s

Выполню сборку второго образа с именем cache2:1.0.

docker build --progress=plain -f Dockerfile.2 -t cache2:1.0 .
[1/3] FROM docker.io/library/python:3.11-slim@sha256:8eb5fc663972b871c528fef04be4eaa9ab8ab4539a5316c4b8c133771214a617 DONE 0.0s
[2/3] RUN pip install flask==3.0.0 CACHED
[3/3] COPY app/ /app/ DONE 0.0s 

Из команды выше уже видно, что для сборки использовались 2 слоя из кэша, хотя в первом слое об этом и не написано. Также выполню docker inspect чтобы сравнить слои образа cache2 с cache1.

docker inspect --format='' cache1:1.0 | jq
  "sha256:d7c97cb6f1fe7cae982649e9f55efe201212e8acaa64bd668c083b204e4efd4c",
  "sha256:15eb6aec49b38357916ea069cb57c890841f4e40588c54ebba117f6314911d37",
  "sha256:9ba402f61141e835fb247b8592f61ea2a885de652bbb368fe9cea93cbc8c324a",
  "sha256:235e8192987624c55713750efe67254a8dbfe540c7a59c1d33353f928e42d80d",
  "sha256:556dcdfbcdbc51dab4c5310208f5b09b7a03e7d9a627cd0defc8d2f5052e78d8"
docker inspect --format='' cache2:1.0 | jq
  "sha256:d7c97cb6f1fe7cae982649e9f55efe201212e8acaa64bd668c083b204e4efd4c",
  "sha256:15eb6aec49b38357916ea069cb57c890841f4e40588c54ebba117f6314911d37",
  "sha256:9ba402f61141e835fb247b8592f61ea2a885de652bbb368fe9cea93cbc8c324a",
  "sha256:235e8192987624c55713750efe67254a8dbfe540c7a59c1d33353f928e42d80d",
  "sha256:556dcdfbcdbc51dab4c5310208f5b09b7a03e7d9a627cd0defc8d2f5052e78d8",
  "sha256:0b636ec1d5fad65a1c6d8512c0ce7535824bab1c7700fb0b1c9b473ae54ea594"

Из вывода выше можно отметить что:

  1. При сборке cache1:1.0 все 5 слоёв были созданы с нуля.
  2. При сборке cache2:1.0 Docker проверил каждый шаг сверху вниз:
Слой (sha256) Новый слой? Комментарий
d7c97cb6 нет (беру из кэша) базовый образ python:3.11-slim
15eb6aec нет (беру из кэша) слой из Python базового образа
9ba402f6 нет (беру из кэша) слой из Python базового образа
235e8192 нет (беру из кэша) слой из Python базового образа
556dcdfb нет (беру из кэша) слой RUN pip install flask==3.0.0
0b636ec1 да (создаю новый) слой COPY app/ /app/

docker layer's cache

Порядок слоёв и кэш

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

📜 Dockerfile.2

FROM python:3.11-slim
RUN pip install php
RUN pip install flask==3.0.0
COPY app/ /app/
CMD ["python", "/app/main.py"]

Теперь если я соберу снова образ, то из кэша будет использован только один слой (FROM python:3.11-slim).

ocker build -f Dockerfile.2 -t cache2:1.0 .
 => CACHED [1/4] FROM docker.io/library/python:3.11-slim@sha256:8eb  0.0s                                        0.0s
 => [2/4] RUN pip install php                                        3.4s
 => [3/4] RUN pip install flask==3.0.0                               4.3s
 => [4/4] COPY app/ /app/                                            0.2s

Поэтому рекомендуется при написании Dockerfile инструкции, которые могут меняться прописывать максимально низко в файле.

ИНВАЛИДАЦИЯ КЭША DOCKER

Как собрать образ Docker без использования кэша

Если вдруг необходимо провести сборку образа без использования кэша нужно использовать флаг --no-cache.

docker build --no-cache -f Dockerfile.2 -t cache2:1.0 .

Когда бывает необходимо не использовать кэш:

  • Когда необходимо обновить пакеты, т.е. получить актуальные версии из репозиториев
  • Если базовый образ был обновлён и необходимо получить свежий базовый образ
  • Когда сборка всегда строиться с нуля, независимо от предыдущего состояния (CI/CD)

Но если необходимо просто получить свежий базовый образ лучше использовать флаг --pull.

Слой контейнера

При создании контейнера поверх слоёв с образом создаётся новый слой в статусе read/write, т.е. слой перезаписываемый, в отличии от слоёв образа, которые в статусе read-only.

Т.е. если я создам контейнер, то произойдёт примерно следующее:

image layer 1  (базовый слой)
image layer 2
image layer 3
------------------------------
container layer (read-write)

Этот верхний слой контейнера хранится отдельно в директории /var/lib/docker/overlay2/. Все манипуляции внутри контейнера сохраняются в слое самого контейнера, а не в слоях образа.

Слой контейнера в DOCKER

docker inspect some-alpnginx | jq '.[0].GraphDriver.Data'
  "LowerDir": "/var/lib/docker/overlay2/b76e86ac1387c177bb70ef2f1c0cdc58b86c228af2b890f474e76d13daf9c803-init/diff:/var/lib/docker/overlay2/a2168b993b381f985089daddef7bfc32fa15eae64575e3661b0e028c7ddf8bcc/diff:/var/lib/docker/overlay2/69e1478c7781777eaec105040d2ee9b8397882ddb8e1f320e399f3a7fbd1230c/diff:/var/lib/docker/overlay2/abb7d7bf0ad7f1b823f223652ccd1dc0f9aecdb5e6a3903a77e3832e1d0342d1/diff:/var/lib/docker/overlay2/2b96faabbd98c4847e4ff63598d11d551eddf829b0902f2fb74153247e95f927/diff:/var/lib/docker/overlay2/5d939a6cb84023566cb2513327474b7f029d69016f57ea4fec8d4f174646dc63/diff:/var/lib/docker/overlay2/d305392fcd4d3f4fa441e7cdb71b7dd41b0b77f767293735b393b9c54404312c/diff:/var/lib/docker/overlay2/c5865d2ecfa2c81b42ffeb09755886e9b907c1ac0b48aa9cd57d4cd2afb07263/diff:/var/lib/docker/overlay2/654377eb111af010abfab1e8293f37b7729584d041fe5e79007fb91293d9a75e/diff",
  "MergedDir": "/var/lib/docker/overlay2/b76e86ac1387c177bb70ef2f1c0cdc58b86c228af2b890f474e76d13daf9c803/merged",
  "UpperDir": "/var/lib/docker/overlay2/b76e86ac1387c177bb70ef2f1c0cdc58b86c228af2b890f474e76d13daf9c803/diff",
  "WorkDir": "/var/lib/docker/overlay2/b76e86ac1387c177bb70ef2f1c0cdc58b86c228af2b890f474e76d13daf9c803/work"

Этот верхний слой контейнера хранится отдельно в директории /var/lib/docker/overlay2/. Все манипуляции внутри контейнера сохраняются в слое самого контейнера, а не в слоях образа. Тут у контейнера 4 пути:

  • LowerDir - Слои образа (read-only)
  • UpperDir - Слой контейнера (read-write). Всё, что ты изменяешь внутри контейнера, записывается именно сюда.
  • MergedDir - Смонтированное объединение всех слоёв
  • WorkDir - Техническая папка OverlayFS

Для того чтобы понять, что в какой директории лежит создам в контейнере новый файл:

docker exec some-alpnginx sh -c "echo test >> /etc/nginx/1.txt"

Этот файл будет сохранён в директории UpperDir контейнера, проверю что он там:

sudo cat /var/lib/docker/overlay2/b76e86ac1387c177bb70ef2f1c0cdc58b86c228af2b890f474e76d13daf9c803/diff/etc/nginx/1.txt
test