Погружение в мир контейнеризации с докером — это путь к оптимизации развёртыванию приложений, а также ключ к упрощению жизни разработчиков и системных администраторов. Меня зовут Андрей Аверков, в IT c 2008 начинал пусть с аналитика-проектировщика IT систем, 11 лет в роли разработчика и последние годы на руководящих должностях. Сейчас я тимлид команды разработки из 9 человек в группе компании Кокос. Мы занимаемся созданием и поддержкой CPA платформ (gdeslon.ru, fxpartners.ru, ads.mobisharks.com), а также проектом по генерации лендингов - lpgenerator.ru. Отдел разработки тесно сотрудничает с отделом IT. И эту статью мне помогал писать @Egorov_Ilja, он руководит IT-отделом в группе компаний Кокос Групп с 2013 года, а вообще в компании уже более 16 лет. В общем, у нас довольно большой опыт в разделении продуктов на части, поэтому, сегодня мы собрали самое основное и необходимое для работы с Docker. В нашей шпаргалке вы найдете все необходимое для успешного старта с докером: от базовых концепций и установки до продвинутых техник работы с контейнерами.

Что такое контейнеры и Docker

Перед тем, как рассказывать о Docker, погрузимся немного в теорию, что такое контейнеры и зачем они нужны. Если вы в курсе, можете переходить сразу ко второй части (чуть ниже), тут будет только база. И сразу простите, тут мало картинок и много кода.

Контейнеры представляют собой метод стандартизации развертывания приложений и их отделения от общей инфраструктуры. Экземпляр приложения запускается в изолированной среде, не влияющей на основную операционную систему.

Для разработчиков это означает, что им не нужно беспокоиться о том, в каком окружении будет функционировать их приложение, наличии необходимых настроек и зависимостей. Они могут просто создать приложение, упаковать все зависимости и настройки в единый образ. Этот образ легко запускать на различных системах, без опасений, что приложение не сможет стартовать.

Docker является платформой для разработки, доставки и запуска контейнерных приложений. Docker позволяет создавать контейнеры, автоматизировать их запуск и развертывание, а также управлять их жизненным циклом. Эта платформа обеспечивает возможность запуска множества контейнеров на одной хост-машине, что обеспечивает высокую степень гибкости и масштабируемости при разработке, тестировании и внедрении приложений.

Docker Daemon представляет собой сервер, функционирующий в фоновом режиме. Он прослушивает запросы от Docker CLI и управляет жизненным циклом контейнеров. Этот сервер отвечает за выполнение различных задач, включая запуск, остановку контейнеров, а также регулирование сетей и портов, а также ведение логов контейнеров.

Docker CLI представляет интерфейс командной строки Docker. Через команды, введенные в командной строке, пользователь указывает Docker Daemon выполнить конкретные действия, такие как поднятие, запуск или остановка контейнеров.

Важно отметить, что Docker CLI можно устанавливать как на локальной системе, так и настраивать для удаленного доступа, взаимодействуя с Docker Daemon посредством REST API. Этот механизм обеспечивает гибкость в управлении Docker из различных окружений.

Функциональность Docker Daemon расширяется за пределы простого управления жизненным циклом контейнеров. Эта подсистема также регулирует сетевые настройки, порты и ведение логов контейнеров. Ниже перечислены наиболее часто используемые команды для взаимодействия с Docker Daemon, которые включают в себя различные аспекты управления контейнерами и системными ресурсами.

Шпаргалка по Docker CLI

Основные команды

docker run <имя_образа> # запуск контейнера на основе указанного образа
docker ps # показать список активных контейнеров
docker ps -a # показать все контейнеры, включая остановленные
docker stop <идентификатор_контейнера> # остановить контейнер
docker rm <идентификатор_контейнера> # удалить контейнер
docker images # показать список всех локальных образов
docker pull <имя_образа> # загрузить образ из Docker Hub
docker rmi <идентификатор_образа> # удалить локальный образ

Создание и работа с образами

docker build -t <имя_образа>:<тег> <путь_к_Dockerfile> # сборка образа на основе Dockerfile
docker tag <старый_тег> <новый_тег> # пометить образ новым тегом
docker tag <имя_образа>:<старый_тег> <новый_репозиторий>/<новый_тег> # переименовать и пометить образ для загрузки в другой репозиторий
docker push <имя_репозитория>/<имя_образа>:<тег> # отправка образа в Docker Hub или другой реестр

Сети и порты

