Привет, друзья!


Хочу поделиться с вами заметками о Docker.


Заметки состоят из 3 частей: первые две теоретические, третья практическая.


Если быть более конкретным:


  • первая часть посвящена самому Docker, Docker CLI и Dockerfile;
  • вторая часть полностью о Docker Compose;
  • в третьей части мы разработаем и "контейнеризуем" приложение, состоящее из клиента (React.js), сервера (Express.js) и базы данных (PostgreSQL), с помощью CLI, Dockerfile и Compose, развернем его где-нибудь (еще не решил где) и настроим CI/CD.

Это часть номер раз.


Что такое Docker?


Начало работы с Docker на английском.


Перевод на русский от Microsoft.


Docker — это открытая платформа для разработки, доставки и запуска приложений. Он позволяет отделить приложения от инфраструктуры и управлять инфраструктурой по аналогии с тем, как мы управляем приложениями.


Docker предоставляет возможность упаковывать и запускать приложение в слабо изолированной среде — контейнере. Изоляция и безопасность позволяют одновременно запускать несколько контейнеров на одном хосте (хостом может быть наша локальная машина, дата центр, облачный провайдер или их микс). Контейнеры являются легковесными и содержат все необходимое для запуска приложения, что избавляет нас от необходимости полагаться на то, что установлено на хосте.


Для чего Docker может использоваться?


Быстрая и согласованная доставка приложений


Docker рационализирует жизненный цикл разработки, позволяя разработчикам работать в стандартизированной среде через локальные контейнеры, предоставляющие приложения и сервисы. Контейнеры отлично подходят для рабочих процессов непрерывной интеграции и непрерывной доставки (continuous integration/continuous delivery, CI/CD).


Отзывчивая разработка и масштабирование


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


Запуск большего количества приложений на одной машине


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


Архитектура Docker


Docker использует клиент-серверную архитектуру. Клиент (Docker client) обращается к демону (Docker daemon), который поднимает (собирает), запускает и распределяет контейнеры. Клиент и демон могут быть запущены в одной системе или клиент может быть подключен к удаленному демону. Клиент и демон общаются через REST API поверх UNIX-сокетов или сетевого интерфейса. Другим клиентом является Docker Compose, позволяющий работать с приложениями, состоящими из нескольких контейнеров.





Демон


Демон (dockerd) регистрирует (слушает) запросы, поступающие от Docker API, и управляет такими объектами как образы, контейнеры, сети и тома. Демон может общаться с другими демонами для управления сервисами.


Клиент


Клиент (docker) — основной способ коммуникации с Docker. При выполнении такой команды, как docker run, клиент отправляет эту команду демону, который, собственно, эту команду и выполняет. Команда docker использует Docker API. Клиент может общаться с несколькими демонами.


Docker Desktop


Docker Desktop — это десктопное приложение для Mac и Windows, позволяющее создавать и распределять контейнерные приложения и микросервисы. Docker Desktop включает в себя демона, клиента, Docker Compose, Docker Content Trust, Kubernetes и Credential Helper.


Реестр


В реестре (registry) хранятся образы контейнеров. Docker Hub — это публичный реестр, который (по умолчанию) используется Docker для получения образов. Имеется возможность создания частных (закрытых) реестров.


При выполнении таких команд, как docker pull или docker run, необходимые образы загружаются из настроенного реестра. А при выполнении команды docker push образ загружается в реестр.


Объекты


При использовании Docker мы создаем и используем образы, контейнеры, сети, тома, плагины и другие объекты. Рассмотрим некоторые из них.


Образы (Images)


Образ — это доступный только для чтения шаблон с инструкциями по созданию контейнера. Часто образ представляет собой модификацию другого образа.


Можно создавать свои образы или использовать образы, созданные другими и опубликованные в реестре. Для создания образа используется Dockerfile, содержащий инструкции по созданию образа и его запуску (см. ниже). Ряд инструкций в Dockerfile приводит к созданию в образе нового слоя (раньше новый слой создавался для каждой инструкции). При изменении Dockerfile и повторной сборке образа пересобираются только модифицированные слои. Это делает образы легковесными, маленькими и быстрыми.


Контейнеры (Containers)


Контейнер — это запускаемый экземпляр образа. Мы создаем, запускаем, перемещаем и удаляем контейнеры с помощью Docker API или CLI (command line interface, интерфейс командной строки). Мы можем подключать контейнеры к сетям, добавлять в них хранилища данных и даже создавать новые образы на основе текущего состояния.


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


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


Пример команды docker run


Следующая команда запускает контейнер ubuntu, интерактивно подключается к локальному сеансу командной строки и выполняет в ней команду /bin/bash:


docker run -i -t ubuntu /bin/bash

