Ставим Docker на диету или Docker Slim Image
Fri 03 February 2023Сегодня хотелось бы рассказать о том, как канонически правильно писать Dockerfile и о сборке контейнеров Docker. Можно сказать это некие лучшие практики для написания Dockerfile. Ведь как бы в последствии не использовали Docker, в обычном docker compose или уже работаете в Kubernetes, контейнер всегда будет являться сущностью! И так, для начала, я рекомендовал бы максимально отказаться от образов на базе Debian/Ubuntu и использовать для работы Alpine Linux, почему? Потому что последний является очень маленьким по объему. Базовый образ Alpine занимает всего 8 мегабайт. Против почти 300 на Ubuntu/Debian. Для многих из вас эти размеры кажутся незначительными, но когда дело дойдет до сборки пару тысяч образов, это будет иметь большое значение. Особенно если ваша инфраструктура построена по всем принципам лучших практик, то есть имеет 3 уровня разработки [sandbox, stage, production], и на каждом уровне ваши разработчики будут использовать контейнеры, которые будут собираться автоматически вашим инструментом CI/CD.
Из всех инструкций Dockerfile, например ENV, ARG, VOLUME не занимают значительного размера в образе, а инструкции COPY, ADD и RUN, и особенно RUN, будут занимать самый большой объем в ваших файлах. Конечно, не считая тех пакетов, которые будут установлены в систему для работы вашего приложения.
Рассмотрим простой пример, напишем следующий Dockerfile:
FROM alpine:latest
RUN apk update
RUN apk add curl
RUN curl -SsL https://nodejs.org/dist/v18.14.0/node-v18.14.0-linux-x64.tar.xz -o /tmp/node-v18.14.0-linux-x64.tar.xz
После чего соберем его и посмотрим его размер, часть вывода на экран я убрал чтобы строк было не так много:
>>> docker build . --tag node
[+] Building 17.6s (8/8) FINISHED
=> [internal] load build definition from Dockerfile 0.0s
=> => transferring dockerfile: 212B 0.0s
=> [internal] load .dockerignore 0.0s
=> => transferring context: 2B 0.0s
=> [internal] load metadata for docker.io/library/alpine:latest 3.2s
=> [2/4] RUN apk update 2.1s
=> [3/4] RUN apk add curl 2.2s
=> [4/4] RUN curl -SsL https://nodejs.org/dist/v18.14.0/node-v18.14.0-linux-x64.tar.xz -o /tmp/node-v18.14.0-linux-x64.tar.xz 8.1s
=> exporting to image 0.1s
=> => exporting layers 0.1s
=> => writing image sha256:23d11cd11f05b68101988440f6db69ef959110925b4ea48398defbfe54631b52 0.0s
=> => naming to docker.io/library/node 0.0s
>>> docker history node
IMAGE CREATED CREATED BY SIZE COMMENT
23d11cd11f05 4 minutes ago RUN /bin/sh -c curl -SsL https://nodejs.org/… 23.5MB buildkit.dockerfile.v0
<missing> 4 minutes ago RUN /bin/sh -c apk add curl # buildkit 2.4MB buildkit.dockerfile.v0
<missing> 4 minutes ago RUN /bin/sh -c apk update # buildkit 2.57MB buildkit.dockerfile.v0
<missing> 3 weeks ago /bin/sh -c #(nop) CMD ["/bin/sh"] 0B
<missing> 3 weeks ago /bin/sh -c #(nop) ADD file:3080f19f39259a4b7… 7.46MB
Давайте посмотрим размер полученного нами образа:
>>> docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
node latest 23d11cd11f05 9 minutes ago 36MB
Как мы можем видеть каждый следующий слой занимает место в контейнере и более того после того, как мы получили пакет, нам уже не нужна утилита curl, верно? Что же мы можем сделать? Первое что вы всегда должны делать, это избавляться от лишних слоев, объединяя то, что можно объединить. Например, мы можем и должны собрать все RUN в одну строку. И конечно мы должны удалить утилиту curl после того как она уже не будет нам нужна, и новый Dockerfile будет выглядеть следующим образом:
FROM alpine:latest
RUN apk update && \
apk add curl && \
curl -SsL https://nodejs.org/dist/v18.14.0/node-v18.14.0-linux-x64.tar.xz -o /tmp/node-v18.14.0-linux-x64.tar.xz && \
apk del curl
>>> docker build . --tag node1
[+] Building 12.7s (6/6) FINISHED
=> [internal] load build definition from Dockerfile 0.0s
=> => transferring dockerfile: 235B 0.0s
=> [internal] load .dockerignore 0.0s
=> => transferring context: 2B 0.0s
=> [internal] load metadata for docker.io/library/alpine:latest 0.8s
=> CACHED [1/2] FROM docker.io/library/alpine:latest@sha256:f271e74b17ced29b915d351685fd4644785c6d1559dd1f2d4189a5e851ef753a 0.0s
=> [2/2] RUN apk update && apk add curl && curl -SsL https://nodejs.org/dist/v18.14.0/node-v18.14.0-linux-x64.tar.xz -o /tmp/node-v18.14.0-linux-x6 11.7s
=> exporting to image 0.1s
=> => exporting layers 0.1s
=> => writing image sha256:0e1a26e3f7f34e4e2ad75188a7bb6d67636bc091e2399cd892edae7d9797f204 0.0s
=> => naming to docker.io/library/node1 0.0s
Теперь, мы снова посмотрим на размер нашего образа:
>>> docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
node1 latest 0e1a26e3f7f3 About a minute ago 33.8MB
node latest 23d11cd11f05 17 minutes ago 36MB
Но и это ещё не все, ведь мы обновили дерево пакетов, установили утилиту и после всех действий у нас остался кеш пакетов, он не нужен нам в образе, по этому допишем ещё пару строк к нашему RUN.
FROM alpine:latest
RUN apk update && \
apk add curl && \
curl -SsL https://nodejs.org/dist/v18.14.0/node-v18.14.0-linux-x64.tar.xz -o /tmp/node-v18.14.0-linux-x64.tar.xz && \
apk del curl && \
rm -rf /var/lib/apt/lists/* && \
rm /var/cache/apk/*
Соберем еще один образ и сравним размеры:
>>> docker build . --tag node2
[+] Building 13.8s (6/6) FINISHED
=> [internal] load build definition from Dockerfile 0.0s
=> => transferring dockerfile: 311B 0.0s
=> [internal] load .dockerignore 0.0s
=> => transferring context: 2B 0.0s
=> [internal] load metadata for docker.io/library/alpine:latest 0.9s
=> CACHED [1/2] FROM docker.io/library/alpine:latest@sha256:f271e74b17ced29b915d351685fd4644785c6d1559dd1f2d4189a5e851ef753a 0.0s
=> [2/2] RUN apk update && apk add curl && curl -SsL https://nodejs.org/dist/v18.14.0/node-v18.14.0-linux-x64.tar.xz -o /tmp/node-v18.14.0-li 12.7s
=> exporting to image 0.1s
=> => exporting layers 0.1s
=> => writing image sha256:a6a33f13aacff2ea5e5954df3b863648de1fdf9ad034e6c01e8a9c29994b20ea 0.0s
=> => naming to docker.io/library/node2 0.0s
>>> docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
node2 latest a6a33f13aacf 14 seconds ago 31.2MB
node1 latest 0e1a26e3f7f3 8 minutes ago 33.8MB
node latest 23d11cd11f05 23 minutes ago 36MB
Мы освободили более 5 мегабайт из образа. И снова вы можете сказать что это очень мало, но если вы соберете 100 таких образов, это уже 500 мегабайт и разница уже значительна, потому что на скачивания из репозитория будет тратиться меньше времени за счет небольшого размера, а значит процесс разработки и развертывания станет быстрее!
Немаловажным будет так же заметить, что последовательность инструкций имеет значение. В следующей последовательности:
- FROM
- ENV или ARG
- RUN
- COPY или ADD
- CMD
Каждый шаг будет кеширован в образе и что бы оптимизировать этот кеш и уменьшить размер, последовательность всегда будет иметь значение!
Теперь главные советы по-порядку:
- Всегда, по возможности, используйте официальные образы, выбирая из них наиболее маленькие по размеру.
- Порядок инструкций в Dockerfile имеет значение
- Объединяйте шаги, которые возможно объединить.
- Удаляйте ненужный кеш менеджеров пакетов, как и сами пакеты, которые больше не будут нужны в работе образа.
- Если RUN требует более сложной конструкции или условий, вы так же можете создать bash/sh скрипт который можете использовать для установки и настройки среды.
Ссылки по теме:
Home »