scratch
и
крошечного http-сервера на основе этой сборки, я смог ужать
результат до 6.32kB!Если предпочитаете видео, вот ролик по статье, выложенный на YouTube!
Раздутые контейнеры
Контейнеры часто превозносятся как панацея, позволяющая справиться с любыми вызовами, связанными с эксплуатацией ПО. Притом, как мне нравятся контейнеры, на практике мне часто попадаются контейнерные образы, отягощенные разнообразными проблемами. Распространенная беда размер контейнера; у некоторых образов он достигает многих гигабайт!
Поэтому я решил устроить челлендж себе и всем желающим и попытаться создать настолько компактный образ, насколько возможно.
Задача
Правила довольно просты:
- Контейнер должен выдавать содержимое файла по http на выбранный вами порт
- Монтирование томов не допускается (так называемое Правило Марека )
Упрощенное решение
Чтобы узнать размер базового образа, можно воспользоваться node.js и создать простой сервер
index.js
:
const fs = require("fs");const http = require('http');const server = http.createServer((req, res) => {res.writeHead(200, { 'content-type': 'text/html' })fs.createReadStream('index.html').pipe(res)})server.listen(port, hostname, () => {console.log(`Server: http://0.0.0.0:8080/`);});
и сделать из него образ, запуская официальный базовый образ node:
FROM node:14COPY . .CMD ["node", "index.js"]
Этот завесил на
943MB
!Уменьшенный базовый образ
Один из простейших и наиболее очевидных тактических подходов к уменьшению размера образа выбрать более компактный базовый образ. Официальный базовый образ node существует в варианте
slim
(по-прежнему на основе debian, но с меньшим
количеством предустановленных зависимостей) и
вариантalpine
на основеAlpine
Linux.С применением
node:14-slim
иnode:14-alpine
в
качестве базового удается уменьшить размер образа
до167MB
и116MB
соответственно.Поскольку образы docker аддитивны, где каждый уровень надстраивается над следующим, здесь уже практически нечего сделать, чтобы еще сильнее уменьшить решение с node.js.
Скомпилированные языки
Чтобы вывести ситуацию на новый уровень, можно перейти к компилируемому языку, где гораздо меньше зависимостей времени исполнения. Есть ряд вариантов, но для создания веб-сервисов часто применяется golang.
Я создал простейший файловый сервер
server.go
:
package mainimport ("fmt""log""net/http")func main() {fileServer := http.FileServer(http.Dir("./"))http.Handle("/", fileServer)fmt.Printf("Starting server at port 8080\n")if err := http.ListenAndServe(":8080", nil); err != nil {log.Fatal(err)}}
И встроил его в контейнерный образ, воспользовавшись официальным базовым образом golang:
FROM golang:1.14COPY . .RUN go build -o server .CMD ["./server"]
Который завесил на
818MB
.Здесь есть проблема: в базовом образе golang установлено много зависимостей, которые полезны при создании программ на go, но не нужны для запуска программ.
Многоступенчатые сборки
В Docker есть возможность под названиеммногоступенчатые сборки, с которыми просто собирать код в среде, содержащей все необходимые зависимости, а потом скопировать полученный исполняемый файл в иной образ.
Это полезно по нескольким причинам, но одна из наиболее очевидных размер образа! Выполнив рефакторинг dockerfile вот так:
### этап сборки ###FROM golang:1.14-alpine AS builderCOPY . .RUN go build -o server .### этап запуска ###FROM alpine:3.12COPY --from=builder /go/server ./serverCOPY index.html index.htmlCMD ["./server"]
Размер полученного образа всего
13.2MB
!Статическая компиляция + образ Scratch
13 MB совсем неплохо, но у нас в запасе осталась еще пара трюков, позволяющих еще сильнее ужать этот образ.
Есть базовый образ под названиемscratch, который однозначно пуст, его размер равен нулю. Поскольку внутри
scratch
ничего нет, любой образ, построенный на его
основе, должен нести в себе все необходимые зависимости.Чтобы это было возможно на основе нашего сервера на go, нужно добавить несколько флагов на этапе компиляции, чтобы обеспечить статическую линковку всех необходимых библиотек в исполняемый файл:
### этап сборки ###FROM golang:1.14 as builderCOPY . .RUN go build \-ldflags "-linkmode external -extldflags -static" \-a server.go### этап запуска ###FROM scratchCOPY --from=builder /go/server ./serverCOPY index.html index.htmlCMD ["./server"]
В частности, мы задаем
external
в качестве режима
линковки и передаем флаг-static
внешнему
линковщику.Благодаря двум этим изменениям удается довести размер образа до
8.65MB
ASM как залог победы!
Образ размером менее 10MB, написанный на языке вроде Go, отчетливо миниатюрен почти для любых обстоятельств но можно сделать еще меньше! Пользовательnemasuвыложил на Github полноценный http-сервер, написанный на ассемблере. Он называется assmttpd.
Все, что потребовалось для его контейнеризации установить несколько зависимостей сборки в базовый образ Ubuntu, прежде, чем запустить предоставленный рецепт
make release
:
### этап сборки ###FROM ubuntu:18.04 as builderRUN apt updateRUN apt install -y make yasm as31 nasm binutilsCOPY . .RUN make release### этап запуска ###FROM scratchCOPY --from=builder /asmttpd /asmttpdCOPY /web_root/index.html /web_root/index.htmlCMD ["/asmttpd", "/web_root", "8080"]
Затем полученный в результате исполняемый файл
asmttpd
копируется в scratch-образ и вызывается
через командную строку. Размер полученного образа всего
6,34kB!