Контейнеры и контейнеризация
Контейнер (Container) — стандартная единица ПО, в которую упаковывается приложение со всеми необходимыми для его полноценной работы зависимостями (кодом, средой запуска, библиотеками и настройками).
Сравнение контейнера и виртуальной машины
Будем называть хост-машиной (host machine) компьютер (сервер), ресурсы которого выделяются под контейнер или виртуальную машину.
Контейнер — процесс или сервис, который напрямую запущен на хост-машине.
Docker-демон следит за тем, чтобы контейнер запускался в полной изоляции от операционной системы хост-машины. Ничего подобного виртуальной машине при этом не создаётся.
Виртуальная машина (Virtual Machine) — изолированная операционная подсистема на хост-машине.
С помощью виртуальной машины можно внутри Windows ОС запустить Linux и наоборот. Существует множество инструментов, чтобы работать с виртуальными машинами (например, Virtual Box).
Преимущества контейнеров
- Возможность упаковать приложение вместе с его средой запуска. Это позволяет контейнеру запускаться одинаково в разных окружениях (операционных системах) и решает пооблему их настройки (подготовки к запуску приложения), а значит на каждом компьютере запуск контейнера происходит одинаково.
- Поскольку в контейнерах содержится только самое необходимое (ничего лишнего), им свойственны легковесность, быстродействие и простота настройки.
Docker
Архитектура Docker
Docker использует клиент-серверную архитектуру.
С клиента, который называется Docker-клиент (Docker client), поступают CLI-команды.
/* примеры CLI-команд */
docker build
docker ps
docker run
Клиент при помощи REST API передаёт команды серверу, который называется Docker-демон (Docker daemon).
Docker-демон собирает, запускает и раздаёт (distribute) контейнеры.
Этапы докеризации приложения
- Создание Dockerfile
- Построение образа
- Создание и запуск контейнера
- Композиция нескольких контейнеров
Создание Dockerfile
Dockerfile содержит инструкции (instructions) — последовательность действий, которые нужно выполнить, чтобы построить образ.
Простой пример Dockerfile для NodeJS-приложения.
# Dockerfile
FROM node:latest
EXPOSE 3001
WORKDIR /app
COPY ./package.json .
COPY ./src ./src
RUN npm install
RUN npm run build
CMD npm run start
Конкретные инструкции Dockerfile разобраны здесь.
Построение образа
Образ (Image) — доступный только для чтения шаблон с инструкциями о том, как запустить какой-то Docker-контейнер, содержащий внутри себя всё необходимое для выполнения этих инструкций.
Один образ может расширять другой.
Базовый образ (Base Image) — образ, который не имеет родительского образа.
Для построения образа используется команда docker build, которая принимает контекст (context) — путь к папке, с которой будет происходить работа в Dockerfile.
/* узнать текущую папку консоли */
ls
/* "." означает, что текущая папка консоли взята в качестве контекста */
docker build .
Образ строится на основании Dockerfile, который по умолчанию берётся из корня контекста.
Каждая инструкция в Dockerfile создаёт новый слой (layer) в образе.
Если Dockerfile лежит не в корне контекста, то можно явно указать путь к файлу.
docker build -f /path/to/a/Dockerfile .
Можно задать явное название образа.
docker build -t your_image_name .
Найти созданный образ можно среди других образов при помощи команды docker images.
docker build -t test .
docker images
/*
REPOSITORY TAG IMAGE ID CREATED SIZE
test latest 9468e6677939 22 seconds ago 730MB
mongo 3.4 aeaac14e1ffb 5 months ago 429MB
redis 4.0 04c446bf216f 5 months ago 89.2MB
node 10.15.3 5a401340b79f 10 months ago 899MB
*/
Образы хранятся в Docker-реестре (Docker registry).
Одним из публичных реестров является Docker Hub. Он используется по умолчанию.
// загрузить image из реестра
docker pull <image>
// загрузить image в реестр
docker push <image>
Создание и запуск контейнера
Контейнер (Container) — запускаемый экземпляр образа.
Для создания и последующего запуска контейнера по образу можно спользовать команду docker run.
docker run -d image_name
Флаг -d используется для запуска контейнера в фоновом режиме (in background), таким текущая консоль не будет занята контейнером и можно будет вводить в неё другие команды.
Можно также задать явное имя контейнеру при создании.
docker run -d --name container_name image_name
Команда docker run объединяет в себе две команды: docker create и docker start.
/* создание контейнера */
docker create image_name
/* запуск ещё не запущенного контейнера */
docker start container_id
Для просмотра списка всех запущенных контейнеров и информации о них используется команда docker ps.
docker ps
/*
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
b56a528cbe78 test "docker-entrypoint.s…" 2 days ago Up About a minute 3001/tcp fervent_brattain
*/
Для просмотра всех контейнеров (в том числе и незапущенных) используется флаг -a.
docker ps -a
Если есть необходимость посмотреть, что лежит внутри запущенного контейнера, можно зайти в него при помощи команды docker exec.
docker exec -i -t container_id bash
/* осуществляется переход в интерактивный режим */
Пример работы в интерактивном режиме.
/* вывод названий файлов и папок в текущей директории "app" */
root:/app# ls
/* вывод файла "package.json" в консоль */
root:/app# cat package.json
/* переход в папку "src" */
root:/app# cd ./src
/* выход из интерактивного режима */
root:/app/src# exit
Флаг -i отвечает за переход в интерактивный режим, флаг -t позволяет эмулировать терминал.
Для остановки контейнера используется команда docker stop.
docker stop container_id
Композиция нескольких контейнеров
Для написания приложения чаще всего не достаточно одного контейнера.
Если есть необходимость иметь несколько контейнеров в одном приложении, то нужно каждый из них настроить, как указано в этапах выше.
Обычно контейнеры зависят друг от друга, поэтому их запуск должен осуществляться в строгой последовательности. Такой запуск называется композицией контейнеров.
Чтобы было проще запускать композицию контейнеров, её шаги описывается в отдельном файле при помощи Docker Compose.
Хранение данных в Volume
Volume — предпочитительный механизм для хранения данных (persisting data), используемых в Docker-контейнере.
Volume даёт контейнеру доступ к какой-то локальной папке на хост-машине, на которой этот контейнер запущен. Файлы из Volume нельзя использовать на этапе сборки (build-time), то есть в Dockerfile нельзя использовать файлы из Volume, они доступны лишь во время выполнения (run-time).
Почему следует использовать Volume
- Volume хранится вне контейнера, поэтому он не увеличивает размер контейнера и не подвержен влиянию жизненного цикла контейнера.
Dockerfile
Использование образов с помощью FROM
Инструкция FROM инициализирует новый этап сборки и устанавливает базовый образ, функциональность которого может быть использована в последующих инструкциях.
# Dockerfile
FROM ubuntu:latest
# Dockerfile
# образ для NodeJS
FROM node:latest
# ...
# npm доступен благодаря образу node
RUN npm i
Валидный Dockerfile должен содержать как минимум одну инструкцию FROM и она должна быть первой инструкцией в файле.
Для создания базового образа используется инструкция FROM scratch.
Копирование файлов с помощью ADD И COPY
Инструкция COPY позволяет скопировать локальный файл или папку с хост-машины в образ. Она принимает два параметра: относительный путь на хост-машине, откуда копировать, и абсолютный путь, по которому данные будут доступны в контейнере.
# Dockerfile
COPY ./package.json /app/
COPY ./src /app/src
Инструкция ADD может делать то же самое, но помимо этого она может принять URL как источник для копирования или разархивировать локальный .tar файл, а затем поместить в образ.
# Dockerfile
ADD ./package.json /app/
ADD ./src /app/src
ADD archive.tar.gz /
ADD http://some_url.here /
Если нет явной необходимости в ADD, лучше использовать COPY.
Запуск команд с помощью RUN и CMD
Инструкция RUN позволяет запускать команды внутри образа (image). Эти команды запускаются один раз во время сборки (build) и записываются в образ как новый слой (layer).
# Dockerfile
RUN echo "Install modules"
RUN npm install
Инструкция CMD описывает команду по умолчанию, которая должна запускаться при запуске готового образа, то есть контейнера.
# Dockerfile
CMD npm start
Таким образом, несмотря на то, что CMD является инструкцией Dockerfile, он запускается не во время сборки, а уже в запущенном контейнере. Чаще всего командой в CMD выступает запуск сервера.
Рабочая директория WORKDIR
Инструкция WORKDIR устанавливает рабочую директорию для инструкций RUN, CMD, COPY, ADD. Все действия, связанные с перечисленными инструкциями, будут происходить в заданной при помощи WORKDIR директории.
# Dockerfile
WORKDIR /app
По умолчанию используется WORKDIR /.
Инструкция WORKDIR может быть использована несколько раз.
WORKDIR /
COPY ./package.json /app/
WORKDIR /app
COPY ./src ./src
Аргументы ARG
Инструкция ARG определяет переменную, которую можно передать во время сборки (build-time) контейнера.
- Объявление аргументов в Dockerfile.
# Dockerfile ARG argument_name ARG another_argument_name=default_value # со значением по умолчанию - Использование аргументов в Dockerfile.
# Dockerfile RUN echo ${argument_name} RUN echo ${another_argument_name} - Передача аргументов в команду сборки контейнера.
docker-compose build --build-arg port=3000 --build-arg env="local"# Dockerfile ARG port ARG env
Переменные окружения ENV
Инструкция ENV сохраняет переменную внутри контейнера. Таким образом переменная в контейнере доступна во время выполнения (run-time).
- Объявление переменных окружения в Dockerfile.
# Dockerfile ENV env=production - Передача в команду запуска контейнера.
docker run -e env=production
Если есть необходимость передать аргумент как переменную окружения, то можно сделать это следующим образом.
# Dockerfile
ARG port
ENV port=${port}
Порты и инструкция EXPOSE
Ранее уже отмечалось, контейнеры достаточно изолированы от окружающего мира, но иногда это можно контролировать.
Если внутри контейнера запущено приложение на каком-то порте (например, 3000), то оно будет доступно только внутри контейнера. Проверить, что оно действительно запущено в контейнере можно, сделав запрос на URL из консоли контейнера.
docker exec -it container_id bash
curl "http://localhost:3000"
При этом у хост-машины нет доступа к приложению, запущенному в контейнере.
Инструкция EXPOSE используется в целях документации, позволяя явно указать, какие порты используются внутри контейнера.
# Dockerfile
FROM node:12.13.1
EXPOSE 3000
Если порт контейнера выставляется наружу, то он называется выставленным (exposed).
Можно также вместо инструкции EXPOSE выставить порт при помощи флага --expose.
docker run --expose 3000 your_image
Выставление порта не является обязательным, поскольку оно не предоставляет хост-машине доступ к приложению.
Чтобы предоставить доступ хост-машине, необходимо опубликовать (publish) порт. В таком случае порт называют опубликованным (published).
При создании контейнера у него по умолчанию нет опубликованных портов.
Для публикации порта используется флаг --publish, -p. Флаг принимает порт хост-машины и порт контейнера в формате hostPort:containerPort.
docker run --publish 4000:3000 your_image
В примере выше приложение, которое запущено внутри контейнера на порте 3000, также доступно и на хост-машине на порте 4000 (http://localhost:4000).
Можно также опубликовать сразу все выставленные порты контейнера на случайные порты хост-машины при помощи флага --publish-all, -P.
docker run --publish-all your_image
Публикация на случайные порты хост-машины не удобна из-за трудности конфигурации приложений, которые от этих портов зависят.
Несколько портов лучше публиковать следующим образом.
docker run -p 3000:3000 -p 3001:3001 your_image
Пример Dockerfile для NodeJS
# Dockerfile
FROM node:12.13.1
ARG port
ARG env
EXPOSE ${port}
COPY ./package.json /app/
COPY ./src /app/src
WORKDIR /app
RUN npm install
RUN NODE_ENV=${env} npm run build
CMD npm run start
docker build --build-arg port=3001 --build-arg env=staging .
Docker Compose
Композиция контейнеров
Чаще всего приложения можно разделить на несколько контейнеров, которые зависят друг от друга. Понятно, что, чтобы приложение заработало, эти контейнеры нужно запускать вместе, причём в определённом порядке. Такой запуск называется композицией контейнеров.
Docker Compose — инструмент, позволяющий составлять композицию контейнеров (запускать приложения, состоящие из нескольких контейнеров).
Docker Compose использует файлы формала YAML (.yml).
Сервисы
Docker Compose файл состоит из сервисов (services). Сервис содержит в себе все данные, необходимые для запуска конкретного контейнера (с предварительным созданием образа для него при необходимости).
Есть два способа запустить сервис.
- Можно указать готовый образ в поле
image(можно скачать его с Docker Hub или создать самому).# docker-compose.yml version: '3.7' services: foo: image: image_name - Можно настроить этап построения в поле
build, указав там путь к Dockerfile, по которому должен быть построен образ.# docker-compose.yml version: '3.7' services: bar: build: context: ./bar dockerfile: Dockerfile
Порядок запуска сервисов
Docker Compose запускает и останавливает контейнеры в порядке их зависимостей.
Если зависимости между контейнерами не указаны, то контейнеры запускаются последовательно.
Настроить зависимость можно при помощи поля depends_on.
В примере ниже запуск сервиса server произойдёт раньше, чем запуск client.
docker-compose up
# docker-compose.yml
version: '3.7'
services:
client:
image: image_name
depends_on: server
server:
image: another_image_name
В случае остановки происходит обратная ситуация: client останавливается раньше, чем server.
docker-compose stop
Здесь важно отметить, что Docker Compose дожидается лишь окончания запуска контейнера, но не полной готовности того, что лежит внутри него. К примеру, база данных в контейнере может быть не готова к соединениям в тот момент, когда контейнер только запустился. В таких случаях нужно конфигурировать приложение таким образом, чтобы подключение к базе данных повторялось через некоторое время после каждой неудачной попытки.
Порты
Можно указать порты на хост-машине и внутри контейнера, по которым будет доступно приложение.
Сервис foo будет доступен на порте 3000 внутри контейнера.
# docker-compose.yml
version: '3.7'
services:
foo:
image: image_name
ports:
- "3000"
Сервис bar будет доступен на порте 4000 внутри контейнера и на порте 3001 на хост-машине.
# docker-compose.yml
version: '3.7'
services:
bar:
image: image_name
ports:
- "3000:4000"
Конфигурация сервиса baz эквивалетна конфигурации сервиса foo.
# docker-compose.yml
version: '3.7'
services:
baz:
image: image_name
ports:
- target: 4000 # порт, выставленный внутри контейнера
published: 3000 # опубликованный порт (на хост-машине)
Пример композиции контейнеров трёхуровнего приложения
Трёхуровневое (3-tier) приложение состоит из клиента, сервера и базы данных. Для каждого уровня необходим отдельный контейнер, а поскольку они связаны друг с другом, создаётся их композиция.
Первым подключается база данных (сервис db), поскольку её может использовать сервер. Вторым подключается сервер (сервис server), поскольку его может использовать клиент. Последним подключается клиент (сервис client).
В docker-compose может указываться уже собранный образ (builded image) вместе с командой, которая должна быть запущена в контейнере; или Dockerfile, по котому образ будет создаваться.
# docker-compose.yml
version: '3.7'
services:
db:
command: mongod
image: mongo:3.6.3
ports:
- "27017:27017"
server:
build:
context: "./server"
dockerfile: Dockerfile
ports:
- "4001:4001"
client:
build:
context: "./client"
dockerfile: Dockerfile
ports:
- "4000:4000"
Переменные ENVIRONMENT и ARGS
Перееменные окружения ENVIRONMENT передаются в уже запущенные контейнеры.
# docker-compose.yml
client:
environment:
- NODE_ENV: production
- SERVER_URL: xxx
Переменные ARGS доступны во время построения образа (build image).
# docker-compose.yml
client:
build:
args:
- port: 3000
- env: production
Передача и использование аргументов в Docker Compose
- Передача любых аргументов осуществляется при запуске Docker Compose в формате
argument=value.CLIENT_PORT=3000 ENV=production docker-compose up --build - Переданные аргументы доступны для использования в YAML-файле в формате
${argument}. Есть возможность задать значение по умолчанию:${argument:-defaultValue}. Без некоторых значений по умолчанию (например, для портов) может возникать ошибка приведения типов.# docker-compose.yml version: '3.7' services: client: build: context: "./client" dockerfile: Dockerfile args: port: ${CLIENT_PORT} env: ${ENV} environment: NODE_ENV: ${ENV} ports: - "${CLIENT_PORT:-3000}:${CLIENT_PORT:-3000}" # по умолчанию 3000:3000
Проверка конфигурации
Можно проверить правильность настройки, а также посмотреть все установленные переменные при помощи следующей команды.
docker-compose config