При выполнении этой команды происходит следующее:


  1. Поскольку на нашей машине нет образа ubuntu, Docker загружает его из реестра (то же самое делает команда docker pull ubuntu).
  2. Docker создает новый контейнер (то же самое делает команда docker container create).
  3. В качестве последнего слоя Docker выделяет контейнеру файловую систему для чтения и записи. Это позволяет запущенному контейнеру создавать и модифицировать файлы и директории в его локальной файловой системе.
  4. Поскольку мы не указали сетевых настроек, Docker создает сетевой интерфейс для подключения контейнера к дефолтной сети. Это включает в себя присвоение контейнеру IP-адреса. Контейнеры могут подключаться к внешним сетям через сетевое соединение хоста.
  5. Docker запускает контейнер и выполняет /bin/bash. Поскольку контейнер запущен в интерактивном режиме и подключен к терминалу (благодаря флагам -i и -t), мы можем вводить команды и получать результаты в терминале.
  6. Выполнение команды exit приводит к прекращению выполнения /bin/bash. Контейнер останавливается, но не удаляется. Мы можем запустить его снова или удалить.

Команды и флаги


docker run


Команда docker run используется для запуска контейнера. Это основная и потому наиболее часто используемая команда.


# сигнатура
docker run [OPTIONS] IMAGE[:TAG|@DIGEST] [COMMAND] [ARG...]
# основные настройки (флаги)
-d - запуск контейнера в качестве отдельного процесса
-p - публикация открытого порта в интерфейсе хоста (HOST:CONTAINER)
# например
-p 3000:3000
-t - выделение псевдотерминала
-i - оставить STDIN открытым без присоединения к терминалу
--name - название контейнера
--rm - очистка системы при остановке/удалении контейнера
--restart - политика перезапуска - no (default) | on-failure[:max-retries] | always | unless-stopped
-e - установка переменной среды окружения
-v - привязка распределенной файловой системы (name:/path/to/file)
# например
-v mydb:/etc/mydb
-w - установка рабочей директории

Следующая команда запускает контейнер postgres:


# \ используется для разделения длинных команд на несколько строк
docker run --rm \
 # название контейнера
 --name postgres \
 # пользователь
 -e POSTGRES_USER=postgres \
 # пароль
 -e POSTGRES_PASSWORD=postgres \
 # название базы данных
 -e POSTGRES_DB=mydb \
 # автономный режим и порт
 -dp 5432:5432 \
 # том для хранения данных
 -v $HOME/docker/volumes/postgres:/var/lib/postgresql/data \
 # образ
 postgres

docker build


Команда docker build используется для создания образа на основе файла Dockerfile и контекста. Контекст — это набор файлов, находящихся в локации, определенной с помощью PATH или URL. PATH — это директория в нашей локальной системе, а URL — это удаленный репозиторий. Контекст сборки обрабатывается рекурсивно, поэтому PATH включает как директорию, там и все ее поддиректории, а URL — как репозиторий, так и все его субмодули. Для исключения файлов из сборки образа используется .dockerignore (синтаксис этого файла похож на .gitignore).


# сигнатура
docker build [OPTIONS] PATH | URL | -

Создание образа:


# в качестве контекста сборки используется текущая директория
docker build .

Использование репозитория в качестве контекста (предполагается, что Dockerfile находится в корневой директории репозитория):


docker build github.com/creack/docker-firefox

docker build -f ctx/Dockerfile http://server/ctx.tar.gz

В данном случае http://server/ctx.tar.gz отправляется демону, которые загружает и извлекает файлы. Параметр -f ctx/Dockerfile определяет путь к Dockerfile внутри ctx.tar.gz.


Чтение Dockerfile из STDIN без контекста:


docker build - < Dockerfile

Добавление тега к образу:


docker build -t myname/my-image:latest .

Определение Dockerfile:


docker build -f Dockerfile.debug .

Экспорт файлов сборки в директорию out:


docker build -o out .

Экспорт файлов сборки в файл out.tar:


docker build -o - . > out.tar

docker exec


Команда docker exec используется для выполнения команды в запущенном контейнере.


# сигнатура
docker exec [OPTIONS] CONTAINER COMMAND [ARG...]
# основные флаги
-d - выполнение команды в фоновом режиме
-e - установка переменной среды окружения
-i - оставить `STDIN` открытым
-t - выделение псевдотерминала
-w - определение рабочей директории внутри контейнера

Пример:


# -U - это пользователь, которым по умолчанию является root
docker exec -it postgres psql -U postgres

В данном случае в контейнере postgres будет запущен интерактивный терминал psql. Выполним парочку команд:


# получаем список баз данных
\l
# подключаемся к базе данных mydb
-d mydb
# получаем список таблиц, точнее, сообщение об отсутствии отношений (relations)
\dt
# выходим
\q

docker ps


Команда docker ps используется для получения списка (по умолчанию только запущенных) контейнеров.


# сигнатура
docker ps [OPTIONS]
# основные флаги
-a - показать все контейнеры (как запущенные, так и остановленные)
-f - фильтрация вывода на основе условия (`id`, `name`, `status` и т.д.)
-n - показать n последних созданных контейнеров
-l - показать последний созданный контейнер
# пример получения списка приостановленных контейнеров
docker ps -f 'status=paused'

Для получения списка образов используется команда docker images.


Команды управления


# запуск остановленного контейнера
docker start CONTAINER

