Подробнее о Docker

Навроцкий Артем

Подробнее о Docker

Навроцкий Артем

Подробнее о Docker

Сборка контейнеров

Нужно быть аккуратыми с RUN-командой

При повторной сборке образов, Docker считает, что если не менялись входные данные, то можно использовать результат предыдущей сборки. Это заметно ускоряет повторную сборку контейнера.

Но директива RUN не является "чистой функцией" и зависит от внешних факторов.

В результате после изменения Dockerfile может возникнуть ситуация, что RUN команды будут выполняться в сильно разное время.

Пример проблемы с RUN-командой

Было:

RUN apt -y update
RUN apt -y install curl nginx

Заменили на:

RUN apt -y update
RUN apt -y install curl nginx git

В итоге:

# docker используент кэш недельной давности
RUN apt -y update
# docker пытается выполнить команду, но кэш apt не актуален
RUN apt -y install curl nginx git

Решение проблемы с RUN-командой

Заменить:

RUN apt -y update
RUN apt -y install curl nginx

На:

RUN apt -y update && \
    apt -y install curl nginx && \
    rm -rf /var/lib/apt/lists/*

Multi-stage builds

При сборке Docker-образов создаётся много слоёв, которые содержат различные временные данные и лишние зависимости.

Для уменьшения размера образов имеет смысл разбить Dockerfile на несколько образов:

Multi-stage builds

Dockerfile

FROM golang:1.7.3 AS build
WORKDIR /go/src/github.com/alexellis/href-counter/
RUN go get -d -v golang.org/x/net/html
COPY app.go .
RUN CGO_ENABLED=0 go build -o /go/bin/app .

FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=build /go/bin/app .
CMD ["./app"]

https://docs.docker.com/develop/develop-images/multistage-build/

Кэширование сборки

$ docker pull docker.joom.it/helm:latest
$ docker build --cache-from=docker.joom.it/helm:latest .

HEALTHCHECK

Dockerfile

FROM nginx:1.17.7
RUN apt-get update && apt-get install -y curl
HEALTHCHECK \
    --interval=5s \
    --timeout=1s \
    --retries=3 CMD curl --head http://localhost/
$ docker build -q -t test . && docker run --rm -it test
sha256:d2064caeda2e5c32d0d39a17c9b2d71e27dbdd6d93b2d6f2361802ea60235d34
127.0.0.1 - - [10/Jul/2020:10:54:45 +0000] "HEAD / HTTP/1.1" 200 0 "-" "curl/7.64.0" "-"
127.0.0.1 - - [10/Jul/2020:10:54:50 +0000] "HEAD / HTTP/1.1" 200 0 "-" "curl/7.64.0" "-"
127.0.0.1 - - [10/Jul/2020:10:54:55 +0000] "HEAD / HTTP/1.1" 200 0 "-" "curl/7.64.0" "-"
127.0.0.1 - - [10/Jul/2020:10:55:00 +0000] "HEAD / HTTP/1.1" 200 0 "-" "curl/7.64.0" "-"
127.0.0.1 - - [10/Jul/2020:10:55:05 +0000] "HEAD / HTTP/1.1" 200 0 "-" "curl/7.64.0" "-"
 

HEALTHCHECK

Статус контейнера

$ docker ps
CONTAINER ID  IMAGE  COMMAND                 CREATED        STATUS
027b1a3d2cd0  test   "nginx -g 'daemon of…"  1 second ago   Up Less than a second (health: starting)

$ docker ps
CONTAINER ID  IMAGE  COMMAND                 CREATED        STATUS
027b1a3d2cd0  test   "nginx -g 'daemon of…"  9 seconds ago  Up 8 seconds (healthy)

$ docker inspect 027b1a3d2cd0 | jq ".[0].State.Health.Status"
"healthy"
 

В чем разница по поведению?

a/Dockerfile

FROM ubuntu
CMD /bin/sleep 600

b/Dockerfile

FROM ubuntu
CMD ["/bin/sleep", "600"]
$ docker build -t test .
$ docker run --rm --name test test

У контейнеров будет разная реакция на:

$ sudo killall -SIGTERM sleep

В чем разница по поведению?

CMD /bin/sleep 600

$ docker exec test ps
    PID TTY          TIME CMD
      1 ?        00:00:00 sh
      7 ?        00:00:00 sleep
      8 ?        00:00:00 ps

CMD ["/bin/sleep", "600"]

$ docker exec test ps
    PID TTY          TIME CMD
      1 ?        00:00:00 sleep
      6 ?        00:00:00 ps

PID = 1 имеет достаточно специфическое поведение с точки зрения обработки сигналов.

Встроенный init

docker run --init

$ docker exec test ps
    PID TTY          TIME CMD
      1 ?        00:00:00 docker-init
      6 ?        00:00:00 sleep
      7 ?        00:00:00 ps

Флаг --init отсутсвует при запуске контейнеров в Kubernetes.

Минималистичный init

tini (https://github.com/krallin/tini)

FROM ubuntu
ADD https://github.com/krallin/tini/releases/download/v0.19.0/tini /tini
RUN chmod +x /tini
ENTRYPOINT ["/tini", "--"]
CMD ["/bin/sleep", "600"]

$ docker exec test ps
    PID TTY          TIME CMD
      1 ?        00:00:00 tini
      6 ?        00:00:00 sleep
      7 ?        00:00:00 ps

Ubuntu vs Alpine

ubuntu/Dockerfile

FROM ubuntu
CMD /bin/sleep 600

Shell в Ubuntu прокидывает SIGTERM до дочернего процесса.

alpine/Dockerfile

FROM alpine
CMD /bin/sleep 600

Shell в Alpine не прокидывает SIGTERM до дочернего процесса.

Контекст сборки

При сборке образа указывается корневой каталог контекста сборки.

Контекст сборки копируется с локальной машины на Docker Daemon.

$ docker build .

Sending build context to Docker daemon  6.51 MB
...

Контекст сборки

Для исключения файлов из контекста сборки используется файл .dockerignore.

$ cat .dockerignore
# comment
*/temp*
*/*/temp*
temp?

Сборка с BuildKit

$ DOCKER_BUILDKIT=1 docker build

Плюсы

Минусы

https://docs.docker.com/develop/develop-images/build_enhancements/

Сборка с BuildKit

$ echo 'WARMACHINEROX' > mysecret.txt
$ DOCKER_BUILDKIT=1 docker build --progress=plain \
    --no-cache --secret id=mysecret,src=mysecret.txt .

Dockerfile

# syntax = docker/dockerfile:1.0-experimental
FROM alpine

# shows secret from default secret location
RUN --mount=type=secret,id=mysecret cat /run/secrets/mysecret

# shows secret from custom secret location
RUN --mount=type=secret,id=mysecret,dst=/foobar cat /foobar

Время сборки backend-а

Статистика сборки (начало 2020):

Execution statistics:
  docker push     20m7.591993776s
  docker build    10m52.346187972s
  shell command   4m51.205196455s
  go build        3m36.109218439s
  docker pull     19.589956997s
  docker image rm 3.795116679s
  file rm         1.217284781s
  docker file     7.277674ms
Build completed: 7m43.878900367s

Отображается wall clock time при сборке в несколько потоков. Но всё равно сильно выделяется время на сборку контейнеров.

Время сборки backend-а

Время docker build

Сборка каждого Dockerfile отправляет контекст сборки (в нашем случае туда входит только исполняемый файл) в Docker Host.

Каждая комадна в Dockerfile провоцирет создание временного Docker-контейнера.

Есть ощущение, что параллельное выполнение docker build частично сериализуется где-то внутри Docker Host.

Время docker push

Во время docker push львиную долю времени занимает сжатие образов GZip-ом.

Альтернативные способы сборки

github.com/GoogleContainerTools/kaniko

github.com/joomcode/go-porter

Best practics

Не используйте тэг в latest

Нельзя использовать тэг latest при деплое в production.

Особенно если деплой в Kubernetes.

Не собирайте контейнеры руками

Никогда не заливайте контейнеры, собранные локально в Docker Registry.

Все контейнеры для Docker Registry должны собраться через CI:

У нас есть инфраструктура для сборки простых образов в репозитории: github.com/joomcode/docker-images

LABEL-ы с информацией о контейнере

При сборке docker-образов крайне желательно заполнять label-ы, чтобы не было вопросов из серии "откуда вообще этот образ взялся":

org.opencontainers.image.created
дата создания образа
org.opencontainers.image.ref.ci
ссылка на сборку образа в CI
org.opencontainers.image.ref.dockerfile
имя Dockerfile из которого собран образ
org.opencontainers.image.ref.dockerfile
имя Dockerfile из которого собран образ
org.opencontainers.image.revision
ревизия, от которой собран образ
org.opencontainers.image.source
репозиторий, из которого собран образ

LABEL-ы с информацией о контейнере

Пример:

com.joom.retention.maxCount: 5
com.joom.retention.maxCountGroup: master
org.opencontainers.image.created: 2020-06-25T16:20:56Z
org.opencontainers.image.ref.ci: https://jenkins.joom.it/job/docker-images/job/...
org.opencontainers.image.ref.dockerfile: backend-api-test-base/Dockerfile
org.opencontainers.image.revision: 41a1d8a485ae38311b141e7c355acf081dcc7f11
org.opencontainers.image.source: https://github.com/joomcode/docker-images.git

Аннотации взяты отсюда: https://github.com/opencontainers/image-spec/blob/master/annotations.md

LABEL-ы с информацией о контейнере

Чтобы не захламлять Dockerfile, можно воспользоваться конструкцией вида:

$ docker build \
    --label org.opencontainers.image.created=$buildDate \
    --label org.opencontainers.image.ref.ci=$BUILD_URL \
    --label org.opencontainers.image.ref.dockerfile=$dockerFile \
    --label org.opencontainers.image.source=$GIT_URL \
    --label org.opencontainers.image.revision=$GIT_COMMIT \
    ...
    

Garbage collection

У нас в docker registry реализован механизм чистки более ненужных образов.

Ответственность за чистку образов возлагается на команды, которые эти образы используют.

Garbage collection

Используемые при чистке репозиторием LABEL-ы:

com.joom.retention.maxDays=3
Количество дней, после которых можно удалить docker-образ
com.joom.retention.maxCount=10
Количество образов, которые достаточно хранить
com.joom.retention.maxCountGroup=master
Группа, в которой удаляются образы по превышении количества
com.joom.retention.deployGroup=backend-master-123456
Объединение образов с одну группу (группа удаляется целиком)
com.joom.retention.pullProtectDays=90
Количество дней, которые образ будет сохраняться с момента последнего скачивания

Запуск нескольких процессов

Docker рассчитан на запуск одного процесса в контейнере.

Если приложение состоит из нескольких процессов, то есть следующие варианты:

Запуск нескольких процессов

Разделение на несколько контейнеров

Запуск нескольких процессов в контейнере

Docker compose

docker-compose.yml

version: "3.8"
services:
  server:
    build: server/
    ports:
     - 8080:80
  client:
    build: client/
    command: python ./client.py
    environment:
     - SERVER=server:80
    depends_on:
     - server
$ docker-compose build && docker-compose up

Docker compose

Запуск нескольких процессов

Для запуска нескольких процессов в контейнере можно воспользоваться supervisord.

Dockerfile

FROM ubuntu
RUN apt-get update \
  && apt-get install -y supervisor nginx \
  && rm -rf /var/lib/apt/lists/*
COPY nginx.conf /etc/supervisor/conf.d/
CMD ["/usr/bin/supervisord", "--nodaemon"]

nginx.conf

[program:nginx]
command=/usr/sbin/nginx -g 'daemon off;'
stdout_logfile=/var/log/nginx-stdout.log
stderr_logfile=/var/log/nginx-stderr.log
autostart=true
autorestart=true
numprocs=1

Запуск нескольких процессов

$ supervisorctl status
nginx          RUNNING   pid 7, uptime 0:00:56

$ supervisorctl stop nginx
nginx: stopped

$ supervisorctl status
nginx          STOPPED   Jul 10 02:10 PM

$ supervisorctl start nginx
nginx: started

$ supervisorctl status
nginx          RUNNING   pid 64, uptime 0:00:05

Docker vs Jenkins

Docker поддерживает запуск сборки "внутри" Docker-контейнера из коробки:

Docker compose при этом не поддерживается.

Jenkins: docker image

pipeline {
  agent {
    docker {
      image "golang:1.13.9-alpine3.11"
      args "--hostname ${env.JOB_NAME.replace('/', '-')}-${env.BUILD_NUMBER}"
      customWorkspace "workspace/example"
      label "onspot"
    }
  }
  ...
}


Jenkins: dockerfile

pipeline {
  agent {
    docker {
      dir "jenkins"
      filename "Dockerfile"
      additionalBuildArgs "--pull"
      args "--hostname ${env.JOB_NAME.replace('/', '-')}-${env.BUILD_NUMBER}"
      customWorkspace "workspace/example"
      label "onspot"
    }
  }
  ...
}

Jenkins: общие особенности

Полезные ссылки

Спасибо за внимание!