Dev

Docker Buildkit: Правильное использование --mount=type=cache

TL;DR Содержимое каталогов, смонтированных через --mount=type=cache, не сохраняется в docker-образе, поэтому кэшировать надо не целевые каталоги, а промежуточные.

В dockerfile:1.3 появилась возможность монтировать файловые системы во время построения образа, в том числе и в целях кэширования: можно кэшировать скачанные пакеты или промежуточные артефакты компиляции.

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

# syntax=docker/dockerfile:1.3
FROM python:3.10

RUN mkdir /pip-packages

RUN --mount=type=cache,target=/pip-packages \
      pip install --target=/pip-packages uwsgi
> docker build -t pip-cache -f Dockerfile.pip .
# ...
[+] Building 14.6s (7/7) FINISHED

Выглядит, что все прошло успешно, но целевой каталог пуст:

> docker run -it --rm pip-cache ls -l /pip-packages
total 0

Что-то явно пошло не так. Во время сборки было видно, что uWSGI компилируется и устанавливается. Можно даже это проверить, добавив ls в процесс сборки:

RUN --mount=type=cache,target=/pip-packages \
      pip install --target=/pip-packages uwsgi \
      && ls -1 /pip-packages
> docker build -t pip-cache --progress=plain -f Dockerfile.pip .
<...>
#6 12.48 Successfully installed uwsgi-2.0.20
<...>
#6 12.91 __pycache__
#6 12.91 bin
#6 12.91 uWSGI-2.0.20.dist-info
#6 12.91 uwsgidecorators.py
#6 DONE 13.0s
<...>

Всё на месте. Но нет, в конечном образе снова пусто:

> docker run -it --rm pip-cache ls -l /pip-packages
total 0

А дело в том, что каталог /pip-packages, находящийся внутри образа, и каталог, который указан в RUN --mount=type=cache,target=<dirname>, разные. Попробуем что-нибудь положить в него заранее и посмотрим, как меняется его содержимое в процессе сборки:

RUN mkdir /pip-packages \
    && touch /pip-packages/foo \
    && ls -1 /pip-packages

RUN --mount=type=cache,target=/pip-packages \
    ls -1 /pip-packages \
    && pip install --target=/pip-packages uwsgi \
    && ls -1 /pip-packages

RUN ls -1 /pip-packages
> docker build -t pip-cache --progress=plain -f Dockerfile.pip-track .
<...>
#5 [stage-0 2/4] RUN mkdir /pip-packages
      && touch /pip-packages/foo
      && ls -1 /pip-packages
#5 sha256:fb542<...>
#5 0.211 foo  👈1️⃣
#5 DONE 0.2s

#6 [stage-0 3/4] RUN --mount=type=cache,target=/pip-packages
      ls -1 /pip-packages
      && pip install --target=/pip-packages uwsgi
      && ls -1 /pip-packages
#6 sha256:10ed6<...>
#6 0.292 __pycache__            👈2️⃣
#6 0.292 bin
#6 0.292 uWSGI-2.0.20.dist-info
#6 0.292 uwsgidecorators.py
#6 2.802 Collecting uwsgi       🤔3️⃣
#6 3.189   Downloading uwsgi-2.0.20.tar.gz (804 kB)
#6 4.400 Building wheels for collected packages: uwsgi
<...>
#6 13.34 __pycache__            👈4️⃣
#6 13.34 bin
#6 13.34 uWSGI-2.0.20.dist-info
#6 13.34 uwsgidecorators.py
#6 DONE 13.4s

#7 [stage-0 4/4] RUN ls -1 /pip-packages
#7 sha256:fb6f4<...>
#7 0.227 foo  👈5️⃣
#7 DONE 0.2s
<...>
  • 1️⃣ файл foo успешно создан
  • 2️⃣ примонтировался каталог с результатами предыдущего docker build, и файла foo там нет
  • 3️⃣ uWSGI снова скачивается, компилируется и устанавливается
  • 4️⃣ в каталоге появилась обновленная сборка uWSGI
  • 5️⃣ в каталоге остался только файл foo

Это означает, что --mount=type=cache работает только в контексте одной инструкции RUN, заменяя директорию, созданную внутри образа RUN mkdir /pip-packages, и затем возвращая ее обратно. При этом кэширование оказалось неэффективным, потому что pip заново установил uWSGI с полной компиляцией.

В данном случае корректно было бы кэшировать не целевую директорию, а /root/.cache, в которую pip складывает все артефакты:

RUN --mount=type=cache,target=/root/.cache \
    pip install --target=/pip-packages uwsgi
> docker build -t pip-cache -f Dockerfile.pip-right .
> docker run -it --rm pip-cache ls -1 /pip-packages
__pycache__
bin
uWSGI-2.0.20.dist-info
uwsgidecorators.py

Теперь все на месте, установленные пакеты никуда не делись.

Проверим эффективность кэширования, добавив пакет requests:

RUN --mount=type=cache,target=/root/.cache \
    pip install --target=/pip-packages uwsgi requests
                                                👆
> docker build -t pip-cache --progress=plain -f Dockerfile.pip-right .
<...>
#6 6.297 Collecting uwsgi
#6 6.297   Using cached uWSGI-<...>.whl  👈
#6 6.561 Collecting requests
#6 6.980   Downloading requests-2.27.1-py2.py3-none-any.whl (63 kB)
<...>

pip взял заранее собранный wheel-файл из /root/.cache и установил из него готовый к использованию пакет.

Все исходники доступны на GitHub.