Контейнер (Container) — стандартная единица ПО, в которую упаковывается приложение со всеми необходимыми для его полноценной работы зависимостями (кодом, средой запуска, библиотеками и настройками).
Будем называть хост-машиной (host machine) компьютер (сервер), ресурсы которого выделяются под контейнер или виртуальную машину.
Контейнер — процесс или сервис, который напрямую запущен на хост-машине.
Docker-демон следит за тем, чтобы контейнер запускался в полной изоляции от операционной системы хост-машины. Ничего подобного виртуальной машине при этом не создаётся.
Виртуальная машина (Virtual Machine) — изолированная операционная подсистема на хост-машине.
С помощью виртуальной машины можно внутри Windows ОС запустить Linux и наоборот. Существует множество инструментов, чтобы работать с виртуальными машинами (например, Virtual Box).
- Возможность упаковать приложение вместе с его средой запуска. Это позволяет контейнеру запускаться одинаково в разных окружениях (операционных системах) и решает пооблему их настройки (подготовки к запуску приложения), а значит на каждом компьютере запуск контейнера происходит одинаково.
- Поскольку в контейнерах содержится только самое необходимое (ничего лишнего), им свойственны легковесность, быстродействие и простота настройки.
Docker использует клиент-серверную архитектуру.
С клиента, который называется Docker-клиент (Docker client), поступают CLI-команды.
/* примеры CLI-команд */
docker build
docker ps
docker run
Клиент при помощи REST API передаёт команды серверу, который называется Docker-демон (Docker daemon).
Docker-демон собирает, запускает и раздаёт (distribute) контейнеры.
- Создание 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 — предпочитительный механизм для хранения данных (persisting data), используемых в Docker-контейнере.
Volume даёт контейнеру доступ к какой-то локальной папке на хост-машине, на которой этот контейнер запущен. Файлы из Volume нельзя использовать на этапе сборки (build-time), то есть в Dockerfile нельзя использовать файлы из Volume, они доступны лишь во время выполнения (run-time).
- Volume хранится вне контейнера, поэтому он не увеличивает размер контейнера и не подвержен влиянию жизненного цикла контейнера.
Инструкция FROM инициализирует новый этап сборки и устанавливает базовый образ, функциональность которого может быть использована в последующих инструкциях.
# Dockerfile
FROM ubuntu:latest
# Dockerfile
# образ для NodeJS
FROM node:latest
# ...
# npm доступен благодаря образу node
RUN npm i
Валидный Dockerfile должен содержать как минимум одну инструкцию FROM и она должна быть первой инструкцией в файле.
Для создания базового образа используется инструкция FROM scratch
.
Инструкция 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 позволяет запускать команды внутри образа (image). Эти команды запускаются один раз во время сборки (build) и записываются в образ как новый слой (layer).
# Dockerfile
RUN echo "Install modules"
RUN npm install
Инструкция CMD описывает команду по умолчанию, которая должна запускаться при запуске готового образа, то есть контейнера.
# Dockerfile
CMD npm start
Таким образом, несмотря на то, что CMD является инструкцией Dockerfile, он запускается не во время сборки, а уже в запущенном контейнере. Чаще всего командой в CMD выступает запуск сервера.
Инструкция WORKDIR устанавливает рабочую директорию для инструкций RUN
, CMD
, COPY
, ADD
. Все действия, связанные с перечисленными инструкциями, будут происходить в заданной при помощи WORKDIR
директории.
# Dockerfile
WORKDIR /app
По умолчанию используется WORKDIR /
.
Инструкция WORKDIR
может быть использована несколько раз.
WORKDIR /
COPY ./package.json /app/
WORKDIR /app
COPY ./src ./src
Инструкция 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 сохраняет переменную внутри контейнера. Таким образом переменная в контейнере доступна во время выполнения (run-time).
- Объявление переменных окружения в Dockerfile.
# Dockerfile
ENV env=production
- Передача в команду запуска контейнера.
docker run -e env=production
Если есть необходимость передать аргумент как переменную окружения, то можно сделать это следующим образом.
# Dockerfile
ARG port
ENV port=${port}
Ранее уже отмечалось, контейнеры достаточно изолированы от окружающего мира, но иногда это можно контролировать.
Если внутри контейнера запущено приложение на каком-то порте (например, 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
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 использует файлы формала 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 передаются в уже запущенные контейнеры.
# 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 в формате
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