docker network ls # показать список сетей
docker run -p <локальный_порт>:<контейнерный_порт> <имя_образа> # определить соответствие портов при запуске контейнера

Работа с Docker Compose

docker-compose up # запустить сервисы, определенные в файле docker-compose.yml
docker-compose downмостановить и удалить сервисы, описанные в файле docker-compose.yml

Работа с Docker Volumes

docker volume create <имя_volume> # создать Docker Volume
docker run -v <имя_volume>:<путь_в_контейнере> <имя_образа> # запустить контейнер, подключив Volume

Логирование и Мониторинг

docker logs <идентификатор_контейнера> # показать логи контейнера
docker stats <идентификатор_контейнера> # отобразить статистику использования ресурсов контейнера

Dockerfile

Команды по созданию образа фиксируются в необработанном (raw) текстовом документе — Docker-файле:

ИНСТРУКЦИЯ аргумент(s)

где <ИНСТРУКЦИЯ> — команда для Docker Daemon, а <аргумент> — сам аргумент или конкретные значения, которые передаются в <ИНСТРУКЦИЮ>.

NB: инструкции нечувствительны регистру, однако их принято писать «капсом», чтобы визуально отличать от аргументов.

Инструкции объясняют, что 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 создать образ и запустить контейнер, нужно:: 

  1. Перейти в директорию, где лежит dockerfile.

  2. Командой docker build создать образ из файла.

  3. По необходимости, проверить образы командой docker images.

  4. Запустить контейнер из образа командой docker run.

В работе с образами можно использовать теги, чтобы указывать версию образов. По умолчанию, Docker присваивает тег <latest> при сборке.

#пример сборки образа с явным указанием тега
docker build -t my-python-app:v1.0

Для отправки образа в реестр 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 предоставляет средства для настройки взаимодействия контейнеров с внешней средой и другими контейнерами, а также управление потреблением ресурсов. Поэтому нет веских причин стараться уместить все компоненты в одном контейнере.

Дополнительные возможности Docker включают в себя способность настраивать взаимодействие между контейнерами и их внешней средой, а также регулирование потребления ресурсов. Если необходимо, чтобы контейнер работал с собственным экземпляром данных, не затрагивая оригинал, можно осуществить монтирование директории из хост-системы в сам контейнер. Этот процесс выполняется с использованием соответствующей команды Docker, что дает дополнительный уровень гибкости при работе с данными внутри контейнера.

docker run -v /путь/к/хост-директории:/путь/в/контейнере имя_образа

Docker Volumes — хранилища, которые связываются с контейнером, но не привязываются к его жизненному циклу. Это означает, что любые данные, которые контейнер отправляет в Volumes, сохранятся даже если контейнер остановить или уничтожить.

#команда для создания Volume в контейнере
docker run -v my_volume:/путь/в/контейнере имя_образа

Реестр Docker

Реестр Docker, или Docker Registry, представляет собой общедоступный репозиторий образов, который выполняет несколько важных функций. Этот сервис позволяет:

  • Централизованное хранение образов и их версий: Docker Registry предоставляет единое место для хранения образов контейнеров и их различных версий, обеспечивая удобный доступ к ним.

  • Ускорение развертывания: Образы скачиваются с реестра непосредственно на целевую систему, что ускоряет процесс развертывания и готовит контейнеры к оперативной работе.

  • Автоматизация процессов сборки, тестирования и развертывания контейнеров: Docker Registry интегрируется в процессы разработки, позволяя автоматизировать сборку, тестирование и развертывание контейнеров.

Docker Hub является публичным реестром, где хранятся общедоступные образы, такие как дистрибутивы Linux, базы данных, языки программирования и многое другое. Важно отметить, что организации могут создавать собственные приватные реестры Docker для хранения конфиденциальных данных, обеспечивая дополнительный уровень безопасности и управления доступом к образам контейнеров.

Чтобы передать переменные окружения в контейнер, используется флаг <-e> совместно с командой <RUN>:

docker run -e MY_VARIABLE=value имя_образа

Контейнер может экспортировать порты для взаимодействия с «внешним миром». Это особенно актуально для веб-приложений, где порты могут использоваться для доступа к веб-серверу.

docker run -p 8080:80 имя_образа

Можно ввести ограничения на ресурсы, используемые контейнером, такие как объем оперативной памяти или количество ядер CPU.

docker run --memory 512m --cpus 0.5 имя_образа

Создание приватного реестра 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 монолитного приложения, который мы намеренно сократили:

Установка пакетов:

