Объяснили как работает Docker, как запустить контейнер и создать образ. А в конце поделились примером переноса монолитного приложения в контейнеры.
11К открытий22К показов
Илья Егоров
Руководитель IT отдела
Андрей Аверков
Team Lead Python
Docker Daemon и CLI
Работу Docker обеспечивают две центральные подсистемы:
Docker Daemon — сервер, работающий в фоновом режиме. Он слушает запросы от CLI и управляет жизненным циклом контейнеров.
Docker CLI — интерфейс командной строки Docker. Через строку отдаётся команда на поднятие, запуск или остановку контейнеров.
Docker CLI передаёт команды Docker Daemon выполнить конкретную команду. Причем CLI можно как установить на хост системе, так и настроить удалённый доступ — общение с Daemon происходит по REST API.
Функционал Docker Daemon не ограничивается запуском или остановкой контейнеров: система регулирует в том числе сети и порты, логирует контейнеры. Ниже приведу самые частотные команды к Docker Daemon.
Шпаргалка по Docker CLI
Основные команды:
# запуск контейнера на основе указанного образа
docker run <имя_образа>
# показать список активных контейнеров
docker ps
# показать все контейнеры, включая остановленные
docker ps -a
# остановить контейнер
docker stop <идентификатор_контейнера>
# удалить контейнер
docker rm <идентификатор_контейнера>
# показать список всех локальных образов
docker images
# загрузить образ из Docker Hub
docker pull <имя_образа>
# удалить локальный образ
docker rmi <идентификатор_образа>
Создание и работа с образами:
# сборка образа на основе Dockerfile
docker build -t <имя_образа>:<тег> <путь_к_Dockerfile>
# пометить образ новым тегом
docker tag <старый_тег> <новый_тег>
# переименовать и пометить образ для загрузки в другой репозиторий
docker tag <имя_образа>:<старый_тег> <новый_репозиторий>/<новый_тег>
# отправка образа в Docker Hub или другой реестр
docker push <имя_репозитория>/<имя_образа>:<тег>
Сети и порты:
# показать список сетей
docker network ls
# определить соответствие портов при запуске контейнера
docker run -p <локальный_порт>:<контейнерный_порт> <имя_образа>
Работа с Docker Compose:
# запустить сервисы, определенные в файле `docker-compose.yml`
docker-compose up
#остановить и удалить сервисы, описанные в файле `docker-compose.yml`
docker-compose down
Работа с Docker Volumes:
# создать Docker Volume
docker volume create <имя_volume>
# запустить контейнер, подключив Volume
docker run -v <имя_volume>:<путь_в_контейнере> <имя_образа>
Логирование и мониторинг:
# показать логи контейнера
docker logs <идентификатор_контейнера>
# отобразить статистику использования ресурсов контейнера
docker stats <идентификатор_контейнера>
Dockerfile
Команды по созданию образа фиксируются в необработанном (raw) текстовом документе — Docker-файле:
ИНСТРУКЦИЯ аргумент(ы)
где ИНСТРУКЦИЯ — команда для Docker Daemon, а аргумент(ы) — сам аргумент или конкретные значения, которые передаются в ИНСТРУКЦИЮ.
Инструкции нечувствительны регистру, однако их принято писать «капсом», чтобы визуально отличать от аргументов.
Инструкции объясняют, что Docker Daemon должен сделать до, во время или после запуска контейнера из образа.
Основные инструкции для Dockerfile
FROM указывает базовый образ, на основе которого нужно создать новый. Чаще всего FROM используется для образов с операционной системой и предустановленными компонентами.
RUN указывает, какие команды необходимо выполнить внутри контейнера во время сборки образа. Так можно установить зависимости или обновить пакеты до нужной версии.
COPY и ADD копирует файлы из локальной файловой системы в контейнер. Чаще всего копируют исходный код приложения.
WORKDIR устанавливает рабочую директорию для последующих инструкций. Так можно последовательно работать с файлами в разных директориях.
CMD определяет аргументы по умолчанию при запуске контейнера.
ENTRYPOINT задаёт команду, которая будет выполнена при запуске контейнера.
Пример Docker-файла для приложения на Python:
# Используем базовый образ с Python
FROM python:3.8
# Устанавливаем зависимости
RUN pip install flask
# Копируем исходный код в образ
COPY . /app
# Указываем рабочую директорию
WORKDIR /app
# Определяем команду для запуска приложения
CMD ["python", "app.py"]
Образ Docker (Docker Image)
Для того, чтобы из Dockerfile создать образ и запустить контейнер, нужно:
Перейти в директорию, где лежит dockerfile.
Командой docker build создать образ из файла.
По необходимости, проверить образы командой docker images.
Запустить контейнер из образа командой docker run.
В работе с образами можно использовать теги, чтобы указывать версию образов. По умолчанию, Docker присваивает тег latest при сборке.
Для отправки образа в реестр Docker Hub используются следующие команды:
docker tag my-python-app:v1.0 username/my-python-app:v1.0
docker push username/my-python-app:v1.0
Загрузка образа из реестра выполняется командой:
docker pull username/my-python-app:v1.0
Образы Docker статичны. А вот контейнеры — изменяемы. Чтобы «обновить» образ, можно запустить из него контейнер, внести изменения и сохранить состояние в новый образ. Делается это с использованием команды docker commit:
docker commit -m "Добавлены изменения" -a "Автор" container_id username/my-python-app:v1.1
Образ Docker — стандартный формат, то есть с ним может работать Docker Daemon на любой платформе. Это позволяет безболезненно портировать проекты с одной системы на другую — контейнеры упаковываются в образы и переносятся. А изоляция всех зависимостей и компонентов внутри образа гарантирует, что проект точно «встанет» на целевую платформу с Docker без дополнительной настройки.
Контейнер (Docker Container)
Контейнер — это работающий в изолированной среде экземпляр образа (инстанс). В один контейнер «упаковывается» один работающий серверный процесс.
Можно, конечно, поместить несколько процессов, хоть целый монолит — строгих ограничений со стороны инструмента нет. Но это считается ошибкой проектирования микросервисной архитектуры. Docker позволяет настраивать взаимодействие контейнеров с внешней средой и другими контейнерами, а также регулировать потребление ресурсов. Так что весомых причин пытаться уместить всё в одном нет.
Дополнительные возможности
Если необходимо, чтобы контейнер работал с собственным экземпляром данных, не изменяя оригинал, мы можем смонтировать директорию из хост-системы в сам контейнер. Делается это командой:
docker run -v /путь/к/хост-директории:/путь/в/контейнере имя_образа
Docker Volumes — хранилища, которые связываются с контейнером, но не привязываются к его жизненному циклу. Это означает, что любые данные, которые контейнер отправляет в Volumes, сохранятся даже если контейнер остановить или уничтожить.
# команда для создания Volume в контейнере
docker run -v my_volume:/путь/в/контейнере имя_образа
Чтобы передать переменные окружения в контейнер, используется флаг -e совместно с командой RUN:
docker run -e MY_VARIABLE=value имя_образа
Контейнер может экспортировать порты для взаимодействия с «внешним миром». Это особенно актуально для веб-приложений, где порты могут использоваться для доступа к веб-серверу.
docker run -p 8080:80 имя_образа
Можно ввести ограничения на ресурсы, используемые контейнером, такие как объем оперативной памяти или количество ядер CPU.
ускорять развертывание — образы скачиваются сразу на целевую систему и готовы к работе;
автоматизировать процессы сборки, тестирования и развертывания контейнеров.
Docker Hub — публичный реестр, в котором хранятся общедоступные образы (дистрибутивов Linux, баз данных, языков и прочее). Организации могут создавать собственные приватные реестры Docker, чтобы хранить конфиденциальные данные.
Создание приватного реестра Docker
Установка Docker Distribution
Docker Distribution — это официальная реализация протокола Docker Registry. Установим его на сервер, который будет служить приватным реестром.
docker run -d -p 5000:5000 --restart=always --name registry registry:2
Эта команда запускает приватный реестр на порту 5000. Опционально можно настроить HTTPS с использованием SSL-сертификата.
Использование Приватного Реестра
Теперь можно использовать реестр для хранения и распространения приватных Docker-образов.
# помечаем образ
docker tag my-image localhost:5000/my-image
# отправляем образ в приватный реестр
docker push localhost:5000/my-image
Запускаем монолит на сервере без Docker
В README приложения можно найти подробную инструкцию как развернуть его на сервере. Для примера возьмём README монолитного приложения, который мы намеренно сократили.
Docker Compose — инструмент для запуска многоконтейнерных приложений в Docker. В .yaml-файле указываются все необходимые настройки и команды. Запуск контейнеров из compose-файла производится командой docker-compose up.
В .yaml-файле мы можем увидеть из каких контейнеров container_name (и каких версий) запускается наше ранее монолитное приложение. А ключевое слово {stage} — это ветка в GitLab, из которой будет подниматься контейнер. По желанию, на одном сервере мы можем запускать контейнеры из разных веток.
От того, что мы разбили приложение на микросервисы, оно не перестало быть монолитным. Микросервисность продукта закладывается на стадии его проектирования и создания, когда каждая задача выделяется в отдельный сервис.
Строка в compose dockerfile: Dockerfile собирает наш контейнер. В файле содержатся такие инструкции:
dockerfile
FROM python:3.6.9-buster
ENV DJANGO_SETTINGS=advgame.local_settings
ARG DEBIAN_FRONTEND=noninteractive
RUN apt-get update \
# dependencies for building Python packages
&& apt-get install -y build-essential \
# psycopg2 dependencies
&& apt-get install -y libpq-dev \
# Translations dependencies
&& apt-get install -y gettext \
# Cron
&& apt-get install -y cron \
# Vim
&& apt-get install -y vim \
# cleaning up unused files
&& apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false \
&& rm -rf /var/lib/apt/lists/*
# Set timezone
ENV TZ=Europe/Moscow
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
# Have to invalidate cache here because Docker is bugged and doesn't invalidate cache
# even if requirements.txt did change
ADD ../requirements.txt /requirements.txt
RUN pip install -r /requirements.txt
COPY ./docker-compose/start.sh /start
RUN chmod +x /start
# Copy hello-cron file to the cron.d directory
COPY ./docker-compose/crontab.docker /etc/cron.d/crontab.docker
# Give execution rights on the cron job
RUN chmod 0644 /etc/cron.d/crontab.docker
# Apply cron job
RUN crontab /etc/cron.d/crontab.docker
COPY . /app
WORKDIR /app
Далее мы написали make-файл который позволит нам управлять конфигурированием проекта:
Make создает своего рода короткий алиас команд для управления сервисами. В нём можно инициализировать проект, пересоздать базу, собрать фронт и так далее. Эти же команды мы будем использовать в GitLab CI файле.
Далее мы запускаем команду make init для инициализации проекта.
В compose-файле ничего не сказано про порты. Основная причина в том, что обращаться к проекту мы будем по доменному имени с помощью Traefik. Приложения работает отдельно от compose-файлов и версий проекта: о появлении новых контейнеров Traefik узнаёт от Docker Daemon, а конфигурация для приложения прописывается в compose-файле после ключевого слова .
Traefik проксирует трафик к контейнеру на основе hostname (не только по HTTP/HTTPS), запрашивает LE-сертификат, сам его продлевает. При это указывать, на какой IP или hostname проксировать или менять конфиг Traefik не нужно.
Если мы поднимаем локальные контейнеры с локальным доменным именем, запросить LE-сертификат не получится. Поэтому с web придётся общаться по HTTP, а в Traefik отключить редирект на HTTPS
Версия образа traefik:v3.0.0-beta2 выбрана не случайно, она поддерживает различные доменные имена для маршрутизации к контейнерам PostgreSQL. В примере выше использование beta2 не обязательно, так как любой запрос на порт 5432 будет проксироваться на единственный контейнер с PostgreSQL.
Когда postgres контейнеров несколько
Для работы с несколькими PostgreSQL-контейнерами и маршрутизации к ним на основе доменных имен требуется создать самоподписанный Wildcard-сертификат локального домена и добавить информацию о нем в конфиг Traefik.
Это делается только для того, чтобы к PostgreSQL-контейнерам можно было обращаться «извне» для работы с базой напрямую. В случае, если контейнеры работают в одной Docker-сети, Traefik не нужен.
Ниже приложили наш GitLab CI файл, в нем мы можем видеть ранее озвученные make-команды
yaml
variables:
APP4_ENV: "gitlab"
default:
tags:
#gtilab runner tag
- dev-project-ex-1
stages:
- ci
- delivery
- build
- deploy
.before_script_template: &build_test-integration
before_script:
- echo "Prepare job"
- sed -i "s!env=local!env=${APP4_ENV}!" ./Makefile
- make cp-env
- make cp-yml
- make up
.verify-code: &config_template
stage: ci
<<: *build_test-integration
only:
refs:
- merge_requests
- develop
- master
Linter:
<<: *config_template
script:
- make build
- make linter
Tests:
<<: *config_template
script:
- make tests
Delivery:
stage: delivery
script:
- echo "Rsync from $CI_PROJECT_DIR"
- sudo rm -rf "/home/project-ex/stands/dev/project-ex/!\(static|node_modules\)"
- sed -i "s!env=local!env=dev!" ./Makefile
- rsync -av --delete-before --no-perms --no-owner --no-group
--exclude "node_modules/"
--exclude "__pycache__/"
--exclude "logs/"
--exclude "docker-compose/docker_data/clickhouse/data/"
$CI_PROJECT_DIR/ /home/project-ex/stands/dev/project-ex
only:
- develop
except:
- master
Build:
stage: build
script:
- echo "cd /home/project-ex/stands/dev/project-ex"
- cd /home/project-ex/stands/dev/project-ex
- echo "make cp-env"
- make cp-env
- echo "cp-yml"
- make cp-yml
- echo "build"
- make build
only:
- develop
except:
- master
Build-front:
stage: build
script:
- echo "cd /home/project-ex/stands/dev/project-ex"
- cd /home/project-ex/stands/dev/project-ex
- echo "build-front"
- make build-front
only:
changes:
- '*.js'
- '*.css'
- '*.less'
refs:
- develop
- master
Deploy:
stage: deploy
script:
- cd /home/project-ex/stands/dev/project-ex
- mkdir -p logs
- make restart
- make migrate
- make collect-static
only:
- develop
except:
- master
Плюсы и минусы Docker
Docker использует ядро операционной системы хоста. Это вводит ряд ограничений: инструмент работает только на 64-битной установке Linux с ядром версии 3.10 или старше. Также он рассчитан на серверные приложения, и работу с графическими интерфейсами не всегда поддерживает.
Вдовесок, Docker требует от разработчика четкости и аккуратности. Неправильная конфигурация контейнера или недостаточные меры безопасности могут поставить всю систему под угрозу.
Как понимаете, у инструмента тоже есть свои недостатки. Но преимуществ несомненно больше: Docker использует общие ядра операционных систем и изолирует приложения с использованием namespaces и cgroups — нет необходимости под каждую задачу запускать отдельную виртуальную машину. Также он оптимизирует распределение ресурсов по контейнерам, способен управлять жизненным циклом приложения — запускать, останавливать, масштабировать и обновлять контейнеры. И самый большой плюс — экосистема и живое комьюнити. В Docker Hub лежит сотни готовых образов, а в сообществе можно задать вопрос или найти готовое решение.
Главное, помните: монолитная архитектура не означает «плохой», «устаревший» или «не модный» подход. Проектировать приложение стоит исходя из здравого смысла и оценки в том числе собственных возможностей. Потому что внедрение микросервисов с CI/CD требует больше навыков и компетенций, чем автоматизация монолитного приложения.