Docker: что, зачем и почему
Введение
Наверное, не осталось компаний, которые игнорируют Docker. Docker — очевидный стандарт в индустрии уже несколько лет, очень удобен и в разработке, и для деплоя (а инструменты оркестрации контейнеров очень помогают в деплое контейнеров в больших приложениях). Здесь я расскажу, как он работает, почему его минусы выдуманы и как с ним вообще работать.
Начнем сначала: виртуализация
Когда-то, чтобы на одном сервере изолированно запускать несколько окружений (в данном случае — операционных систем в хостовой операционной системе), мы использовали виртуализацию. Виды виртуализации (аппаратная, программная) мы с вами здесь разбирать не будем — статья совсем не о том. Вкратце — есть программа-гипервизор, которая умеет запускать виртуальные компьютеры внутри операционной системы — хоста.
Звучит круто, правда? Но затраты ресурсов на сам гипервизор не радуют совсем. Поэтому в 2005 году разработчики Linux добавили в ядро такую штуку, как OpenVZ.
OpenVZ
OpenVZ — это первая попытка уйти от виртуализации в привычном смысле и сократить использование ресурсов. Это технология виртуализации на уровне операционной системы (если точнее, то на уровне ядра Linux). Как говорят разработчики Linux, затраты на виртуализацию составляют всего 1-3%, что несравнимо меньше стандартных на тот момент решений.
Но появилось ограничение: с помощью OpenVZ можно запускать только дистрибутивы Linux.
Плавно перейдем к контейнерам
Вернемся в 2002 год, когда в ядро Linux добавили первое пространство имен для изоляции файловой системы — mount
. А в 2007 году Google разработал Process Containers, который умел ограничивать использование аппаратных ресурсов для процессов. Позднее этот механизм, к тому времени переименованный в cgroups
, был добавлен в ядро Linux. В 2013 году это расширили и добавили в ядро пространства имен пользователей (user
).
А еще в 2008 году (простите, что так перемещаюсь во времени, но так нужно) в Linux добавили такую штуку, как Linux Containers (LXC).
LXC
Эта штука умела запускать несколько контейнеров Linux на одном сервере. Она использовала уже существующий cgroups
и неймспейсы. И это рано или поздно должно было превратиться во что-то очень важное и удобное. Так и случилось.
Docker
Наконец-то мы доехали до основной темы статьи :) в 2013 году никому не известная компания Docker, Inc выпустила свой продукт — Docker. На тот момент Docker был просто удобной оберткой над LXC, но именно из-за удобства он очень быстро набрал популярность.
Позднее он перешел на собственную библиотеку libcontainer
(не менее связанную с ядром Linux). А когда стало понятно, что контейнеры — это надолго, компании Linux Foundation и Docker в 2015 году создали Open Container Initiative, которая до сих пор занимается разработкой стандартов для Linux-контейнеров.
В чем вообще прикол контейнеров?
Контейнеры — это такая прекрасная штука, которая может запустить операционную систему (Linux, да) в отдельном процессе, изолированном от остальной системы. Сам он будет уверен, что в системе только он и запущен. А изоляция эта обеспечивается все теми же containers
и cgroups
. Собственно, Docker так и работает.
Давайте определимся с понятиями (не теми, что на зоне)
- Image. Образ. Файл, в который упакована среда исполнения приложения и самое это приложение. То, из чего вы запускаете контейнеры.
- Registry Server. То место, где хранятся образы — туда мы можем запушить наш образ или стянуть оттуда существующий. Самый популярный и дефолтный — Docker Hub, но можно создавать и свои.
- Container. То, что запускается из образа, тот самый изолированный процесс.
- Container Engine. Движок контейнеризации. То, что скачивает образы и запускает контейнеры (ну, не совсем, это нам так кажется). Ну, собственно, главный пример — Docker.
- Container Runtime. А вот эта штука действительно запускает контейнеры.
runc
,crun
, вот это все. - Host. Система, где все это происходит.
А теперь можно и к практике
Что мы с этим докером делаем? Скачиваем образы (по дефолту с Docker Hub, но можно указать и другой registry server). Это делается командой docker pull
. К примеру: docker pull mongo:6.2
скачает вам образ MongoDB версии 6.2. Если не указывать версию явно, скачается последняя версия (latest
). Команда эта необязательна: мы можем сделать docker create mongo:6.2
(создать на основе скачанного образа контейнер) или docker run mongo:6.2
(создать из образа контейнер и запустить его). Тогда образ скачается автоматически, если он еще не скачан.
Вот самые популярные команды, которые мы запускаем:
# справочная информация
docker --help # список доступных команд
docker <command> --help # информация по команде
docker --version # версия Docker
docker info # общая информация о системе
# работа с образами
docker search debian # поиск образов по ключевому слову debian
docker pull ubuntu # скачивание последней версии (тег по умолчанию latest) официального образа ubuntu (издатель не указывается) из репозитория по умолчанию docker.io/library
docker pull prom/prometheus # скачивание последней версии (latest) образа prometheus от издателя prom из репозитория docker.io/prom
docker pull docker.io/library/ubuntu:18.04 # скачивание из репозитория docker.io официального образа ubuntu с тегом 18.04
docker images # просмотр локальных образов
docker rmi <image_name>:<tag> # удаление образа. Вместо <image_name>:<tag> можно указать <image_id>. Для удаления образа все контейнеры на его основе должны быть как минимум остановлены
docker rmi $(docker images -aq) # удаление всех образов
# работа с контейнерами
docker run hello-world # Hello, world! в мире контейнеров
docker run -it ubuntu bash # запуск контейнера ubuntu и выполнение команды bash в интерактивном режиме
docker run --name docker-getting-started --publish 8080:80 docker/getting-started # запуск контейнера gettind-started с отображением (маппингом) порта 8080 хоста на порт 80 внутрь контейнера
docker run --detach --name mongodb docker.io/library/mongo:4.4.10 # запуск контейнера mongodb с именем mongodb в фоновом режиме. Данные будут удалены при удалении контейнера!
docker ps # просмотр запущенных контейнеров
docker ps -a # просмотр всех контейнеров (в том числе остановленных)
docker stats --no-stream # просмотр статистики
docker start alpine # создание контейнера из образа alpine
docker start <container_name> # запуск созданного контейнера. Вместо <container_name> можно указать <container_id>
docker start $(docker ps -a -q) # запуск всех созданных контейнеров
docker stop <container_name> # остановка контейнера. Вместо <container_name> можно указать <container_id>
docker stop $(docker ps -a -q) # остановка всех контейнеров
docker rm <container_name> # удаление контейнера. Вместо <container_name> можно указать <container_id>
docker rm $(docker ps -a -q) # удаление всех контейнеров
# система
docker system info # общая информация о системе (соответствует docker info)
docker system df # занятое место на диске
docker system prune -af # удаление неиспользуемых данных и очистка диска
Есть прикол: внутри контейнера есть собственная файловая система, которая снаружи не видна и удаляется при удалении контейнера. Иногда мы хотим пробросить какие-то файлы внутрь контейнера, чтобы сохранить их на хосте (ну, к примеру, с той же монгой мы наверняка хотим иметь все данные в базе локально). Так как мы можем пошарить данные контейнера с хостом?
Есть два метода:
- named volumes — именованные тома хранения данных. Cохраняются в
./docker/volumes
, не удаляются при удалении контейнера, могут шариться между несколькими контейнерами. - bind mount — монтирование каталога с хоста в контейнер. Простой проброс каталога с хоста в контейнер. Таким образом, данные в проброшенных файлах доступны и хранятся локально.
Вот как это все использовать:
# справочная информация
docker <command> --help
# named volume
docker run --detach --name jenkins --publish 80:8080 --volume=jenkins_home:/var/jenkins_home/ jenkins/jenkins:lts-jdk11 # запуск контейнера jenkins с подключением каталога /var/jenkins_home как тома jenkins_home
docker volume ls # просмотр томов
docker volume prune # удаление неиспользуемых томов и очистка диска. Для удаления тома все контейнеры, в которых он подключен, должны быть остановлены и удалены
# bind mount
# запуск контейнера node-exporter с монтированием каталогов внутрь контейнера в режиме read only: /proc хоста прокидывается в /host/proc:ro внутрь контейнера, /sys - в /host/sys:ro, а / - в /rootfs:ro
docker run \
-p 9100:9100 \
-v "/proc:/host/proc:ro" \
-v "/sys:/host/sys:ro" \
-v "/:/rootfs:ro" \
--name node-exporter prom/node-exporter:v1.1.2
Создание образов
Да, можно не просто качать уже существующие образы, но и создавать свои. Есть такая штука — Dockerfile
.
Вот супер-базовый пример создания образов (на самом деле, этот самый докерфайл — просто набор команд, которые нужно выполнить, чтобы создать образ):
# Dockerfile
# создание файла Dockerfile декларативного описания
FROM ubuntu:20.04
RUN apt-get update && apt-get install -y iputils-ping
# запуск команды build из каталога с Dockerfile для создания образа simust/ubuntu-ping:20.04
docker build -t ubuntu-ping:20.04 .
docker images
# tag, login, push
docker tag ubuntu-ping:20.04 our-registry/ubuntu-ping:20.04 # создание из локального образа ubuntu-ping:20.04 тега с репозиторием для издателя simust
docker images
# вход в репозиторий docker.io под пользователем simust и отправка образа
docker login -u simust docker.io
docker push our-registry/ubuntu-ping:20.04
Понятно, что registry можно не указывать, если нам достаточно иметь образ локально или мы хотим использовать Docker Hub как дефолтный registry.
Docker Compose
Docker Compose — это такая удобная простая штука для развертывания проектов. Она объединяет в себе сборку, загрузку и запуск нескольких контейнеров. По умолчанию для такого используется файл docker-compose.yml
.
Выглядеть он может примерно так:
version: '3.5'
services:
postgres:
image: postgres:latest
restart: unless-stopped
volumes:
- ./private/var/lib/postgresql:/var/lib/postgresql
environment:
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=postgres
- POSTGRES_DB=test_app_db
healthcheck:
test: ['CMD-SHELL', 'pg_isready -U postgres']
interval: 5s
timeout: 5s
retries: 5
ports:
- '5432:5432'
mongo:
image: mongo:latest
restart: unless-stopped
volumes:
- ./private/mongodb_data_container:/var/lib/postgresql
healthcheck:
test: echo 'db.runCommand({serverStatus:1}).ok' | mongosh admin -u root -p rootpassword --quiet | grep 1
interval: 5s
timeout: 5s
retries: 5
environment:
MONGO_INITDB_ROOT_USERNAME: root
MONGO_INITDB_ROOT_PASSWORD: rootpassword
ports:
- "27017:27017"
api:
build:
context: ./
dockerfile: ./deploy/Dockerfile
environment:
- POSTGRES_DB_HOST=postgres
- POSTGRES_DB_USERNAME=postgres
- POSTGRES_DB_PASSWORD=postgres
- POSTGRES_DB_NAME=test_app_db
- POSTGRES_DB_PORT=5432
- MONGO_HOST=mongo
- MONGO_USERNAME=root
- MONGO_PASSWORD=rootpassword
- MONGO_PORT:27017
- APP_PORT:3000
ports:
- "3000:3000"
depends_on:
mongo:
condition: service_healthy
postgres:
condition: service_healthy
Выглядит понятно, правда? Мы хотим скачать последние версии postgres и mongo и сбилдить наше приложение из докерфайла, после чего запустить все это добро, дождаться готовности postgres и mongo и запустить наше приложение, сделав так, чтобы оно было доступно на 3000 порту.
Cобираем мы все это дело так: docker-compose build
, а запускаем так: docker-compose up
. Стопнуть все можно так: docker-compose down
, почитать логи так: docker-compose logs -f app
. docker-compose ps
покажет нам запущенные сервисы, а docker-compose exec [service name] [command]
— выполнить команду внутри запущенного сервера.
Итоги
Я очень постарался просто и коротко все объяснить, но в статье такого формата не распишешь все возможности Docker. Эта статья поможет вам базово научиться работать с докером, но, пожалуйста, привыкайте читать документацию. Вот она, документация — там все сильно подробнее.
А так — ну, контейнер вы запустить сможете, образ сбилдить — тоже. Уже неплохо.
Интересный пост?
Вот еще похожие:
- Событийно-ориентированная архитектура и Node.js Events
- Реактивное программирование: теория и практика
- Как и зачем писать тесты?
- Функциональное программирование. Что это и зачем?
- Микросервисы или монолит?