sudo apt-get install -y build-essential libssl-dev zlib1g-dev libbz2-dev \
       libreadline-dev libsqlite3-dev wget curl llvm libncurses5-dev libncursesw5-dev \
       xz-utils tk-dev libffi-dev liblzma-dev python-openssl git npm redis-server vim ffmpeg

Установка pyenv:

$ curl https://pyenv.run | bash
$ echo 'export PATH="$HOME/.pyenv/bin:$PATH"' >> ~/.bashrc && \
echo 'eval "$(pyenv init -)"' >> ~/.bashrc && \
echo 'eval "$(pyenv virtualenv-init -)"' >> ~/.bashrc && source ~/.bashrc

Установки Python 3.6.9:

pyenv install 3.6.9

Если установлена версия Ubuntu 20+ и возникла ошибка при попытке установки, применить следующие команды:

$ sudo apt install clang -y
$ CC=clang pyenv install 3.6.9

Создание виртуального окружения:

$ pyenv virtualenv 3.6.9 cpa-project

Активация виртуального окружения:

$ pyenv activate cpa-project

Установка NodeJS 8.11.3:

$ npm i n -g
$ sudo n install 8.11.3
$ sudo n # в появившемся окне выбираем версию 8.11.3

Клонирование проекта

   $ git clone git@github.com:User/cpa-project.git

Переходим в проект:

$ cd cpa-project

Установка зависимостей python (убедитесь, что виртуальное окружение активно):

$ pip install -U pip
$ pip install -r requirements.txt

Установка зависимостей NodeJS:

$ npm install

Проводим миграции для базы данных и создаем тестовые данные:

$ python manage.py migrate
$ python manage.py generate_test_data

Сборка клиентской части

$ npm run watch # Для разработки с автоматическим ребилдом
$ npm run build # Для продакшена

Как видите, для запуска монолита на сервере прописываются и выполняются множества команд, а процесс требует участия человека.

Собираем тот же проект в Docker

Давайте посмотрим, как это же приложение можно развернуть в среде Docker. Для начала выделим сервисы для compose-файла и придумаем им названия:

Compose-файл
yaml
version: '3'
services:
 {stage}-project-ex--app:
   container_name: {stage}-project-ex--app
   build:
     context: ..
     dockerfile: Dockerfile
   env_file:
     - ".env.{stage}"
   networks:
     - stage_project-ex_network
   depends_on:
     - {stage}-project-ex--redis
     - {stage}-project-ex--clickhouse
     - {stage}-project-ex--postgres
     - {stage}-project-ex--mailhog
   volumes:
     - ..:/app/
     - ./crontab.docker:/etc/cron.d/crontab.docker
   command: /start
   labels:
     - "traefik.enable=true"
     - "traefik.http.routers.{stage}_fp_app.rule=Host(`web.{stage}.project-ex.io`)"
     - "traefik.http.services.{stage}_fp_app.loadbalancer.server.port=8000"
     - "traefik.http.routers.{stage}_fp_app.entrypoints=websecure"
     - "traefik.http.routers.{stage}_fp_app.tls.certresolver=stage_project-ex_app"

 {stage}-project-ex--app-cron:
   container_name: {stage}-project-ex--app-cron
   build:
     context: ..
     dockerfile: Dockerfile
   env_file:
     - ".env.{stage}"
   networks:
     - stage_project-ex_network
   depends_on:
     - {stage}-project-ex--redis
     - {stage}-project-ex--clickhouse
     - {stage}-project-ex--postgres
     - {stage}-project-ex--mailhog
   volumes:
     - ..:/app/
     - ./crontab.docker:/etc/cron.d/crontab.docker
   command: sh -c "printenv >> /etc/environment && crontab /etc/cron.d/crontab.docker && cron -f"

 {stage}-project-ex--front:
   container_name: {stage}-project-ex--front
   build: ./frontend-builder
   env_file:
     - ".env.{stage}"
   networks:
     - stage_project-ex_network
   depends_on:
     - {stage}-project-ex--app
   volumes:
     - ..:/app/

 {stage}-project-ex--clickhouse:
   container_name: {stage}-project-ex--clickhouse
   image: yandex/clickhouse-server:20.4.6.53
   env_file:
     - ".env.{stage}"
   networks:
     - stage_project-ex_network
   volumes:
     - /home/project-ex/stands/{stage}/docker_data/clickhouse/data:/var/lib/clickhouse
     - ./docker_data/clickhouse/schema:/var/lib/clickhouse/schema
     - ./docker_data/clickhouse/users.xml:/etc/clickhouse-server/users.xml
     - ./docker_data/clickhouse/project-ex.xml:/etc/clickhouse-server/users.d/default-user.xml
   labels:
     - "traefik.enable=true"
     - "traefik.tcp.routers.{stage}_fp_clickhouse.rule=HostSNI(`*`)"
     - "traefik.tcp.routers.{stage}_fp_clickhouse.entryPoints=clickhouse"
     - "traefik.tcp.routers.{stage}_fp_clickhouse.service={stage}_fp_clickhouse"
     - "traefik.tcp.services.{stage}_fp_clickhouse.loadbalancer.server.port=8123"

 {stage}-project-ex--postgres:
   container_name: {stage}-project-ex--postgres
   image: postgres:13.11-alpine
   env_file:
     - ".env.{stage}"
   networks:
     - stage_project-ex_network
   stdin_open: true
   tty: true
   volumes:
     - {stage}-project-ex--postgres:/var/lib/postgresql
   labels:
     - "traefik.enable=true"
     - "traefik.tcp.routers.postgres.rule=HostSNI(`*`)"
     - "traefik.tcp.routers.postgres.entryPoints=postgres"
     - "traefik.tcp.routers.postgres.service=postgres"
     - "traefik.tcp.services.postgres.loadbalancer.server.port=5432"

 {stage}-project-ex--redis:
   container_name: {stage}-project-ex--redis
   image: redis:alpine
   env_file:
     - ".env.{stage}"
   networks:
     - stage_project-ex_network
   volumes:
     - {stage}-project-ex--redis:/data

 {stage}-project-ex--mailhog:
   container_name: {stage}-project-ex--mailhog
   image: mailhog/mailhog:v1.0.1
   env_file:
     - ".env.{stage}"
   networks:
     - stage_project-ex_network
   labels:
     - "traefik.enable=true"
     - "traefik.http.routers.{stage}_fp_mailhog.rule=Host(`mail.{stage}.project-ex.io`)"
     - "traefik.http.services.{stage}_fp_mailhog.loadbalancer.server.port=8025"
     - "traefik.http.routers.{stage}_fp_mailhog.entrypoints=websecure"
     - "traefik.http.routers.{stage}_fp_mailhog.tls.certresolver=stage_project-ex_app"