# приостановление всех процессов, запущенных в контейнере
docker pause CONTAINER

# остановка контейнера
docker stop CONTAINER

# "убийство" контейнера
docker kill CONTAINER

# перезапуск контейнера
docker restart CONTAINER

# удаление остановленного контейнера
docker rm [OPTIONS] CONTAINER
# основные флаги
-f - принудительное удаление (остановка и удаление) запущенного контейнера
-v - удаление анонимных томов, связанных с контейнером
# пример удаления всех остановленных контейнеров
docker rm $(docker ps --filter status=exited -q)

# удаление образа
docker rmi IMAGE

###

# управление образами
docker image COMMAND

# управление контейнерами
docker container COMMAND

# управление томами
docker volume COMMAND

# управление сетями
docker network COMMAND

# управление docker
docker system COMMAND

Другие команды


Для получения логов запущенного контейнера используется команда docker logs:


docker logs [OPTIONS] CONTAINER
# основные флаги
-f - следование за выводом
-n - n последних строк

Для удаления всех неиспользуемых данных (контейнеры, сети, образы и, опционально, тома) используется команда docker system prune. Основные флаги:


-a - удаление всех неиспользуемых образов, а не только обособленных (dangling)
--volumes - удаление томов

Предостережение: применять эту команду следует с крайней осторожностью, поскольку удаленные данные не подлежат восстановлению.


Полный список команд и флагов.


Dockerfile


Dockerfile — это документ (без расширения), содержащий инструкции, которые используются для создания образа при выполнении команды docker build.


Предостережение: не используйте / в качестве PATH для контекста сборки. Это приведет к передаче демону всего содержимого жесткого диска вашей машины.


Инструкции выполняются по одной. Результаты наиболее важных инструкций фиксируются в виде отдельных слоев образа. Обратите внимание: каждая инструкция выполняется независимо от других. Это означает, что выполнение RUN cd /tmp не будет иметь никакого эффекта для последующих инструкций.


Dockerfile может содержать следующие инструкции:


# Комментарий
ИНСТРУКЦИЯ аргументы

# Основные
# FROM - родительский образ
FROM <image>[:<tag>] [AS <name>]
# пример
FROM node:12-alpine AS build

# WORKDIR - установка рабочей директории для инструкций RUN, CMD, ENTRYPOINT, COPY и ADD
WORKDIR /path/to/dir
# пример
WORKDIR /app

# COPY - копирование новых файлов или директорий из <src>
# и их добавление в файловую систему образа по адресу, указанному в <dest>
COPY <src> <dest>
COPY ["<src>", "<dest>"]
# пример
COPY package.* yarn.lock ./
# или
COPY . .
# ADD, в отличие от COPY, позволяет копировать удаленные файлы,
# а также автоматически распаковывает сжатые (identity, gzip, bzip2 или xz) локальные файлы

# ADD - копирование новых файлов, директорий или удаленного (!) файла из <src>
# и их добавление в файловую систему образа по адресу, указанному в <dest>
ADD <src> <dest>
ADD ["<src>", "<dest>"]
# пример
ADD some.txt some_dir/ # <WORKING_DIR>/some_dir/

# RUN - выполнение команды в новом слое на основе текущего образа и фиксация результата
RUN <command>
# или
RUN ["executable", "arg1", "arg2"] # Кавычки должны быть двойными
# пример
RUN npm install

# CMD - предоставление дефолтных значений исполняемому контейнеру
CMD ["executable", "arg1", "arg2"]
# или если данной инструкции предшествует инструкция ENTRYPOINT
CMD ["arg1", "arg2"]
# или
CMD command arg1 arg2
# пример
CMD [ "node", "/app/src/index.js" ]
# RUN выполняет команду и фиксирует результат,
# CMD ничего не выполняет во время сборки, а определяет команду для образа
# (!) выполняется только одна (последняя) инструкция CMD

# ENTRYPOINT - настройка исполняемого контейнера
ENTRYPOINT ["executable", "arg1", "arg2"]
ENTRYPOINT command arg1 arg2
# пример
ENTRYPOINT ["top", "-b"]
CMD ["-c"]
# docker run -it --rm --name test top -H
# top -b -H
# разница между ENTRYPOINT и CMD:
# https://docs.docker.com/engine/reference/builder/#understand-how-cmd-and-entrypoint-interact
# https://stackoverflow.com/questions/21553353/what-is-the-difference-between-cmd-and-entrypoint-in-a-dockerfile

# переменные
# ${var} или $var
# пример
FROM busybox
ENV FOO=/bar
WORKDIR ${FOO}    # WORKDIR /bar
ADD . $FOO        # ADD . /bar
COPY \$FOO /qux   # COPY $FOO /qux

# Другие
# LABEL - добавление метаданных к образу
LABEL <key>=<value>
# пример
LABEL version="1.0"

# EXPOSE - информация о сетевом порте, прослушиваемом контейнером во время выполнения
EXPOSE <port> | <port>/<protocol>
# пример
EXPOSE 3000

