Как стать автором
Обновить

Три простых приема для уменьшения Docker-образов

Время на прочтение 8 мин
Количество просмотров 23K
Всего голосов 26: ↑22 и ↓4 +18
Комментарии 28

Комментарии 28

На самом деле в этом есть и плюс, ведь если вдруг какой-то злоумышленник сможет получить доступ к контейнеру, он причинит намного меньше вреда, чем если бы у него был доступ к shell

Замуровать свой дом, чтобы ничего не украли? Так себе плюс. Хотя экономия 500 МБ конечно существенна.
Согласен. Вообще статья выглядит как реклама distroless. Гуголь в каждый дом )

На самом деле микроменеджмент докер-образов реально помогает. И тут есть больше возможностей, чем в статье.
1. всегда существует squash. У него минус в том, что он схлопывает все слои и поэтому суммарный объем ВСЕХ докер-образов в системе может увеличиться (т.к. нет общих слоев).
2. правильная расстановка requirements.txt, npm install и пр. в файле (лучше максимально в конец, но еще до внедрения пользовательского кода).
3. чистить в том же блоке RUN зависимости, которые нужны для сборки образа (убираем кэши и пр.). Про мультистейдж, кстати, сказали — супер!
и многое-многое другое.
Alpine Linux — клёвая вещь! По возможности на нём всё делаю, но надо быть осторожнее. Допустим, тех же библиотек для PostGIS в репах нет, и приходится шаманить. Не скажу, что это проблема, но легко на создание образа вместо 20 минут можно потратить 3 часа.
У alpine есть недостаток. На маленьких и простых образах — alpine впереди, образ реально меньше debian/ubuntu. Но если alpine накачать python + numpy + scikit + pandas и прочую дичь, то размер получается такой же как у соответствующего образа на ubuntu + в минусах более долгое время сборки на alpine.
Это уже после удаления зависимостей компиляции и очистки кеша? Имею ввиду вот это:
RUN apk update \
&& apk add --no-cache --virtual .build-deps libffi-dev build-base zlib-dev jpeg-dev \
&& pip install -r /tmp/requirements.txt --no-cache-dir \
&& apk del .build-deps

я, по-моему, вполне конкретно высказался.
Да, это после удаления зависимостей.
А еще Вы в курсе, что как Вы делаете — не стоит так? Потому что в Вашем сниппете инструкция RUN будет каждый раз выполняться при изменении requirements.txt. Т.е. мы сильно проигрываем в двух вещах: в кэшировании последующих слоев. И во времени сборки.


И еще подтвержу свою точку зрения ссылкой на статью https://habr.com/ru/post/415513/
Вывод простой — чем больше пакетов — тем меньше выгода от alpine.

Про удаление кеша Вы не сказали, так что я предположил.
Ну и про RUN спасибо, конечно, но знаю. Это кусок из базового образа для продукта, который обновляется вручную только при изменении зависимостей. В CI/CD у нас собирается другой, который основан на этом.
Так какой же базовый образ выбрать?

Здесь огромная ошибка. Нужно для dev и prod выбирать одно и то же. Они могут отличаться способом внедрения исходников, но базовый образ и все библиотеки должны быть одни и те же. Иначе у вас получаются разные окружения с соответствующими последствиями.

Я соглашусь. Но можно попробовать сделать как. dev — пускай разрабы делают что угодно и как им удобно. Но вот тогда нам понадобится еще одна pre-prod среда (stage? uat?), в которой образы будут уже собираться "по науке" и идентично prod'у.


Иначе у вас получаются разные окружения с соответствующими последствиями.
prod никогда не будет 100% такой же, как и тестовая среда. Всегда придется чем-то жертвовать. Тем же объемом вливаемых данных в среду. Иначе держать две полностью идентичные среды с зеркалированием трафика влетает в очень большую копеечку и попросту не нужно. Поэтому задача админов, программистов — выбрать те параметры, критерии, которые не очень существенны. И именно ими пожертвовать при моделировании тестовой среды.
Верно, но можно перестать кататься на квадратных колесах и сразу сесть за круглые.
У нас на сервисе, например, построено примерно так:
1. Есть базовый образ с необходимым ЯП, модулями пр. зависимостями. Эти образы меняются крайне редко.
2. dev работает именно на этих образах путем монтирования в них исходников с машины разработки.
3. prod образы билдятся на CI/CD, но содержат только ADD исходников в образ. Больше никаких изменений не допускается.
4. Все конфиги (nginx, mysql, php, webpack и т.д.) вынесены в docker config. Единственный минус, докер не может их обновлять, только менять имя или удалять/создавать.

Каких-то проблем с такой конфигурацией не наблюдается. Размер образов может быть внушительный, но, честно говоря, не на столько, чтобы создавать этим проблемы.
Размер образов может быть внушительный, но, честно говоря, не на столько, чтобы создавать этим проблемы.

Соглашусь, что это не является проблемой, если речь не идет про образы по 5ГиБ, но ведь есть и такие кейсы.
И легко можно потратиться на трафик, если будет куча скачиваний.
https://www.brianchristner.io/docker-image-base-os-size-comparison/
https://nickjanetakis.com/blog/the-3-biggest-wins-when-using-alpine-as-a-base-docker-image

bash в Alpine «называется» ash.

ну-ну