volumes:
 {stage}-project-ex--postgres:
   name: {stage}-project-ex--postgres
   driver: local
 {stage}-project-ex--redis:
   name: {stage}-project-ex--project-ex
   driver: local

networks:
 stage_project-ex_network:
   external: true
   name: stage_project-ex_network

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
dir=${CURDIR}
project=project-ex
env=local
interactive:=$(shell [ -t 0 ] && echo 1)
ifneq ($(interactive),1)
  optionT=-T
endif

uid=$(shell id -u)
gid=$(shell id -g)
# Команда для docker-compose exec
c=
# Параметр для docker-compose exec
p=

dc:
  @docker-compose -f ./docker-compose/$(env).yml --env-file=./docker-compose/.env.$(env) $(cmd)

compose-logs:
  @make dc cmd="logs" env="$(env)"

cp-env:
  [ -f ./docker-compose/.env.$(env) ] && echo ".env.$(env) file exists" || cp ./docker-compose/.env.example ./docker-compose/.env.$(env)
  sed -i "s/{stage}/$(env)/g" ./docker-compose/.env.$(env)
  @if [ "$(env)" = "local" ] ; then \
     sed -i "s/{domain}/ma.local/g" ./docker-compose/.env.$(env) ; \
  fi;
  @if [ "$(env)" = "dev" ] ; then \
     sed -i "s/{domain}/dev.project-ex.io/g" ./docker-compose/.env.$(env) ; \
  fi;

cp-yml:
  @if [ ! "$(env)" = "local" ] ; then \
     [ -f ./docker-compose/$(env).yml ] && echo "$(env).yml file exists" || cp ./docker-compose/stage.example.yml ./docker-compose/$(env).yml ; \
     sed -i "s/{stage}/$(env)/g" ./docker-compose/$(env).yml; \
  fi;

init:
  docker network ls | grep stage_project-ex_network > /dev/null || docker network create stage_project-ex_network
  @make cp-env
  @make cp-yml
  [ -f ./docker-compose/.env.$(env) ] && echo ".env.$(env) file exists" || cp ./docker-compose/.env.$(env).example ./docker-compose/.env.$(env)
  @make dc cmd="up -d"
  @make dc cmd="start $(env)-$(project)--postgres" env="$(env)"
  sleep 5 && cat ./docker-compose/docker_data/pgsql/data/init_dump.sql | docker exec -i $(env)-$(project)--postgres psql -U project-ex
  @make dc cmd="exec $(env)-$(project)--app python ./manage.py migrate" env="$(env)"
  @make ch-restore env="$(env)"
  @make build-front env="$(env)"
  @make collect-static env="$(env)"

