Создайте минимальный образ Docker
Если вы знакомы с docker , вы, вероятно, знаете, что в хранилище образов docker используется технология многоуровневого хранения Union FS . Когда вы создаете образ Docker, он строится по одному слою за раз, при этом предыдущий слой служит основой для следующего слоя, и каждый слой не изменяется после его создания. Из-за этого при сборке docker-образа мы должны быть особенно внимательны, чтобы включать в каждый слой только то, что необходимо, и удалять как можно больше лишнего в конце сборки. Например, если вы создаете простое приложение, написанное на Go, в принципе вам нужен только бинарный файл, скомпилированный с помощью Go, и нет необходимости сохранять инструменты и среду для сборки.
docker официально предоставляет специальный пустой образ, использование этого образа означает, что нам не нужен какой-либо существующий образ в качестве основы, и мы напрямую используем наши пользовательские инструкции в качестве первого слоя образа.
FROM scratch
...
На самом деле, мы можем создать собственный рабочий образ.
$ tar cv --files-from /dev/null | docker import - scratch
Возникает вопрос, какие изображения мы можем сделать, используя скретч-образы в качестве основы? Ответ заключается в том, что все исполняемые файлы, не нуждающиеся в каких-либо зависимостях, могут быть созданы с использованием нуля в качестве базового образа. Конкретно для статически скомпилированных программ под linux нет необходимости в поддержке runtime со стороны операционной системы, все уже включено в исполняемый файл, например, многие приложения, разработанные на языке Go, используют прямой FROM scratch
метод для создания образов, поэтому конечный образ размер очень маленький.
Вот код простого веб-приложения, разработанного на языке Go.
package main
import (
"fmt"
"net/http"
)
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, you've requested: %s\n", r.URL.Path)
})
http.ListenAndServe(":80", nil)
}
Мы можем использовать go build
для компиляции этой программы и создания образа докера на основе нуля со следующим файлом докера.
FROM scratch
ADD helloworld /
CMD ["/helloworld"]
Затем начните компилировать и создавать образ докера.
mc@mcmbp:~/gocode/src/hello# go build -o helloworld
mc@mcmbp:~/gocode/src/hello# docker build -t helloworld .
Sending build context to Docker daemon 7.376MB
Step 1/3 : FROM scratch
--->
Step 2/3 : ADD helloworld /
---> 000f150706c7
Step 3/3 : CMD ["/helloworld"]
---> Running in f9c2c6932a34
Removing intermediate container f9c2c6932a34
---> 496f865c05e4
Successfully built 496f865c05e4
Successfully tagged helloworld:latest
Итак, образ построен успешно, давайте посмотрим на его размер.
mc@mcmbp:~/gocode/src/hello# docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
helloworld latest 496f865c05e4 8 seconds ago 7.37MB
Но запустив этот образ, вы обнаружите, что контейнер не может быть создан.
mc@mcmbp:~/gocode/src/hello# docker run -ti -p 80:80 helloworld
standard_init_linux.go:207: exec user process caused "no such file or
Причина в том, что наш исполняемый файл helloworld работает с некоторыми библиотеками, такими как libc, которые все еще динамически связаны, а рабочий образ полностью пуст, поэтому при сборке исполняемого файла helloworld укажите флаг статической ссылки -static
и другие параметры, чтобы сгенерированный двоичный файл hellowrld статически связывал все библиотеки.
$ CGO_ENABLED=0 go build -a -ldflags '-extldflags "-static"' -o helloworld .
Затем воссоздайте образ докера.
mc@mcmbp:~/gocode/src/hello# CGO_ENABLED=0 go build -a -ldflags '-extldflags "-static"' -o helloworld .
mc@mcmbp:~/gocode/src/hello# docker build -t helloworld .
Sending build context to Docker daemon 7.316MB
Step 1/3 : FROM scratch
--->
Step 2/3 : ADD helloworld /
---> 3fec774cb2a4
Step 3/3 : CMD ["/helloworld"]
---> Running in cbe7dc97d6ad
Removing intermediate container cbe7dc97d6ad
---> d15a1e6d759a
Successfully built d15a1e6d759a
Successfully tagged helloworld:latest
mc@mcmbp:~/gocode/src/hello# docker image ls -a
REPOSITORY TAG IMAGE ID CREATED SIZE
helloworld latest d15a1e6d759a 3 seconds ago 7.31MB
<none> <none> 3fec774cb2a4 3 seconds ago 7.31MB
Запустите образ докера.
mc@mcmbp:~/gocode/src/hello# docker run -ti -d -p 80:80 helloworld
3c77ae750352245369c4d142e4e57fd3c0f1e11d67ef857235417ec475ef6286
mc@mcmbp:~/gocode/src/hello# curl -v localhost
* Rebuilt URL to: localhost/
* Trying 127.0.0.1...
* Connected to localhost (127.0.0.1) port 80 (#0)
> GET / HTTP/1.1
> Host: localhost
> User-Agent: curl/7.47.0
> Accept: */*
>
< HTTP/1.1 200 OK
< Date: Tue, 19 Mar 2019 12:54:28 GMT
< Content-Length: 27
< Content-Type: text/plain; charset=utf-8
<
Hello, you've requested: /
* Connection #0 to host localhost left intact
Но возникает вопрос, если мы скомпилируем бинарники helloowrld и сделаем образы на MacOS, сможем ли мы запустить контейнеры docker? Ответ - нет!
Нам нужно указать GOOS=linux
, что означает, что полная команда компиляции должна выглядеть так.
$ CGO_ENABLED=0 GOOS=linux go build -a -ldflags '-extldflags "-static"'
Для некоторых OCD-программистов, которые хотят пойти дальше, почему бы не скомпилировать исполняемый файл внутри контейнера, а затем не собрать образ? Преимущество этого состоит в том, что он контролирует среду компиляции языка Go, обеспечивает повторяемость компиляции и удобен для некоторых проектов, которые интегрируются с инструментами непрерывной интеграции.
Docker-CE 17.5 представляет новую функцию для создания образов с нуля, которая называется «Многоэтапные сборки». С помощью этой новой функции мы можем написать наш dockerfile следующим образом.
FROM golang as compiler
RUN CGO_ENABLED=0 go get -a -ldflags '-s' \
github.com/morvencao/helloworld
FROM scratch
COPY --from=compiler /go/bin/helloworld .
EXPOSE 80
CMD ["./helloworld"]
Да, вы правильно прочитали, это действительно dockerfile, содержащий две директивы FROM со следующими оговорками.
FROM golang as compiler
заключается в том, чтобы дать первому этапу сборки имя, называемоеcompiler
COPY --from=compiler /go/bin/helloworld .
является ссылкой на вывод сборки первого этапа для сборки второго этапа
Если у вас нет имени для первого этапа, вы можете использовать номер этапа сборки (начиная с 0), чтобы указать его, например: --from=0
, но из соображений удобочитаемости лучше дать ему имя.
После завершения сборки давайте посмотрим на размер образа.
mc@mcmbp:~/gocode/src/hello# docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
helloworld latest 071ca07e23f5 1 minutes ago 7.31MB
<none> <none> 2471fd63f0e7 1 minutes ago 720MB
golang latest 6d0bfafa0452 2 weeks ago 703MB
Запустите образ докера.
mc@mcmbp:~/gocode/src/hello# docker run -ti -d -p 80:80 helloworld
3c77ae750352245369c4d142e4e57fd3c0f1e11d67ef857235417ec475ef6286
mc@mcmbp:~/gocode/src/hello# curl -v localhost
* Rebuilt URL to: localhost/
* Trying 127.0.0.1...
* Connected to localhost (127.0.0.1) port 80 (#0)
> GET / HTTP/1.1
> Host: localhost
> User-Agent: curl/7.47.0
> Accept: */*
>
< HTTP/1.1 200 OK
< Date: Tue, 19 Mar 2019 12:54:28 GMT
< Content-Length: 27
< Content-Type: text/plain; charset=utf-8
<
Hello, you've requested: /
* Connection #0 to host localhost left intact