Подноготная структуры Docker и подробно о всех основных функциях собрана в одном месте
- Выжимка по работе с Docker
Простыми словами ключевое отличие это отстутсвие у Docker гостевой OS. Docker ничего общего не имеет с виртуальными машинами, разве что только изолированность.
Вся суть в процессах работы Linux. К примеру при запуске работы приложения на Nodejs первым делом запускается новый процесс с помощью внутреннего вызова комманды fork
, который создаст новый процесс, при этом скоппировав предидущий процесс. Поэтому будет Bash
потом fork
потом второй Bash
после этого подменяется новым процессом. Это делается с помощью комманды execv
это заменит текущий процесс замененого Bash
на текущий процесс ноды. У этого процесса появится свой process ID и он будет уже функционировать уже в рамках этого process ID.
Этот процесс он при этом не является изолированным. Поскольку если другой процесс занимает ту же часть диска, то удалит текущий процесс. Эта проблема изорилованности процессов была до тех пор, пока в Linux не появилась команда chroot
- эта комманда позволяет получить прообраз изоляции с помощью изменения root директории. В Docker используется конечно же не chroot
, там используются namespaces
. Но это некая аналогия, которая показывает как работает Docker. Запускается процесс, потом ему изменяют root директорию на какую-то кастомную и он не будет видеть никакие другие соответствующие бинарники, папки и прочее внутри своего изолированного пространства. Он также может скопировать необходимые другие бинарники и запускатся уже с ними.
Когда запускается новый процесс, то Docker открывает новый namespace
. Фактически, когда мы запускаем контейнер, мы запускаем новый Namespace
. Он имеет несколько наборов, так званные СGroups. Которые управляют ограничениями по памяти. IPC - это управления процесами, которые позволяют создавать изолированость и общение в namespace. Свой network. Mount который говорит какие директории доступны, которые доступны и какие нет. Process ID, который может повторятся с process ID текущего, то есть при запуске на хосте, process ID является уникальным, то в namespace process ID может быть свой. User, который может быть своим по аналогии с process ID.
Кроме этого в Docker существуют некоторые обвязки, которые обвязывают все эти параметры namespace и непосредственно какие-то Vоlume и т.д.
Поэтому по сути контейнер это некая изолированная часть, изолированный namespace, который запускается на ядре хостовой машины и функционирует полностью изолировано кроме того, что оно получает доступ к ресурсам этой машины. Благодаря этой изоляции мы получаем полностью изолированное пространство в котором мы можем делать ряд различных вещей и кроме того можно запускать разные библиотеки, тем самым на одном ядре может например находится Gem Linux, а на другом namespace - Ubuntu.
Поэтому контейнер это не виртуальная машина, это изолированный namespace с дополнительными обвязками Docker в котором у нас запускается приложение, запускаются различные библиотеки с соответствующим ядром и после этого этот контейнер стартует и позволяет с ним работать с помощью удобного API.
Сам Docker не имеет ядра и поэтому мы не можем запускать процессы для одной архитектуре и запускать на другой архитектуре. То есть Docker не позволяет делать эмуляцию между процессами, поэтому если контейнер был запущен на архитектуре х64, то он успешно откроется только на такой архитектуре, а не на армовской или ещё какой-либо другой.
Внутри Docker состоит из трёх частей и когда, например, вводится комманда Docker ps
то это работа не с самим docker'ром, а с клиентом внутри docker'а. Это просто удобная СLI утилита.
У нас есть клиент - тот самый клиент из которого вводятся комманды по типу docker рs
и всё что угодно. После этого делается запрос к API к хосту, на этом хосте крутится Docker daemon, Docker daemon проверяет есть ли у нас такой image внутри, в наличии локально. Если такой image нету, то daemon пошел скачивать его из общего регистра, например пошел на Docker hub. После этого он скачивает нужный image и запускать новый контейнер. На самом деле всё что он делает это создаёт новый namespace и передаёт туда этот Image. Кладёт туда нужные либы и распаковывает этот image, чтобы всё запустить.
Docker container
- это сущность отвечающая за работу с контейнерами.
Но название container
можно опускать, поскольку в большинстве комманд он по умолчанию устроен под container, поскольку при работе с Docker очень приходится работать именно с container и поэтому команды работы с контейнерами вынесены на верхний уровень. Из-за этого следующие команды равны по своей сущности:
docker start
- docker container start
docker stop
- docker container stop
Когда мы делаем docker run
у нас скачивается image
. Этот Image
запускается и превращается в контейнер. После того как мы запустили контейнер у нас появляется жизненный цикл контейнера:
Когда мы запустили контейнер, у него появляется жизненный этап running
. Запускается наше приложение и контейнер работает.
Когда нужно остановить контейнер то необходимо использовать команду docker stop
, которая приводит контейнер в состояние - stopped
.
Когда нужно контейнер уничтожить контейнер, то необходимо использовать команду docker kill
.
Когда нужно перезагрузить контейнер, то нужна команда docker restart
.
Когда нужно поставить контейнер на паузу, то нужна команда docker pause
.
Если нужно удалить контейнер, то используется команда docker rm
Если нужно удалить контейнер, даже если он запущен, тогда необходимо применить команду docker rm -f
.
Каждая команда сопровождается сигналом, который позволяет нам завершить процесс:
Комманда Docker | Сигнал | Пояснение |
---|---|---|
docker stop | SIGTERM SIGKILL |
При введении команды, Docker'у посылается команда SIGTERM, которая завершает процесс, но если в течении какого-то времени контейнер не останавливается, то через какое-то время Docker с помощью сигнала SIGNKILL убьёт этот контейнер. То есть даже если контейнер повис, Docker всё равно его убьёт |
docker pause | SIGNSTOP | Ставит контейнер на паузу |
docker kill | SIGNKILL | Убивает контейнер, даже если он повис |
Комманда | Синтаксис | Пояснение |
---|---|---|
run --name | docker run --name <container_name> | Создание контейнера с указанным именем |
start | docker container start <container_name> | Запуск контейнера с указанным именем |
stop | docker container stop <container_name> | Остановка контейнера с указанным именем |
ps -a | docker ps -a | Вывести все (как запущенные так и остановленные) контейнеры |
ps | docker ps | Вывести все запущенные контейнеры |
remove | docker container remove <container_name> | Удалить контейнера с указанным именем |
prune | docker container prune | Удалить все остановленные контейнеры |
rename | docker remae <old_container_name> <new_container_name> | Переименовать контейнер с указанным именем на новое имя |
stats | docker stats | Выводит статистику по всем контейнерам в реальном времени таких параметров как: ЦПУ, использование памяти, выход и выход сети, ID процесса и т.д. |
inspect | docker inspect <container_name> | Получить всю подробную информацию о контейнере в формате JSON, в том числе State, ID процесс или Image`и с которых был сделан контейнер |
inspect -s | docker inspect -s <container_name> | Получить размер контейнера |
inspect -f "{{.field}}" | docker inspect -f "{{.field.field}}" <container_name> | Получить детали конкретной строчки из JSON |
Контейнер на протяжении своего жизненного цикла создаёт логи. Чтобы получить все логи конкретного контейнера необходимо ввести комманду:
docker logs <container_name>
Поскольку лента логов достаточно длинная и поэтому слабо читабельная, то уже с помощью команд Linux есть возможность витянуть из логов что-то конкретное.
Для этого необходимо использовать оператор пайпа ( | ), чтобы передать результаты предидущей команды в следующую команды, а также использовать функцию grep
. Функция grep
позволяет вытащить необходимый кусок текста описывая регулярные выражения в RegExp внутри запроса.
Синтаксис | Пояснение |
---|---|
docker logs <container_name> \ grep id |
Выводит все строки содержащие id |
docker logs <container_name> \ grep id -A 10 |
Выводит 10 строк после нахождения строки содержащую id |
docker logs <container_name> \ grep id -В 15 |
Выводит 15 строк до нахождения строки содержащую id |
docker logs <container_name> \ grep id -m 2 |
Выводит 2 первых строки содержащие id |
docker logs <container_name> text.txt | Сохранить логи, которые будут выведены в файл text.txt |
Порой необходимо войти в контейнер руками, чтобы что-нибудь сделать, подправить или посмотреть. Синтаксис docker команда для контейнера:
docker exec [параметры] <container_name> [комманда]
При этом параметры могут быть следующими:
Параметр | Пояснение |
---|---|
-i | итерактивное |
-t | псевдо tty |
-d | запуск в фоне |
-e | переменная окружения |
-u | пользователь |
-w | рабочая директория |
А комманды:
Комманда | Пояснение |
---|---|
bash | исполнить код |
pwd | выводит текущую рабочую директорию |
Любое исполнение команд сработает, если у контейнера статус running
, другими словами – контейнер запущен.
Image из себя представляет некий список слоёв. Каждый из этих слоёв имеет свой уникальный идентификатор. Это поможет сильно экономить пространство на диске.
У нас есть некий контейнер, который (впрочем как и все) состоит из слоёв. Каждый слой на самом деле это тоже Image. Это некий слепок, который содержит некую информацию. Все слои, которые содержит наш Image доступны нам только на чтение. То есть когда строится наш контейнер, он сформировывает 6 слоёв. После того как создастся наш контейнер, создастся ещё один, тонкий слой и вот этот слой доступен на запись. Поэтому сколько бы мы одинаковых контейнеров не запускали, они все будут базироватся на одном Image таким образом сильно экономя пространство.
Каждый Image имеет конкретный этап сборки и при запуске двух одинаковых контейнеров, будет накладыватся лишь один, отличающийся слой, при этом та часть, что доступна для чтения будет как раз общая.
Все комманды можно получить с помощью комманды --help
Команда | Описание | Пример комманды |
---|---|---|
pull | Выкачать Image с Docker Hub | docker pull <image_name> |
images | Просмотреть все Image'и | docker images |
save | Сохранить Image на диск | docker save --output <image_name_archive> <image_name> |
history | Просмотреть как был собран образ | docker history <image_name> |
build | Построить image из dockerfile | docker build |
import | Развернуть образ из архива | docker image import <image_archive_name> |
inspect | Получить подробную информацию по самому образу: когда был создан, его ID, название контейнеров и так далее | docker inspect <image_name> |
ls | Вывести все текущие образы, которые есть. Также работает динамическая строка форматирования | docker image ls |
prune | Чистит неактивные образы или образы у которых отсутствует тэг | docker image prune |
push | Пушит сбилденный образ в registry | docker push <image_name> |
rm | Удаляет конкретный образ | docker image rm <image_name> |
save | Сохранить образ в некоторый архив, чтобы потом куда-нибудь его перекинуть | docker image save <image_name> |
tag | Создать тэг для контейнера | docker tag <tag_name> |
Dockerfile состоит из строк, где каждая строка начинается из какой-то команды и её аргументов. Например WORKDIR /opt/app означает:
- WORKDIR комманда
- /opt/app - аргумент
Каждый раз когда добавляется строка, добавляется новый слой. Поэтому необходимо оптимизировать Dockerfile. При этом есть ограничение количества строк в размере 127.
Помимо постройки image с dockerfile есть ещё такое понятие как контекст. Контекст это путь выполнения команды docker build
со всеми вложенными директориями. Поэтому если указать начальную папку как корневую, то контекстом будет вся операционная система это будет медленно и неефективно. Поэтому важно понимать, где начинается контекст. Там где производится docker build
там и контекст.
При этом мы не можем выйти вверх. Поэтому если нам нужны какие-то файлы из верха, то нам нужно начинать строить приложение из этого верха. Также из контекста можно что-то удалить с помощью файла .dockerignore
например node_modules
как например в .gitignore
Подробнее о командах dockerfile:
- Аргументы это дополнительные параметры, которые можно передать при сборке. Аргументы никогда не остаются после того, как был собран image. Это аргументы, которые распростроняются во время билда. В финальном сборке их не будет. Это для передачи из вне. Это полезно например, когда билдится какой-нибудь нодовский модуль и нужен приватный репозиторий и туда передать какой-то токен, который должен существовать только в рамках билда и в рамках этого билда для того, чтобы установить зависимости.
FROM
это то на чём базируется image. Он должен быть обязателен при каждой сборке. Также этому image можно задать алиас, чтобы сделать multistage билдинг.ONBUILD
- это полезно только тогда, когда строится какой-то image, который базируется на другом образе.LABEL
- это мета информация. Это та информация, которая останется в финальной сборке и её можно будет посмотреть.USER
иWORKDIR
это информация о пользователе, который будет иметь доступ к рабочей папки. Всё что до строчки сUSER
выполняется с дефолтным пользователем в корневой папке, а всё что ниже - выполняется уже с привязанным пользователем.- Команда
ADD
чаще используется когда нужно добавить что-то из хостовой машины в контейнер сборки. Например это может быть гит репозиторий. Первым аргументом он принимает то, что нужно скопировать, а вторым аргументом куда это нужно скопировать. В отличии отCOPY
способен делать дополнительные фичи, такие как разархивация архива или например скачать архив с URL. - Команда
COPY
в отлчии отADD
позволяет нам коппировать инфорацию из других image при multistage build.
SHELL
- позволяет также как иUSER
иWORKDIR
установить какой shell будет использоваться после этой строки.RUN
- позволяет выполнить какую-то комманду вSHELL
например установка каких-то зависимостей из репозитория.ENV
- позволяет объявить и использовать их несколько раз. Если нам нужно передать какие-то переменные, но при этом чтобы эти переменные не передались в финальную сборку, нужно передать эти команды перед самой командой.STOPSIGNAL
- позволяет отправить специализированный сигнал.EXPOSE
- это просто некоторая документация для тех, кто будет использовать этот image о том, что можно будет прокинуть порт 80 по TCP и в него чем-то там дёрнуть и будет получено то что там спрятано.#
- комментарии.
Исходный код сохраняется в финальном образе и это является проблемой. Плюс также в production версии нам нужен лишь наш код и только node_modules, которые используются исключительно в production версии.
В multistage build мы можем скрыть наши секреты, установив их в первом образе, который зашифрует их в последующих образах.
Сетями Docker управляет библиотека libnetwork
которая позволяет управлять сетями внутри docker'а. При этом важно отметить, что libnetwork
испольузет стандартые элементы, которые есть в Linux ядре.
Libnetwork использует в себе:
Network Namespace
- этоnamespace
, которые использует контейнер.Linux Bridge
- это некоторые виртуальный мост, который позволяет передать пакеты из одного входа к другому.Virtual Ethernet Devices
- это виртуальная эмуляция, будто контейнер имеет какой-то ethernet выход и мы можем к нему подключится и что-то от него получить.IP tables
- это управление портами.
Когда у нас поднимается контейнер, он по умолчанию подключается к сети Bridge. Как только он подключается к сети внутри контейнера создаётся виртуальный ethernet адаптер, который и позволяет взаемодействовать с этой сетью. За этим мостом распологается реальный ethernet хостовой машины, который может быть как физический так и виртуальный, но который имеет реальный ethernet адрес.
Поэтому каждый контейнер имеет условную розетку (veth) при этом каждый контейнер может иметь несколько розеток, чтобы подключатся к различным сетям. Потому что один контейнер может теоритически быть подключённым к различным сетям.
Сетевой драйвер это вариант работы с сетью. Всего их в суме по умолчанию в суме 5:
bridge
- это самый часто используемый контейнер. По сути это некоторая изолированная сеть между контейнерами.host
- это сетевой драйвер позволяет убрать слой изоляции и тем самым, позволяет работать напрямую с файловой системой хостовой машины.overlay
- это тип сетей, который позволяет соединить множество хост машин. Объеденить одной сетью множество контейнеров, чтобы те могли находить друг друга. Чаще всего используется в swarm.macvlan
- используется крайне редко. Он позволяет создать уникальный адрес для контейнера и прокинуть его наружу. По сути в сети появляется новое устройство со своей сетью. Необходимо использовать крайне акуратно поскольку большое количество таких сетей может сказыватся на производительности всех сетей в целом.null
- это без сети. Когда нужно, чтобы контейнер не имел никакого доступа в какую-либо сеть.
Команда | Описание | Пример комманды |
---|---|---|
connect | Подключить контейнер к сети | docker network connect <network_name> |
create | Создать сеть | docker network create <network_name> |
disconnect | Отключить контейнер от сети | docker network disconnect <network_name> |
inspect | Параметры сети | docker network inspect <network_name> |
ls | Получить весь список сетей | docker network ls |
prune | Удалить все отключённые сети | docker network prune |
rm | Удалить конкретную сеть | docker network rm <network_name> |
Драйвер Bridge необходим, когда мы хотим соединить в рамках одной сети несколько контейнеров и одного хоста. Он обеспечивает локальный service discovery, чтобы один контейнер мог обращаться к другому сервису по имени. Часто он используется для локальной розработки, например чтобы поднять фул стэк.
При подключении контейнера к сетевому драйверу Bridge у него появляется доступ к общей сети Bridge Network. Он также получает IP в рамках этой сети. Когда контейнеров несколько в одной сети, то каждый из них получает свой IP адрес, в рамках этой сети.
Когда же у нас, например три контейнера и когда конейнеры 1 и 2 подключены к 1 сети, а 2 и 3 к другой, тогда у контейнера 2 будет возможность подключится к 1 и к 3 контейнеру, но при этом контейнеры 1 и 3 не смогут между собой общаться. Благодаря такой схеме можно изолировать контейнеры по разным сетям при этом объеденив их одним контейнером. Такой подход часто практикуется в микросервисной архитектуре, когда у нас есть одна общая шина приёма событий, но при этом каждый сервис реагирует лишь на свой тип этих событий.
Когда же нам нужен доступ к контейнеру с хостовой машины. Тогда вступает в роботу Port Mapping.
Port Mapping позволяет сказать контейнеру, что в рамках этой сети если стучатся на порт 80, то переадрисуй нас на опрт 8080.
Хост сеть позволяет нам убрать слой абстракции, которые предоставляют Docker network и получать доступ непосредственно к файловой системе хост машины. Доступ получается только с точки зрения к сети.
Это полезно когда есть одна база данных и она развёрнута на одном из хосте специально выделенным под эту базу данных и дополнительный слой абстракции не нужен, потому что контейнер всего один.
Null сеть необходима в том случае, когда нужно, чтобы вообще не было никакого доступа ни к какой сети.
Такое может быть полезно, когда например нужно чтобы контейнер сделал какую-либо одну операцию над файлами. Тогда этому контейнеру не нужна никакая сеть и он не должен никуда подключатся. Тако позволяет сэкономить ресурсы. Самый популярный вариант это контейнеры которые поднимаются, выполняет какую-то задачу и убивается, например генерация и выдача каких-то данных.
Все данные и состояние контейнера хранятся до того момента, пока контейнер не удалили. Поэтому когда мы останавливаем контейнер, в реальности данные этого контейнера больше не доступны, до момента когда мы не запустим контейнер. Вторая проблема в том, что если мы удалим контейнер, то все данные будут утеряны. Это плохо потому что можно обновлять базу данных, но данные базы данных в любом случае необходимо сохранять.
Поэтому есть Volume. Volume работают как в swarm режиме так и в обычном. Преимущественно они используются в обычном режиме, поскольку в swarm нельзя предугадать на какой ноде поднимется контейнер и там нужно специальный тип хранилища.
Есть три различных типа, как мы можем подключить Volume к нашему контейнеру:
- Volume - это основной способ рекомендованый, который позволяет подключить область хостовой машины к контейнеру. Внутри Docker создаётся специальная область, по простому - папка. Она биндится на папку внутри контейнера. Всё что попадём в папку, попадает напрямую в контейнер Docker'а.
- Bind mounts - это когда биндится не в область внутри Docker, а в любую папку файловой системы хост машины.
- tempfs - это когда данные не биндятся к определенной папке, а просто хранятся в памяти. Это может быть полезно для временного хранилища каких либо секретных данных.
Команда | Описание | Пример комманды |
---|---|---|
ls | Получить список всех Volume | docker volume ls |
create | Создать Volume | docker volume create <volume_name> |
inspect | Получить детали определенного Volume | docker volume inspect <volume_name> |
rm | Удалить Volume. Может быть исполнено, если все связанные контейнеры уже удалены | docker volume rm <volume_name> |
remove -f | Удаляет Volume в любом случае | docker volume rm <volume_name> -f |
Если заинспектить любой из Volume, то можно найти поле: Mountpoint
- это то место, где распологается данные этого Volume на хостовой машине и все данные, что будут складоваться, будут накапливатся в данной папке.
Чтобы добавить данные необходимо ввести следующую команду:
docker run —name <volume_name> -d -v <volume_name>:<docker_vowkdir>/<endpoint> <image_name>
docker run —name volume-1 -d -v demo:opt/app/data deno4:latest
или если прокидываются порты:
docker run —name <volume_name> -d -v <volume_name>:<docker_vowkdir>/<endpoint> -p <output_port>:<input_port> <image_name>
docker run —name volume-1 -d -v demo:opt/app/data -р 3000:3000 deno4:latest
Чтобы подключить ещё один контейнер в эту же папку:
docker run —name <volume_name_2> -d -v <volume_name>:<docker_vowkdir>/<endpoint> -p <output_port>:<input_port> <image_name>
docker run —name volume-2 -d -v demo:opt/app/data -р 3001:3000 deno4:latest
После, чтобы зайти в папку необходимо зайти по локальной сети на нужный порт с вызовом нужного маршрута:
curl "127.0.01:<port>/<endpoint>
curl "127.0.01:3001/get
Cоздание с подтягиванием корневого пути:
docker run —name <volume_name> -d -p <output_port>:<input_port> -v <path>:<container_folder_path> <image_name>
docker run —name volume-1 -d -p 3000:3000 -v /home/vladislav/data:/app/data demo_image
Если папки на которую биндится нету – то она создастся. После выполнения команды в Volume ничего не будет. В таком случае было просто расширено хостовая машина вместе с Docker. Это нужно, когда необходимо прокинуть какой-то config. Например положить рядом с запуском приложения какой-то config и его нужно подсунуть вместе с запуском контейнера. C помощью Bind Mount можно биндить не только папки, но и конкретные файлы. То есть в по сути это не перекладывание файла в папку, а связывания файла с Volume. Если прибиндить папку, а потом этот контейнер куда-то уйдет, то уже не будет известно, что ушли какие-то данные и они не будут захломлять систему. Если с Volumes можно сделать только volume prune, то с bind mount придется искать руками где он был примонтирован и соответсвенно ремувать его.
TempFS это по сути монтирования маленького кусочка кода внутрь памяти. Точнее это хранение кусочка файловой системы внутри памяти. При этом у этого хранения есть свои особенности:
- не работает в swarm режиме.
- нельзя шарить между контейнерами.
- удаляются после остановки контейнера.
Это может понадобится, для временного хранения данных, которые б в дальнейшем нельзя было бы найти. То есть если в контейнере были сохранены какие-то данные, то даже если нет Volume, то можно зайти в контейнер и посмотреть в writable-слой данные, где эти данные сохранены.
docker run —name <container_name> -d -p 3000:3000 —tmpfs <container_path_folder> <image_name>