/ # mbp-gaal:~ gaal$ docker run -it --rm alpine /bin/sh
/ # readlink /bin/sh
/bin/busybox
/ # which ash
/bin/ash
/ # readlink /bin/ash
/bin/busybox```
Пройдите этот квест дальше. Следующий этап: найдите информацию о командной оболочке, которая используется в busybox.

Можете продолжить за меня.
И еще — если бы bash там был полноценный, то не пришлось делать apk add bash, чтобы работали стандартные баш-скрипты....

Я новичок в docker и не понял из статьи 1 вещь. Зачем мы используем вначале node:8 (с Ubuntu, 700+ MiB) а потом во второй стадии node:alpine (50+ MiB), если… мы просто можем без всяких стадий сразу взять alpine и наш npm install сделать уже прямо там по месту.


Я столкнулся с парой проблем, когда npm install в alphine не мог собрать ряд бинарников из third-party npm packages, но все они решились за счёт apk add. В итоге размеры nodejs-app образа всё равно копеечные.


И сразу вопрос. Вот допустим мы решили остаться с Ubuntu и двумя стадиями. В итоге у нас результирующий образ мелкий, а промежуточный большой. Мы можем использовать мелкий-образ, удалив большой? Там ведь unionFS. Она не будет пытаться обратиться к "большому слою" за файлами из него? Или COPY --from копирует по настоящему и привязки нет?

Мы можем использовать мелкий-образ, удалив большой?

они абсолютно независимы. На целевой сервер уедет мелкий оьбраз.
Или COPY --from копирует по настоящему и привязки нет?

ответил выше
Я столкнулся с парой проблем, когда npm install в alphine не мог собрать ряд бинарников из third-party npm packages

верно, поэтому все стадии сборки должны быть сделаны на совместимых образах. Условно — можно на первой стадии скомпилировать некий проект в убунту, а на второй стадии запустить его в эльпайн, но придется добавить все необходимые библиотеки рантайма (ну, типа libc). Либо, что проще — на каждой стадии использовать эльпайн (к сожалению, не всегда возможно из-за зависимостей).
НЛО прилетело и опубликовало эту надпись здесь
НЛО прилетело и опубликовало эту надпись здесь
Спасибо, поправил!

По теме минимизации размеров, забыли самое главное. Это scratch образы.
И в этом деле лучше всего проявляются особеннности Golang, где размер образа равен размеру бинарника.


FROM golang:alpine as go_builder
COPY app  /app
WORKDIR /app
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app .

FROM scratch
COPY --from=go_builder /app .
ENTRYPOINT ["/app"]
А есть где-то детали как из scratch запускать go-шный net/http?
Я пробовал бинарк (собранный статически, с отключенным CGO) запускать — не стартует и даже не ругается…
нужно больше информации, сделайте минимальный пример из двух файлов app.go и Dockerfile
Вроде оно запустилось:

docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
ffc1cf352438 slytomcat/urlshortener:latest "./URLshortener" 16 hours ago Up 16 hours 0.0.0.0:8080->8080/tcp urlshortener


и само себя в контейнере видит: по логам self health-check на старте проходит:

docker logs urlshortener
2020/03/17 15:56:02.516053 service.go:379: starting server at localhost:8080
2020/03/17 15:56:02.629846 service.go:261: token request from 127.0.0.1:41640 () parameters: 'http://localhost:8080/favicon.ico', 1: URL saved, token: rGqyrT , exp: 1
2020/03/17 15:56:02.641881 service.go:182: redirect request from 127.0.0.1:41640 (), token: rGqyrT: redirected to http://localhost:8080/favicon.ico
2020/03/17 15:56:02.653982 service.go:315: expire request from 127.0.0.1:41640 (): token expiration of rGqyrT has set to -1
2020/03/17 15:56:02.654200 service.go:375: initial health-check successfuly passed


но в контейнер не пробиться — пишет:

$ curl localhost:8080
curl: (56) Recv failure: Connection reset by peer


Сам проектик тут: github.com/slytomcat/URLshortener/tree/dev (там в мастере на alpine, а в dev — пробую scratch)

Там классический net/http.
ваш github.com/slytomcat/URLshortener/blob/dev/docker-compose.yml завелся без особых проблем. Ищите проблему у себя в хосте, или компоуз файле. Может быть на хоcте что-то уже висит на 8080, или не хватает `ports: — 8080:8080`. или в ./cnfr.json не 8080 порт))))
Там по дефолту (если нет в конфиге) используется localhost:8080

А композе — да попробую, просто пробовал через docker run.
Вот то же самое но в виде простейшего примера:
app.go:
package main

import (
"log"
"net/http"
)

func main() {

http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("Ok")) })

log.Println(http.ListenAndServe("localhost:8080", nil))

}


Dockerfile:
FROM scratch
WORKDIR /app
COPY app /app
ENTRYPOINT ["/app/app"]


Сборка:
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -installsuffix cgo .

Образ:
docker image build -t app:latest .
...
Successfully built 23f024474567
Successfully tagged app:latest


Запуск:
run --name="app" -d -p 8080:8080 app:latest


Проверяем:
docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
e1fa361b8ecd app:latest "/app/app" 12 seconds ago Up 9 seconds 0.0.0.0:8080->8080/tcp app


Тест:
curl localhost:8080
curl: (7) Failed to connect to localhost port 8080: Connection refused


Что я делаю не так?
Разобрался: надо было --network host в doker run указывать.
Зарегистрируйтесь на Хабре , чтобы оставить комментарий