Привет, друзья!
Хочу поделиться с вами заметками о Docker
.
Заметки состоят из 4 частей: 2 теоретических и 2 практических.
Если быть более конкретным:
- эта часть посвящена самому
Docker
,Docker CLI
иDockerfile
; - в второй части рассказывается о
Docker Compose
; - в третьей части мы разработаем приложение, состоящее из 3 сервисов (клиента, админки и API) и базы данных (PostgreSQL);
- в четвертой части мы это приложение "контейнеризуем".
Если вам это интересно, прошу под кат.
Что такое 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
При выполнении этой команды происходит следующее:
- Поскольку на нашей машине нет образа
ubuntu
,Docker
загружает его из реестра (то же самое делает командаdocker pull ubuntu
). Docker
создает новый контейнер (то же самое делает командаdocker container create
).- В качестве последнего слоя
Docker
выделяет контейнеру файловую систему для чтения и записи. Это позволяет запущенному контейнеру создавать и модифицировать файлы и директории в его локальной файловой системе. - Поскольку мы не указали сетевых настроек,
Docker
создает сетевой интерфейс для подключения контейнера к дефолтной сети. Это включает в себя присвоение контейнеруIP-адреса
. Контейнеры могут подключаться к внешним сетям через сетевое соединение хоста. Docker
запускает контейнер и выполняет/bin/bash
. Поскольку контейнер запущен в интерактивном режиме и подключен к терминалу (благодаря флагам-i
и-t
), мы можем вводить команды и получать результаты в терминале.- Выполнение команды
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
:
# 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
, а образ, содержащий MonghoDB
— EXPOSE 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
, а для пользователей Windows
— named pipe
.
Выбор правильного типа монтирования
Независимо от выбранного типа монтирования, для контейнера данные выглядят одинаково. Они представляют собой директорию или файл в файловой системе контейнера.
Разница между томами, bind mount
и tmpfs mount
заключается в том, где хранятся данные на хосте.
- тома хранятся в части файловой системы хоста, управляемой
Docker
(/var/lib/docker/volumes/
наLinux
). Процессы, не относящиеся кDocker
, не должны модифицировать эту часть. Тома — лучший способ хранения данных вDocker
bind mount
может храниться в любом месте системы хоста. Это могут быть даже важные системные файлы и директории. Модифицировать их могут любые процессыtmpfs mount
хранится в памяти и не записывается в файловую систему
Тома (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
Список томов
docker 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!