create_test_db:
  @make dc cmd="exec $(env)-$(project)--postgres dropdb --if-exists -U project-ex project-ex_test" env="$(env)" > /dev/null
  @make dc cmd="exec $(env)-$(project)--postgres createdb -U project-ex project-ex_test" env="$(env)"
  cat ./docker-compose/docker_data/pgsql/data/init_dump.sql | docker exec -i $(env)-$(project)--postgres psql -U project-ex project-ex_test

bash-front:
  @make dc cmd="exec $(env)-$(project)--front sh" env="$(env)"

<make> создает своего рода короткий алиас команд для управления сервисами. В нём можно инициализировать проект, пересоздать базу, собрать фронт и так далее. Эти же команды мы будем использовать в GitLab CI файле. Далее мы запускаем команду <make init> для инициализации проекта.


Переходим к другому compose файлу вспомогательного сервиса. Данному сервису не требуется частая перезагрузка, его достаточно настроить «один» раз, а далее он сам следит за всеми контейнерами Docker-демона.

Compose-файл
yaml
version: '3'
services:
 stage-project-ex--traefik:
   image: "traefik:v3.0.0-beta2"
   container_name: "stage-project-ex--traefik"
   command:
     - "--log.level=DEBUG"
     - "--providers.docker=true"
     - "--providers.docker.exposedbydefault=false"
     - "--entrypoints.web.address=:80"
     - "--entrypoints.web.http.redirections.entryPoint.to=websecure"
     - "--entrypoints.websecure.address=:443"
     - "--entrypoints.postgres.address=:5432"
     - "--entrypoints.clickhouse.address=:8123"
     - "--entrypoints.mongo.address=:27017"
     - "--certificatesresolvers.stage_project-ex_app.acme.httpchallenge=true"
     - "--certificatesresolvers.stage_project-ex_app.acme.httpchallenge.entrypoint=web"
     - "--certificatesresolvers.stage_project-ex_app.acme.email=it@email-ex.com"
     - "--certificatesresolvers.stage_project-ex_app.acme.storage=/letsencrypt/acme.json"
   restart: always
   ports:
     - 80:80
     - 443:443
     - 5432:5432
     - 8123:8123
     - 27017:27017
   networks:
     - stage_project-ex_network
   volumes:
     - "/opt/letsencrypt:/letsencrypt"
     - "/var/run/docker.sock:/var/run/docker.sock:ro"

networks:
 stage_project-ex_network:
   external: true
   name: stage_project-ex_network

Настройка портов

В первом compose-файле ничего не сказано про порты. Основная причина в том, что обращаться к проекту мы будем по доменному имени с помощью Traefik. Приложения работает отдельно от compose-файлов и версий проекта: о появлении новых контейнеров Traefik узнаёт от Docker Daemon, а конфигурация для приложения прописывается в compose-файле после ключевого слова <labels>.

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 становится излишним.

В <compose traefik> добавляем:

yaml
    command:
      - "--providers.file.filename=/conf/dynamic-conf.yml"
    volumes:
      - "./tls:/tls"
      - "./conf:/conf"

В conf/dynamic-conf.yml прописываем файлы сертификатов:

yaml
tls:
  certificates:
    - certFile: /tls/something.com.pem
      keyFile: /tls/something.com.key

В директорию tls/ кладём файлы Wildcard-сертификата, созданные Bash-скриптом  <mkcert.sh something.com>:

bash
#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'

 DOMAIN_NAME=$1

if [ ! -f $1.key ]; then

  if [ -n "$1" ]; then
    echo "You supplied domain $1"
    SAN_LIST="[SAN]\nsubjectAltName=DNS:localhost, DNS:*.localhost, DNS:*.$DOMAIN_NAME, DNS:$DOMAIN_NAME"
    printf $SAN_LIST
  else
    echo "No additional domains will be added to cert"
    SAN_LIST="[SAN]\nsubjectAltName=DNS:localhost, DNS:*.localhost"
    printf $SAN_LIST
  fi

  openssl req \
    -newkey rsa:2048 \
    -x509 \
    -nodes \
    -keyout "$1.key" \
    -new \
    -out "$1.crt" \
    -subj "/CN=compose-dev-tls Self-Signed" \
    -reqexts SAN \
    -extensions SAN \
    -config <(cat /etc/ssl/openssl.cnf <(printf $SAN_LIST)) \
    -sha256 \
    -days 3650

  echo "new TLS self-signed certificate created"