# ENV - установка переменных среды окружения
ENV <key>=<value>
# пример
ENV MY_NAME="No Name"

# VOLUME - создание точки монтирования
VOLUME ["/var/log"]
VOLUME /var/log

# USER - установка пользователя для использования при запуске контейнера
# в любых инструкциях RUN, CMD и ENTRYPOINT
USER <user>[:<group>]
USER <UID>[:<GID>]

# ARG - определение переменной, которая может быть передана через командную строку при
# выполнении команды `docker build` с помощью флага `--build-arg <name>=<value>`
ARG <name>[=<default value>]

# ONBUILD - добавление в образ триггера, запускаемого при использовании
# данного образа в качестве основы для другой сборки
ONBUILD <INSTRUCTION>

Справка по Dockerfile.


Рекомендации по Dockerfile


Рассмотрим следующий Dockerfile:


# syntax=docker/dockerfile:1
FROM ubuntu:18.04
COPY . /app
RUN make /app
CMD python /app/app.py

Выполнение каждой инструкции (кроме CMD) из этого файла приводит к созданию нового слоя:


  • FROM создает слой из образа ubuntu:18.04
  • COPY добавляет файлы из текущей директории
  • RUN собирает приложение с помощью make
  • CMD определяет команду для запуска приложения в контейнере

При запуске образа и генерации контейнера мы добавляем новый слой, доступный для записи, поверх остальных. Все изменения в запущенном контейнере, такие как создание новых файлов, их модификация или удаление записываются в этот слой.


Создание эфемерных контейнеров


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


Понимание контекста сборки


При выполнении команды docker build контекстом сборки, как правило, является текущая директория. Предполагается, что Dockerfile находится в этой директории. Путь к Dockerfile, находящемуся в другом месте, можно указать с помощью флага -f. Независимо от того, где находится Dockerfile, все файлы и директории из текущей директории отправляются демону в качестве контекста сборки.


В следующем примере мы


  • создаем (mkdir) директорию myapp, которая используется в качестве контекста сборки
  • переходим в нее (cd)
  • создаем файл hello с текстом "hello"
  • создаем Dockerfile, читающий (cat) содержимое файла hello
  • собираем образ с тегом helloapp:v1

mkdir myapp && cd myapp
echo "hello" > hello
echo -e "FROM busybox\nCOPY /hello /\nRUN cat /hello" > Dockerfile
docker build -t helloapp:v1 .

Размещаем Dockerfile и hello в разных директориях и собираем вторую версию образа без использования кеша предыдущей сборки (-f определяет путь к Dockerfile):


# создаем директории
mkdir -p dockerfiles context
# перемещаем файлы
mv Dockerfile dockerfiles && mv hello context
# собираем образ
docker build --no-cache -t helloapp:v2 -f dockerfiles/Dockerfile context

.dockerignore


В файле .dockerignore указываются файлы, не имеющие отношения к сборке и поэтому не включаемые в нее. Синтаксис .dockerignore похож на синтаксис .gitignore или .npmignore.


Многоэтапная сборка


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


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


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

Пример Dockerfile для Go-приложения:


# syntax=docker/dockerfile:1
FROM golang:1.16-alpine AS build

# устанавливаем инструменты
# выполняем `docker build --no-cache .` для обновления зависимостей
RUN apk add --no-cache git
RUN go get github.com/golang/dep/cmd/dep

# список зависимостей из `Gopkg.toml` и `Gopkg.lock`
# эти слои будут собираться повторно только при изменении файлов `Gopkg`
COPY Gopkg.lock Gopkg.toml /go/src/project/
WORKDIR /go/src/project/
# устанавливаем зависимости
RUN dep ensure -vendor-only

# копируем проект и собираем его
# этот слой будет собираться повторно только при изменении файлов из директории `project`
COPY . /go/src/project/
RUN go build -o /bin/project

# получаем образ, состоящий из одного слоя
FROM scratch
COPY --from=build /bin/project /bin/project
ENTRYPOINT ["/bin/project"]
CMD ["--help"]

Лишние библиотеки


Для уменьшения сложности, количества зависимостей и времени сборки следует избегать установки дополнительных и ненужных библиотек "на всякий случай".


Разделение приложений


Каждый контейнер должен иметь одну ответственность (single responsibility). Разделение приложений на несколько контейнеров облегчает горизонтальное масштабирование и переиспользуемость контейнеров. Например, стек веб-приложения может состоять из 3 отдельных контейнеров, каждый со своим уникальным образом, для управления приложением, базы данных и сервера или распределенного кеша, хранящегося в памяти. Если контейнеры зависят друг от друга для обеспечения возможности их коммуникации следует использовать сети.


Минимизация количества слоев


В старых версиях Docker каждая инструкция в Dockerfile приводила к созданию нового слоя. Сейчас новые слои создаются только инструкциями RUN,COPY и ADD. Другие инструкции создают временные промежуточные образы, которые не приводят к увеличению размера сборки.


Сортировка многострочных аргументов


Многострочные аргументы рекомендуется сортировать в алфавитном порядке. Также рекомендуется добавлять пробел перед обратным слэшем (\). Пример:


RUN apt-get update && apt-get install -y \
 bzr \
 cvs \
 git \
 mercurial \
 subversion \
 && rm -rf /var/lib/apt/lists/*

Использование кеша сборки


При сборке образа Docker изучает все инструкции в порядке, определенном в Dockerfile. После изучения инструкции Docker обращается к своему кешу. Если в кеше имеется соответствующий образ, новый образ не создается. Для сборки образа без обращения к кешу используется настройка --no-cache=true.


Рекомендации по инструкциям


FROM


В качестве основы для создания образа рекомендуется использовать официальные образы из DockerHub версии alpine.


LABEL


Подписи позволяют структурировать образы проекта, добавлять информацию о лицензиях, могут использоваться для автоматизации и т.д.


# одна подпись
LABEL com.example.version="0.0.1-beta"
# несколько подписей
LABEL vendor=ACME\ Incorporated \
     com.example.is-beta= \
     com.example.is-production="" \
     com.example.version="0.0.1-beta" \
     com.example.release-date="2021-01-12"

RUN


Длинные и сложные инструкции RUN рекомендуется разделять на несколько строк с помощью обратного слэша (\). Это делает Dockerfile более читаемым, облегчает его понимание и поддержку.


RUN apt-get update && apt-get install -y \
   package-bar \
   package-baz \
   package-foo  \
   && rm -rf /var/lib/apt/lists/*

CMD


Инструкция CMD используется для запуска программ в контейнере вместе с аргументами. CMD должна использоваться в форме CMD ["executable", "param1", "param2"]. В большинстве случаев первым элементом должен быть интерактивный терминал, такой как bash, python или perl. Например, CMD ["perl", "-de0"], CMD ["python"] или CMD ["php", "-a"]. При использовании ENTRYPOINT следует убедиться, что пользователи понимают, как работает эта инструкция.


EXPOSE


Инструкция EXPOSE определяет порты, на которых контейнер регистрирует соединения. Рекомендуется использовать порты, которые являются традиционными для приложения. Например, образ, содержащий веб-сервер Apache, должен использовать EXPOSE 80, а образ, содержащий MonghoDBEXPOSE 27017.


ENV


Для облегчения запуска программы можно использовать ENV для обновления переменной среды окружения PATH для приложения, устанавливаемого контейнером. Например, ENV PATH=/usr/local/nginx/bin:$PATH обеспечивает, что CMD ["nginx"] просто работает.


Инструкция ENV также может быть полезна для предоставления обязательных для сервиса переменных, таких как PGDATA для Postgres.


Наконец, ENV может использоваться для установки номеров версий, что облегчает их обновление.


ADD или COPY


Хотя ADD и COPY имеют похожий функционал, в большинстве случаев следует использовать COPY, поскольку эта инструкция является более прозрачной, чем ADD. COPY поддерживает копирование в контейнер только локальных файлов, а ADD также позволяет извлекать файлы из локальных архивов и получать файлы по URL, но вместо последнего лучше использовать curl или wget: это позволяет удалять ненужные файлы после извлечения.


Если Dockerfile состоит из нескольких этапов, на которых используются разные файлы из контекста, эти файлы рекомендуется копировать индивидуально. Это позволяет обеспечить инвалидацию кеша только для модифицированных файлов. Например:


COPY package.json /app
RUN npm i
# предполагается, что директория node_modules указана в .dockerignore
COPY . /app

ENTRYPOINT


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


Рассмотрим пример образа для инструмента командной строки s3cmd:


ENTRYPOINT ["s3cmd"]
CMD ["--help"]

Данный образ может быт запущен следующим образом:


docker run s3cmd

Это приведет к выводу справки.


Либо мы может передать параметры для выполнения команды:


docker run s3cmd ls s3://mybucket

Это может быть полезным при совпадении названия образа со ссылкой на исполняемый файл.


VOLUME


Инструкция VOLUME следует использовать для доступа к любой области хранения базы данных, хранилищу настроек или файлам/директориям, созданным контейнером. Крайне не рекомендуется использовать VOLUME для мутабельных и/или пользовательских частей образа.


WORKDIR


Для ясности и согласованности для WORKDIR всегда следует использовать абсолютные пути. Также WORKDIR следует использовать вместо инструкций типа RUN cd ... && do-something, которые трудно читать, отлаживать и поддерживать.


Управление данными


По умолчанию файлы, создаваемые в контейнере сохраняются в слое, доступном для записи (writable layer). Это означает следующее:


  • данные существуют только на протяжении жизненного цикла контейнера, и их сложно извлекать из контейнера, когда в них нуждается другой процесс
  • слой контейнера, доступный для записи, тесно связан с хостом, на котором запущен контейнер. Данные нельзя просто взять и переместить в другое место
  • запись в такой слой требует наличия драйвера хранилища (storage driver) для управления файловой системой. Эта дополнительная абстракция снижает производительность по сравнению с томами данных (data volumes), которые пишут напрямую в файловую систему

Docker предоставляет 2 возможности для постоянного хранения данных на хосте: тома (volumes) и bind mount. Для пользователей Linux также доступен tmpfs mount, а для пользователей Windowsnamed pipe.


Выбор правильного типа монтирования


Независимо от выбранного типа монтирования, для контейнера данные выглядят одинаково. Они представляют собой директорию или файл в файловой системе контейнера.


Разница между томами, bind mount и tmpfs mount заключается в том, где хранятся данные на хосте.





  • тома хранятся в части файловой системы хоста, управляемой Docker (/var/lib/docker/volumes/ на Linux). Процессы, не относящиеся к Docker, не должны модифицировать эту часть. Тома — лучший способ хранения данных в Docker
  • bind mount может храниться в любом месте системы хоста. Это могут быть даже важные системные файлы и директории. Модифицировать их могут любые процессы
  • tmpfs mount хранится в памяти и не записывается в файловую систему

Управление данными в Docker.


Тома (Volumes)


Тома — предпочтительный способ хранения данных, генерируемых и используемых контейнерами. Они полностью управляются Docker, в отличие от bind mount, которые зависят от структуры директории и операционной системы хоста. Преимущества томов состоят в следующем:


  • тома легче восстанавливать и мигрировать
  • томами можно управлять с помощью Docker CLI и Docker API
  • тома работают как в Linux, так и в Windows контейнерах
  • тома могут более безопасно распределяться между контейнерами
  • движки томов (volume drivers) позволяют хранить тома на удаленных хостах и облачных провайдерах, шифровать содержимое томов или добавлять в них новый функционал
  • содержимое новых томов может заполняться (population) контейнером
  • тома имеют более высокую производительность

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


Флаги -v и --mount


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


Движок тома может быть определен только с помощью --mount.


  • -v или --volume: состоит из 3 полей, разделенных двоеточием (:). Поля должны указываться в правильном порядке. Значение каждого поля не является очевидным
    • в случае именованных томов первое поле — это название тома, которое является уникальным в пределах хоста. В случае анонимных томов это поле опускается
    • второе поле — это путь файла или директории, монтируемой в контейнере
    • третье поле является опциональным и представляет собой разделенный запятыми список настроек, таких как ro
  • --mount: состоит из нескольких пар ключ/значение, разделенных запятыми. Синтаксис --mount является более многословным, зато порядок ключей не имеет значения, а значения ключей являются более очевидными
    • type: тип монтирования, может иметь значение bind, volume или tmpfs
    • source: источник монтирования. Для именованных томов — это название тома. Для анонимных может быть опущено. Может определяться как source или src
    • destination: путь монтируемого в контейнере файла или директории. Может определяться как destination, dst или target
    • readonly: если указана данная настройка, том будет доступен только для чтения. Может определяться как readonly или ro
    • volume-opt: данная настройка может определяться несколько раз, принимая пары название настройки/ее значение

-v и --mount принимают одинаковые настройки. При использовании томов с сервисами, доступен только флаг --mount.


Создание и управление томами


В отличие от bind mount, тома могут создаваться и управляться за пределами любого контейнера.


Создание тома


docker volume create my-vol

Список томов


docket volume ls

Анализ тома


docker volume inspect my-vol

Удаление тома


docker volume rm my-vol

Запуск контейнера с томом


При запуске контейнера с несуществующим томом, он создается автоматически. В следующем примере том myvol2 монтируется в директорию app контейнера:


docker run -d \
 --name devtest \
 -v myvol2:/app \
 nginx:latest

# или
docker run -d \
 --name devtest \
 --mount source=myvol2,target=/app \
 nginx:latest
# далее будет использоваться только `-v`

В данном случае том будет доступен как для чтения, так и для записи.


Использование тома с Docker Compose


Единичный сервис (single service) Compose с томом может выглядеть так:


version: "1.0"
services:
 frontend:
   image: node:lts
   volumes:
     - myapp:/home/node/app
volumes:
 myapp:

Том будет создан при первом вызове docker compose up. При последующих вызовах данный том будет использоваться повторно.


Том может быть создан отдельно с помощью docker volume create. В этом случае в docker-compose.yml может быть указана ссылка на внешний (external) том:


version: "1.0"
services:
 frontend:
   image: node:lts
   volumes:
     - myapp:/home/node/app
volumes:
 myapp:
   external: true

Популяция тома с помощью контейнера


При запуске нового контейнера, создающего том, когда контейнер содержит файлы и директории в монтируемой директории, содержимое этой директории копируется в том. После этого контейнер монтирует и использует том. Другие контейнеры, использующие том, будут иметь доступ к его предварительно заполненному (pre-populated) содержимому.


В следующем примере мы создаем контейнер nginxtest и заполняем новый том nginx-vol содержимым из директории /usr/share/nginx/html, в которой хранится дефолтная разметка:


docker run -d \
 --name=nginxtest \
 -v nginx-vol:/usr/share/nginx/html \
 nginx:latest

Пример монтирования тома, доступного только для чтения


# :ro
docker run -d \
 --name=nginxtest \
 -v nginx-vol:/usr/share/nginx/html:ro \
 nginx:latest

Резервное копирование, восстановление и передача данных томов


Для создания контейнера, монтирующего определенный том, используется флаг --volumes-from.


Резервное копирование тома


Создаем контейнер dbstore:


docker run -v /dbdata --name dbstore ubuntu /bin/bash

Следующая команда


  • запускает новый контейнер и монтирует в него том из контейнера dbstore
  • монтирует директорию из локального хоста как /backup
  • передает команду для архивации содержимого тома dbdata в файл backup.tar в директории /backup:

docker run --rm \
 --volumes-from dbstore \
 -v $(pwd):/backup \
 ubuntu tar \
 cvf /backup/backup.tar /dbdata

Восстановление данных тома


Создаем новый контейнер:


docker run -d \
 -v /dbdata \
 --name dbstore2 \
 ubuntu /bin/bash

Распаковываем архив в том нового контейнера:


docker run --rm \
 --volumes-from dbstore2 \
 -v $(pwd):/backup \
 ubuntu bash -c \
 "cd /dbdata && tar xvf /backup/backup.tar --strip 1"

Удаление томов


Данные томов сохраняются после удаления контейнеров. Существует 2 типа томов:


  • именованные: имеют определенный источник за пределами контейнера, например, awesome:/foo
  • анонимные: не имеют определенного источника, поэтому при удалении контейнера демону следует передавать инструкции по их удалению

Для удаления анонимного контейнера можно использовать флаг --rm. Например, здесь мы создаем анонимный том /foo и именованный том awesome:/bar:


docker run --rm -v /foo -v awesome:/bar busybox top

После удаления этого контейнера будет удален только том /foo.


Для удаления всех неиспользуемых томов используется команда docker volume prune.


Более подробную информацию о томах можно получить здесь.


Сети (Networks)


Сети позволяют контейнерам общаться между собой. Сетевой интерфейс Docker не зависит от платформы.


Основная функциональность сетей представлена следующими драйверами:


  • bridge (мост): дефолтный сетевой драйвер. Такие сети, как правило, используются, когда приложение состоит из нескольких автономных контейнеров, которые должны иметь возможность общаться между собой
  • host: этот драйвер также предназначен для автономных контейнеров, он позволяет использовать сети хоста напрямую, удаляя изоляцию между контейнером и хостом
  • overlay (перекрытие): такие сети объединяют нескольких демонов вместе и позволяют групповым сервисам (swarm services) взаимодействовать друг с другом. Эти сети также могут использоваться для обеспечения коммуникации между групповым сервисом и отдельным контейнером или между двумя автономными контейнерами на разных демонах
  • ipvlan: такая сеть предоставляет пользователю полный контроль над адресацией IPv4 и IPv6
  • macvlan: этот драйвер позволяет присваивать контейнеру MAC-адрес, в результате чего контейнер появляется в сети как физическое устройство
  • none: отключает сети для данного контейнера. Обычно, используется в дополнение к кастомному сетевому драйверу
  • плагины

Резюме


  • bridge: для обеспечения взаимодействия контейнеров, находящихся на одном хосте
  • host: когда сетевой стек не должен быть изолирован от хоста
  • overlay: для обеспечения взаимодействия контейнеров, находящихся на разных хостах, или для приложений, работающих вместе через групповые сервисы

Обзор сетей.


Использование дефолтной сети bridge


Сейчас мы запустим 2 контейнера alpine и посмотрим, как они могут взаимодействовать между собой.


  • Открываем терминал и выполняем следующую команду:

docker network ls

Получаем список сетей bridge, host и none.


  • Запускаем 2 контейнера alpine. ash — это дефолтный терминал Alpine (аналог bash). Флаги -dit означают запуск контейнера в фоновом режиме (-d), интерактивно (чтобы дает возможность вводить команды) (-i) и с псевдотерминалом (чтобы видеть ввод и вывод) (-t). Поскольку мы не указываем --network, контейнеры подключаются к дефолтной сети bridge:

# контейнер номер раз
docker run -dit --name alpine1 alpine ash

# и номер два
docker run -dit --name alpine2 alpine ash

Проверяем, что оба контейнера запущены:


docker container ls
# или
docker ps

  • Анализируем сеть bridge на предмет подключенных к ней контейнеров:

docker network inspect bridge

Видим созданные нами контейнеры в разделе "Containers" вместе с их IP-адресами (172.17.0.2 для alpine1 и 172.17.0.3 для alpine2).


  • Подключаемся к alpine1:

docker attach alpine1

Видим приглашение #, свидетельствующее о том, что мы являемся пользователем root внутри контейнера. Взглянем на сетевой интерфейс alpine1:


id addr show

Первый интерфейс (lo) нас не интересует. Нас интересует IP-адрес второго интерфейса (172.17.0.2).


  • Проверяем подключение к Интернету:

ping -c 2 google.com

Флаг -c 2 означает 2 попытки ping.


  • Обратимся ко второму контейнеру. Сначала по IP-адресу:

ping -c 2 172.17.0.3

Работает. Теперь попробуем обратиться по названию контейнера:


ping -c 2 alpine2

Не работает.


  • Останавливаем и удаляем контейнеры:

docker container stop alpine1 alpine2
docker container rm alpine1 alpine2

Обратите внимание: дефолтная сеть bridge не рекомендуется для использования в продакшне.


Использование кастомной сети bridge


В следующем примере мы снова создаем 2 контейнера alpine, но подключаем их к кастомной сети alpine-net. Эти контейнеры не подключены к дефолтной сети bridge. Затем мы запускаем третий alpine, подключаемый к bridge, но не к alpine-net, и четвертый alpine, подключаемый к обеим сетям.


  • Создаем сеть alpine-net. Флаг --driver bridge можно опустить, поскольку bridge является сетью по умолчанию:

docker network create --driver bridge alpine-net

  • Получаем список сетей:

docker network ls

Анализируем сеть alpine-net:


docker network inspect alpine-net

Получаем IP-адрес сети и пустой список подключенных контейнеров.


Обратите внимание на сетевой шлюз, который отличается от шлюза bridge.


  • Создаем 4 контейнера. Обратите внимание на флаг --network. При выполнении команды docker run мы можем подключить контейнер только к одной сети, поэтому подключение alpine4 к alpine-net выполняется отдельно:

docker run -dit --name alpine1 --network alpine-net alpine ash

docker run -dit --name alpine2 --network alpine-net alpine ash

docker run -dit --name alpine3 alpine ash

docker run -dit --name alpine4 alpine ash
# !
docker network connect alpine-net alpine4

Получаем список запущенных контейнеров:


docker ps

  • Анализируем bridge и alpine-net:

docker network inspect bridge

Видим, что к данной сети подключены контейнеры apline3 и alpine4.


docker network inspect alpine-net

А к этой сети подключены контейнеры alpine1, alpine2 и alpine4.


  • В кастомных сетях контейнеры могут обращаться друг к другу не только по IP-адресам, но также по названиям. Данная возможность называется автоматическим обнаружением сервиса (automatic service discovery). Подключаемся к alpine1:

docker container attach alpine1

ping -c 2 alpine2
# success

ping -c 2 alpine 4
# success

ping -c 2 alpine1
# success

  • Пробуем обратиться к alpine3:

ping -c 2 alpine3
# failure

  • Отключаемся от alpine1 и подключаемся к alpine4:

docker container attach alpine4

ping -c 2 alpine1
# success

ping -c 2 alpine2
# success

ping -c 2 alpine3
# failure
# но
ping -c 2 172.17.0.2 # IP-адрес alpine3
# success

ping -c 2 alpine4
# success

  • Все контейнеры могут подключаться к Интернету:

ping -c 2 google.com
# success

  • Останавливаем и удаляем контейнеры и сеть alpine-net:

docker container stop alpine1 alpine2 alpine3 alpine4

docker container rm alpine1 alpine2 alpine3 alpine4

docker network rm alpine-net

Это конец первой части.


Благодарю за внимание и happy coding!




Комментарии (7)


  1. qbz
    15.12.2021 16:47
    +1

    Вот это приятный разговор. Вообще, Игорь, хотел бы выразить благодарность за усердие и статьи, хоть некоторые из них, конечно, не достаточно полны/компетентны. Но, как говорится, не ошибается тот, кто ничего не делает, поэтому еще раз спасибо и продолжайте в том же духе!


    1. aio350 Автор
      15.12.2021 20:03

      Спасибо. Постараюсь


  1. zorn-v
    15.12.2021 21:15
    +1

    Странно что docker-compose упомянут вскользь и не в попад.

    Вместо зубодробительной команды можно создать файл, и поднимать одной командой.

    Или вы правда считаете что запоминать все параметры докера НУЖНО ?

    Помнить - да, запоминать - вы издеваетесь ?

    Да я помню про хелп, но одно дело составить файл и поднимать одной командой, другое - каждый раз вспоминать параметры.

    PS: Я всегда создаю docker-compose.yaml даже для одного контейнера вместо bash скрипта с кучей параметров (раньше делал наоборот)


    1. TyVik
      15.12.2021 23:20

      Зачем bash скрипт с кучей параметров, если есть make? Почти на всех проектах есть Makefile с популярными командами. Например, по `make es up` выполняется вот такая команда, которая запускает локально Elasticsearch - `docker run --name es -p 9200:9200 -p 9300:9300 -e "discovery.type=single-node" es:latest`


      1. zorn-v
        15.12.2021 23:40

        А зачем make если есть docker-compose ?

        Не, понятно что у вас mac, и вам насрать как будут запускать все остальные.

        Есть такая платформа, Windows называется.

        Сам ее не люлю )


        1. TyVik
          16.12.2021 04:26

          Во-первых, у меня Linux, во-вторых, на сервере тоже Linux, а в-третьих под виндой тоже есть Linux.

          Давно Винду не трогал, но не вижу причин почему make не должен запускаться под WSL2.


  1. zorn-v
    15.12.2021 23:40

    Нельзя удалить