Comments 28
На самом деле в этом есть и плюс, ведь если вдруг какой-то злоумышленник сможет получить доступ к контейнеру, он причинит намного меньше вреда, чем если бы у него был доступ к shell
Замуровать свой дом, чтобы ничего не украли? Так себе плюс. Хотя экономия 500 МБ конечно существенна.
На самом деле микроменеджмент докер-образов реально помогает. И тут есть больше возможностей, чем в статье.
1. всегда существует squash. У него минус в том, что он схлопывает все слои и поэтому суммарный объем ВСЕХ докер-образов в системе может увеличиться (т.к. нет общих слоев).
2. правильная расстановка requirements.txt, npm install и пр. в файле (лучше максимально в конец, но еще до внедрения пользовательского кода).
3. чистить в том же блоке RUN зависимости, которые нужны для сборки образа (убираем кэши и пр.). Про мультистейдж, кстати, сказали — супер!
и многое-многое другое.
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.
Так какой же базовый образ выбрать?
Здесь огромная ошибка. Нужно для 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
Я новичок в 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"]
Я пробовал бинарк (собранный статически, с отключенным CGO) запускать — не стартует и даже не ругается…
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.
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
Что я делаю не так?
Три простых приема для уменьшения Docker-образов