else

  echo "certificate files already exist. Skipping"

fi

Правим <labels> контейнера postgres в compose-файле:

yaml
    labels:
      - "traefik.enable=true"
      - "traefik.tcp.routers.qa222_postgres.rule=HostSNI(`qa222.something.com`)"
      - "traefik.tcp.routers.qa222_postgres.entryPoints=postgres"
      - "traefik.tcp.routers.qa222_postgres.service=qa222_postgres"
      - "traefik.tcp.services.qa222_postgres.loadbalancer.server.port=5432"
      - "traefik.tcp.routers.qa222_postgres.tls=true"

CI/CD проекта

Ниже приложили наш GitLab CI файл, в нем мы можем видеть ранее озвученные <make>-команды. Для того, чтобы развернуть проект на сервере, был настроен ci/cd, который позволяет:

  • проверить качество кода;

  • запустить тесты;

  • собрать билды образов докера;

  • доставить всё это до сервера.

Данный 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 ощутимо перевешивают. Использование общих ядер операционных систем и изоляция приложений через namespaces и cgroups устраняет необходимость в запуске отдельных виртуальных машин для каждой задачи. Docker также оптимизирует распределение ресурсов между контейнерами и обеспечивает управление жизненным циклом приложения, включая запуск, остановку, масштабирование и обновление контейнеров. Кроме того, существует развитая экосистема Docker Hub, где хранятся сотни готовых образов, и активное сообщество, где можно обсудить вопросы или найти решения.

Важно помнить, что, несмотря на преимущества микросервисной архитектуры, монолитный подход также имеет свое место и может быть целесообразным, особенно в зависимости от требований проекта и компетенций команды разработчиков.

Комментарии (11)


  1. MrAlone
    22.03.2024 07:22
    +3

    Как это оказалось здесь?

    1) Кто-то в 2024 году не знает про Докер?
    2) Это называется "распилить монолит на части"? А где тут монолит, и каким магическим образом он был "распилен"?

    > Как видите, для запуска монолита на сервере прописываются и выполняются множества команд, а процесс требует участия человека.

    А потом мы эти команды поместили в докерфайл, и участия человека не нужно. Гениально!


    1. averkov Автор
      22.03.2024 07:22

      Возможно, кто-то еще не знает. Мы поделились нашим опытом переноса существующего проекта в докер. Рассказали как это происходит и оформили в шпаргалку (вдруг кому-то понадобиться). Задача была перенести зависимости с сервера в докер, чтобы проект мог развиваться и обновляться командой разработки


      1. MrAlone
        22.03.2024 07:22
        +3

        Да, но это не про то, как распилить монолит. Как перенести в Докер - ок. Как распилить на части монолит - не ок.
        Кликбейтный заголовок, который не соответствует действительности.


  1. artemaa
    22.03.2024 07:22
    +2

    Вы меня конечно извините за душнильство, но в docker уже довольно давно завезли docker compose, вместо docker-compose


    1. averkov Автор
      22.03.2024 07:22

      Извиняю )


  1. neiroman2k
    22.03.2024 07:22

    Для чего в первом compose имена сервисов со stage в фигурных скобках ?


    1. neiroman2k
      22.03.2024 07:22

      Ааа, оно дальше в make файл заменяется на тип окружения. Для себя принял решение не использовать "-" в именах сервисов, тк потгм невозможно нормально распарсить выдачу через docker inspect --format ..ибо ругается, что недопустимый символ. На _ норм


      1. averkov Автор
        22.03.2024 07:22

        Мы тоже уже переделали на _


  1. akireyev
    22.03.2024 07:22
    +1

    Интересно было бы почитать про то как проект на, скажем, LifeRay, содержащий полсотни портлетов перенести на докер, а вот эту кликбейтную простыню.


  1. WebMonet
    22.03.2024 07:22

    Для меня больше всего проблем с докером возникает при настройке прав на доступ к файлам. Созданное от рута в контейнере не удаляется снаружи и т.д.

    Как правильно это сделать? Есть-ли какой-то универсальный способ?


    1. averkov Автор
      22.03.2024 07:22

      Нужно прокинуть нужный UID и GID в контейнер