{{countBasket}}
От экспертов «1С‑Рарус»: Подробное руководство по контейнеризации в Linux и Windows. Docker для 1С. Часть 1
От экспертов «1С‑Рарус»: Подробное руководство по контейнеризации в Linux и Windows. Docker для 1С. Часть 1

От экспертов «1С‑Рарус»: Подробное руководство по контейнеризации в Linux и Windows. Docker для 1С. Часть 1

31.10.2025
171 мин
9430

Оглавление

  1. Введение
  2. Глава первая, в которой мы знакомимся с ядром Linux и его функциями
  3. Глава вторая, в которой ядро Linux приобрело возможность контейнеризации
  4. Глава третья, в которой мы знакомимся с Docker и его вкусностями
  5. Глава четвертая, в которой Docker пытается подружиться с Windows
  6. Глава пятая, в которой мы пытаемся изготовить Docker-образ по файлу с рецептами
  7. Глава шестая, в которой мы запускаем наш первый контейнер
  8. Глава седьмая, в которой мы собираем образ для сервиса СЛК (SLC)
  9. Глава восьмая, в которой мы строим площадку для запуска контейнеров
  10. Заключение

Введение

Здравствуйте, друзья. В этой статье мы погрузимся в мир Linux и контейнеризации. Рассмотрим основные моменты архитектуры контейнеризации, сравним контейнеризацию на Linux и на Windows. Познакомимся со сборкой docker-образов и с запуском контейнеров с помощью docker compose.

Глава первая, в которой мы знакомимся с ядром Linux и его функциями

И начнем наше повествование с описания устройства ядра Linux. Оно потому и называется ядром, что является монолитной программой и живет, по большей части, в одном файле. Размер ядра в сжатом виде составляет всего 3 МБ, что совсем не говорит о его неважности.

Ядро — это самая важная программа в системе, управляющая взаимодействием между пользовательскими приложениями и аппаратной частью компьютера. Ядро никогда не отдыхает... На верхнем уровне ядро обрабатывает поступающие системные вызовы от пользовательских приложений. На нижнем уровне ядро обрабатывает аппаратные прерывания, сигналы, поступающие от периферии, процессора, памяти, различных устройств. С помощью драйверов ядро преобразует команды приложений в понятный для устройств машинной код. И обратно преобразует сигналы с устройств в понятные для приложений команды.

Управление очередью процессов, распределение памяти, работа с файловой системой, работа с сетевым стеком — все это функции ядра, с которыми оно очень умело справляется.

Ядро Linux было сконструировано в 1991, опираясь на принципы и стандарты работы операционных систем Unix, зародившихся еще в 1970-х годах.

Основные черты Unix архитектуры:

  • Архитектура системы должна быть многоуровневая. Это значит, что функциональные компоненты разделены на несколько уровней, что обеспечивает стабильность и расширяемость:
    • Первый уровень — это уровень ядра и драйверов, выполняющих работу непосредственно с аппаратной частью.
    • Второй уровень — это уровень работы системных утилит и фоновых приложений (демонов) с правами администратора (root-правами).
    • Третий уровень — это уровень работы пользовательских приложений с ограниченными правами пользователя.

      Ядро Linux

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

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

Ядро Linux. Модули.

Модульной архитектуре соответствуют и пользовательские приложения. Кроме этого, большое влияние на способ реализации приложений оказала философия Unix, сводящаяся к трём основным пунктам:

  • Пишите программы, которые делают что-то одно и делают это хорошо.

    В связи с этим в Linux огромное число утилит, отвечающие за разные аспекты функционирования системы:

    • ls — выводит информацию о файлах текущего каталога;
    • df — выводит информацию о занимаем месте на диске;
    • du — выводит информацию о размере файлов;
    • cat — выводит содержание файлов;
    • и т. д.
  • Пишите программы, которые бы поддерживали текстовые потоки, поскольку это универсальный интерфейс.

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

  • Пишите программы, которые работали бы вместе.

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

    cat rphost*/*.log | grep .*,EXCP,.*mdmp | awk -F ',' '{count[$12 " " $15]+=1} END {for (i in count) {print count[i] " " i}}'

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

Глава вторая, в которой ядро Linux приобрело возможность контейнеризации

Разделение доступа к файлам с помощью приложения chroot

Unix-подобные операционные системы придерживаются идеи «Всё есть файл». Поэтому в файлы отображается любой компонент операционной системы: устройства, процессы, директории, сетевые сокеты, каналы. И поэтому файловое пространство является одним из важнейших узлов функционирования операционной системы, а не просто местом, где хранится информация.

Структура файловой системы (ФС) соответствует стандарту иерархии файловой системы FHS (Filesystem Hierarchy Standard). Начальным узлом ФС является корень «/», относительно которого возникает иерархия системных директорий, где располагаются файлы системных и пользовательских приложений.

Файловая структура

Согласно критериям архитектуры, Unix приложение является модульным и хранит информацию о себе и своем функционировании не в одной какой-то директории (как в Windows), а распределяет ее по разным каталогам, в зависимости от функциональности той или иной части приложения.

И тут возникает вопрос, а что будет, если одно приложение повредит данные другого приложения? Ответ очевиден, это может привести к сбою функционирования приложений в системе. Поэтому еще до рождения Linux в средах Unix прорабатывалась технология разделения приложений таким образом, чтобы одно из них не могло повлиять на ход выполнения другого, тем самым обезопасив критично важные приложения от новых, еще до конца не проверенных.

Именно для этого в 1982 году была разработана утилита chroot, которая помогала запускать приложение в файловой системе, изолированной от основной файловой системы, со своей корневой директорией «/». Процессу выделялось свое адресное пространство в памяти и свои файловые дескрипторы. И это стало первым кирпичиком в технологии контейнеризации.

Файловая структура контейнера

Разделение доступа к ресурсам namespaces

Шло время, технологии развивались. Изоляция процессов с помощью chroot проработала 15 лет, однако Unix по-прежнему имел единое пространство процессов для всех пользовательских приложений, единый сетевой стек. Различные приложения могли конфликтовать между собой за общие ресурсы, что не могло не приводить к сбоям в ОС с несколькими пользователями.

И с появлением новых более мощных вычислительных систем задача организации эффективного контроля со стороны ядра над процессами по использованию аппаратных ресурсов становилась как никогда актуальной.

На протяжении 2000-х в разных операционных системах различными компаниями предлагались различные способы для управления и организации доступа к ресурсам процессов. В итоге к 2008 году в ядре Linux окончательно оформился стек технологий по управлению ресурсами.

Первая технология называется Namespaces — пространство имен. Это абстракция, дополнительная программная оболочка, которая стала посредником между ядром, процессами и физическими ресурсами. Благодаря чему и стала возможность изоляции процессов.

Существуют следующие пространства имён:

  • Mount — пространство имён файловой системы, пришедшее на замену chroot. Это независимое дерево файловой системы, ассоциированное с определённой группой процессов. Каждый Mount предоставляет уникальный вид файловой системы для всех процессов, принадлежащих этому пространству. При создании новых пространств можно установить флаг для копирования списка точек монтирования из родительского пространства в дочернее. Для разных дочерних пространств можно организовать доступ к одним и тем же точкам монтирования.

    Mount — пространство имён файловой системы

  • Network — пространство имён для сетей.

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

  • IPC — пространство имён для межпроцессных взаимодействий.

    IPC включает в себя семафоры, разделяемую память и очереди сообщений. Соответственно, процессы из разных пространств имен не могут влиять друг на друга.

  • PID — пространство имён для номеров процессов.

    Подобно иерархической структуре каталогов файловой системы процессы также выстраиваются в дерево. При старте операционной системы сначала запускается процесс с идентификационным номером (PID) 1. В дереве процессов он является корневым. Он, в свою очередь, запускает другие процессы и службы.

    Механизм namespaces позволяет создавать отдельное ответвление дерева процессов с собственным корневым PID 1. Процесс, создающий такое ответвление, являются частью основного дерева, но его дочерний процесс уже будет корневым в новом дереве. Процессы в новом дереве никак не взаимодействуют с родительским процессом и даже не «видят» его. В то же время процессам в основном дереве доступны все процессы дочернего дерева.

    Процессы контейнера

  • User — пространство имён для пользователей.

    Все процессы в операционной системе принадлежат какому-либо пользователю. Существуют привилегированные и непривилегированные процессы, что определяется их пользовательским ID (UID). Пользовательские пространства имен изолируют связанные с безопасностью идентификаторы и атрибуты. В частности, ID пользователей, групповые ID, корневой каталог, ключи и возможности. Процесс может иметь обычный непривилегированный UID вне своего пространства имен и в то же время иметь UID=0 внутри него.

    Пользователи контейнера

  • UTS (Unix Time Sharing) — пространство имён для hostname и NIS (Network Information Services).

    UTS позволяет получать собственные доменные имена NIS и хостовые имена hostname.

В итоге технология Namespaces решает вопросы изоляции. Вопросы ограничения ресурсов для изолированных процессов решает второй механизм cgroups (control group).

Ограничения доступа к ресурсам cgroups

cgroups — это технология организации процессов в иерархическую группу, на которую накладывается изоляция через пространство имен и распределяются ограничения вычислительных ресурсов (процессорные, сетевые, ресурсы памяти, ресурсы ввода-вывода) со стороны ядра Linux.

Cgroups состоит из двух частей:

  1. cgroup core — ядро, которое монтируется в ядро ОС и в первую очередь отвечает за иерархическую организацию процессов.
  2. Контроллеры:
    • blkio — управляет лимитами чтения и записи на блочных устройствах.
    • cpu — управляет доступом к ресурсам процессора.
    • cpuset — выделяет определённые CPU (ЦПУ) иерархической группе процессов.
    • cpuacct — управляет ресурсами cpu (работает совместно с cpu).
    • devices — ограничивает доступ к устройствам.
    • hugetlb — управляет работой групп с большими страницами памяти (huge pages).
    • net_cls — размечает специальными тегами сетевые пакеты, что позволяет процессам, генерирующим их, определять какие процессы их сгенерировали.
    • perf_event rdma — предоставляет интерфейс для инструмента анализа производительности в Linux (perf).
    • freezer — управляет приостановкой и возобновлением процессов выполнения задач внутри контрольной группы.
    • memory — управляет выделением памяти для групп процессов.
    • net_prio — управляет динамическим назначением приоритета трафика.
    • pids — ограничивает количество процессов в рамках контрольной группы.
    • unified — автоматически монтирует файловую систему в каталог /sys/fs/cgroup/unified при запуске системы.

В итоге в 2008 году в ОС Linux с версией ядра 2.6.24 появилась первая полноценная контейнерная система LXC (Linux Containers). LXC опирается на cgroups и принцип изоляции пространства имен, обеспечивает работу контейнеров на уровне операционной системы, использует собственные процессорное и сетевое пространство без необходимости создания полноценной виртуальной машины.

Контейнеризация и виртуализация

С тех пор и по сей день контейнеры получили широкое распространение, так как обладают следующими важными преимуществами по сравнению с виртуальными машинами:

  • Легковесность и скорость запуска, так как управляющее ядро находится на хостовой машине.
  • Прозрачность контейнера со стороны хостовой машины, ограничение доступа к хостовой машине со стороны контейнера.
  • Низкий уровень потребления ресурсов системы контейнеризации.
  • Вложенная контейнеризация — внутри контейнеров можно создавать новые контейнеры.

Они применяются там, где нужна скорость и масштабируемость:

  • Развертывание микросервисов.
  • Тестирование и отладка.
  • Горизонтальное масштабирование приложений. Это когда запускается несколько одинаковых контейнеров на нескольких серверах, что повышает общую производительность приложения.

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

  • В LXC нельзя установить любую операционную систему. Операционная система ограничена версией ядра хоста контейнера. Установка контейнера с более новым ядром Linux скорее всего приведет к ошибкам при использование новой функциональности. Старое ядро Linux обычно обратно совместимо с новым ядром, но для стабильности на продуктивных системах лучше следить, чтобы версия ядра контейнера совпадала с версией ядра хоста.
  • В LXC нельзя самостоятельно установить драйвера. Т. е. драйвера должны быть установлены и на хосте, и в контейнере. Например:
    • Нельзя развернуть HASP службу без установки драйвера hasp на хосте.
    • Нельзя просто так настроить nfs сервер отдельно от хоста.
  • Доступ к некоторым устройствам требует режима повышения привилегий контейнера, что при неправильной настройке может привести к нарушению безопасности. Например, существует привилегированный режим контейнера, при переходе в который можно управлять хостом. Поэтому нужно понимать и следить за настройками доступа из контейнера. Так, для настройки LXC с сервисом OpenSearch необходимо задать настройку для контейнера:

    lxc.prlimit.memlock: unlimited

  • Также часть действий из контейнера контролируются профилями безопасности систем мандатного доступа типа AppArmor или SELinux. Нужно уметь их корректно настраивать для специфичной работы контейнера.
  • В контейнерах сложно организовать GUI (графический интерфейс пользователя).

В общем случае перечисленные ограничения не заметны при работе с LXC, но иногда возникают задачи, решение которых требует опыта и знаний.

Для преодоления некоторых ограничений контейнеров включается привилегированный режим. А для сохранения безопасности на помощь приходит обычная виртуализация. При виртуализации для операционной системы создается полная эмуляция вычислительного оборудования — так называемая виртуальная машина, в рамках которой и происходит развертывание необходимой среды. Хостовая машина полностью закрыта для виртуальной машины. Да, виртуальные машины не настолько быстрые, как контейнеры, их сложнее масштабировать, однако совместное использование виртуальных машин и контейнеров (т. е. запуск контейнеров внутри виртуальных машин) дает прекрасное компромиссное решение между безопасностью и масштабируемостью.

Гипервизор + контейнеры

Глава третья, в которой мы знакомимся с Docker и его вкусностями

Зачем нужен docker?

Технология контейнеризации отлично пришлась ко двору разработки программного обеспечения. С одной стороны, специалистам требовались быстро разворачиваемые песочницы для тестирования разрабатываемых приложений без риска навредить настроенной среде разработки. С другой стороны, инфраструктура, в которой работали приложения, усложнялась, и требовались инструменты быстрой настройки инфраструктуры, чтобы приложения после запуска работали так, как задумывалось. Проще говоря, стал востребован API для простого управления контейнерами, посредством встраивания его в код.


Такой API появился в 2013 году в инструменте Docker, что открыло новую страницу в организации разработки и поставки приложений. Теперь стала возможна поставка не просто приложения, а совокупности приложения и всех его зависимостей (библиотеки, конфигурации, файлов настроек, скриптов обслуживания) упакованных в единый образ, который можно разворачивать в любой операционной системе Linux без необходимости заново настраивать окружение.

Docker не просто предоставил команды работы с контейнерами, в рамках этой технологии были описаны новые принципы создания, распространения и запуска контейнеров. Давайте их рассмотрим.

Файловая система как слоеный торт

Вся концепция поставки приложений в docker основана на многоуровневой файловой системе OFS (Overlay File System).

Основной принцип работы такой ФС состоит в том, что она состоит из нескольких слоев. Нижние ее слои доступны только на чтение, а верхние слои доступны и на чтение, и на изменение. Эти слои объединяются в единую файловую систему, в которой может работать то или иное приложение. Причем в итоговой файловой системе можно менять файлы, возникшие в ней еще на нижнем уровне, но в любой момент времени снятие изменений верхнего уровня возвращает файлы нижнего уровня в первоначальное состояние.

При этом не стоит путать OFS и HFS. HFS — это принцип организации структуры директорий и файлов, создаваемых в файловой системе, а OFS — это то, как будет организовано хранение этих директорий и файлов на дисковом пространстве.

В итоге, зачем нужна OFS?

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

Docker нам предоставляет возможность описать слои файловой системы будущего контейнера:

  1. Первые слои, которые остаются неизменными на протяжение всей жизни контейнера и образуют образ контейнера.
  2. Вторые слои с томами, которые содержат изменяемые данные — эти данные критически важные, их нельзя потерять
  3. Третьи слои с файлами настроек и с секретами для приложения, которые могут накладываться как на файлы образа в момент запуска контейнера, так и на отдельные файлы тома. Они также остаются неизменными.
  4. Четвертый слой, который отражает контекст работы окружения, его текущее состояние, так как в Linux все есть файл. Данные этого слоя не страшно потерять.

Совокупность слоев даёт нам удобный инструмент управления инфраструктурой.

Таким образом docker разделил запуск контейнера на 5 основных этапов:

  1. Подготовка образа начального контейнера.
  2. Подготовка томов, где будет накапливаться важная информация запуска контейнера.
  3. Подготовка конфигурационных файлов и параметров запуска приложения внутри контейнера.
  4. Подготовка сети, определение портов для доступа к контейнеру.
  5. Сам запуск и работа контейнера.

Первый этап — упаковка приложения и его настроек в единый образ начального контейнера Docker

Что такое образ начального контейнера? Это подготовленная операционная система с установленными в ней приложениями. Данный пакет мы можем запустить в контейнере на любой машине Linux.
Для того, чтобы создать образ достаточно:

  • Установить, например, на виртуальной машине операционную систему.
  • Поставить туда необходимые приложения, задать необходимые настройки.
  • Упаковать данную систему в архив простой командой:

    tar --numeric-owner --exclude=/proc --exclude=/sys -cf os-base.tar /
  • Перенести полученный архив в настроенную операционную систему с docker.
  • И, абракадабра, мы получаем начальный образ контейнера с помощью команды:

    cat os-base.tar | docker import os:1.1

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

Однако, зачем все это делать руками и тратить кучу времени каждый раз? Эти действия нужно же еще как-то документировать... Создатели docker подумали точно так же и упростили данный процесс.

Все действия по выбору операционной системы, по установке и настройке приложений описывается в специальном файле dockerfile, о структуре которого мы расскажем ниже. Вместо архива с операционной системой мы переносим в нашу хостовую ОС только один файлик. По этому файлику docker поднимает технический контейнер, выполняющий процесс подготовки начального образа. Команда запуска сборки образа имеет довольно простой вид:

docker build -t ИмяОбраза:ВерсияОбраза

Второй этап — работа Docker с томами

Для того, чтобы критично важные данные, полученные в контейнере, не были уничтожены при удалении самого контейнера, docker предоставляет механизм volumes.

При выполнении команды:

docker volume create ИмяТома

создается директория по пути /var/lib/docker/volumes/ИмяТома на хостовой машине.

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

Таким образом docker volume позволяет:

  • Сохранять критически важные данные при удалении самого контейнера.
  • Обмениваться данными между несколькими контейнерами.
  • Архивировать данные, переносить их на другую машину.

Для удобной работы с томами существуют следующие команды:

  • docker volume ls — просмотр список томов;
  • docker volume inspect ИмяТома — просмотр параметров и пути к месту расположения тома;
  • docker volume rm ИмяТома — удаление тома;
  • docker volume prune — очистка неиспользуемых временных томов.

Стоит подчеркнуть, что для подключения томов docker предоставляет широкий набор настроек.

Можно указать протоколы подключения внешних томов:

  • nfs;
  • sftp;
  • samba.

Можно указать драйвера для подключения томов других файловых систем:

  • tmpfs;
  • btrfs;
  • zfs.

Обо всем этом подробно описано в официальной документации docker (docs.docker.com/engine/storage/). Настроек настолько много, и они настолько интересные, что их описание требует отдельной статьи.

Отдельно стоит упомянуть про альтернативу docker volumes — это точки монтирования.

Через точки монтирования связываются внешние каталоги на хостовой машине с внутренними путями контейнера.

Тома контейнера и точки монтирования имеют разную техническую реализацию. Если тома — это часть файловой системы контейнеров, то точки монтирования, это, по своей сути, внешние диски, добавленный в файловую систему по определенному пути. Соответственно и операции по управлению файлами в томах проходят быстрее, чем аналогичные операции для точек монтирования.

Третий этап — настройки приложения внутри контейнера Docker

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

К таким настройкам могут относиться:

  • конфигурационные файлы;
  • секреты;
  • сертификаты.

Для работы с данными настройками docker предлагает функционал: configs и secrets. С помощью данного функционала настройки и пароли описываются в файлах, и в момент запуска контейнера накладываются во внутренней файловой системе поверх файлов настроек приложения.

Более подробно о данном функционале мы расскажем ниже.

Четвертый этап — настройка сети

Все запускаемы docker-контейнеры определяют свою внутреннюю подсеть. С хостовой сетью данные подсети по умолчанию соединяются через сетевой мост.

Сеть docker

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

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

Тем не менее, можно создавать и свои сетевые интерфейсы для более гибкой настройки сети. Docker предоставляет следующие виды сетевых драйверов:

  • Мост — как мы говорили, он создается по умолчанию. Подсеть контейнера имеет свой диапазон IP-адресов. В внешней сети контейнер доступен по IP-адресу хостовой машины.
  • Хост — в данном случае контейнер запускается в сети хоста без какой-либо изоляции. Контейнер получает тот же IP-адрес, что и хост. Данный способ организации сети используют для уменьшения задержек, возникающих при общении хоста и контейнера по сетевому мосту.
  • Нет сети — в данном случае между сетями контейнера и хоста нет никакой связи.
  • Overlay — docker создает свою надсеть между контейнерами с использованием ресурсов сети, организованной между различными хостами, где эти контейнеры развернуты. Узлы overlay сети соединены логическими туннелями, которые используют маршруты underlay сети, т. е. сети между хостами, для передачи данных. Ключевая идея overlay — отделить логическую топологию и сервисы от физической реализации сети. Доступ к контейнеру можно получить по IP-адресу хоста через сетевой мост. А контейнеры между собой общаются по внутренним IP‑адресам.
  • IPvlan — способ организации сети, в которой контейнеры соединяются с внешней VLAN. С помощью данного драйвера контейнеры получают свои уникальные IP-адреса в рамках работы общей сети. В данном варианте настройки пропадает необходимость обращения к контейнерам через IP-адрес хоста.
  • MACvlan — с помощью данного драйвера сетевому интерфейсу задается свой MAC-адрес, что делает его похожим на физический, а не виртуальный интерфейс. Такая реализация сети используется, например, для мониторов сетевого трафика.

Пятый этап — запуск контейнера Docker

На данном этапе применяются все настройки запуска контейнера:

  • переменные среды;
  • порты;
  • созданные тома;
  • созданные настройки и секреты;
  • используемые сети;
  • каталоги монтирования хостовой системы;
  • команды запуска приложений в контейнере;
  • параметры доступа к ресурсам хоста.

Инфраструктура контейнеров

Обо всем об этом мы подробно расскажем далее.

Глава четвертая, в которой Docker пытается подружиться с Windows

Архитектура Windows

А как же Windows, неужели она могла остаться в стороне от столь прогрессивной технологии?

Впервые технология контейнеризации появилась в Windows в 2016 году. И в этом же году Windows раскрыла свои окна для функционала docker.

С первого взгляда docker на Linux ничем не отличается от docker для Windows. Но это только с первого взгляда. За 10 лет существования контейнеризация в Windows так и не смогла достигнуть той гибкости настроек, реализованной на Linux. Что говорить, если Windows-контейнеры для MSSQL, впервые созданные в 2017 году, официально были сняты с поддержки в 2021.

Нет, нельзя сказать, что в Windows контейнеры плохо реализованы. Они функционируют довольно стабильно. Просто они другие и на данный момент не позволяют реализовывать те же фишечки, что и в Linux.

Основное различие в контейнеризации этих двух операционных систем — это различия в архитектуре ядер. Если, как мы говорили раннее, ядро Linux ближе к монолитной архитектуре, то архитектура ядра Windows ближе к микроядерной архитектуре.

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

Однако, у Windows ядро совсем не микро, оно имеет размер 10 МБ, и в этом смысле является монолитным, но с другой стороны его функционал строго ограничен. Ядро отвечает за:

  • диспетчеризация потоков;
  • обработку прерываний и исключений;
  • мультипроцессорную синхронизацию .

При этом, в отличие от Linux, ядро Windows не является самостоятельным, а работает в совокупности с другими компонентами системы. Оно является связующим звеном между исполнительным уровнем (Executive layer — EX), драйверами, графическим интерфейсом, уровнем аппаратных абстракций (Hardware Abstraction Layer, HAL).

Ядро Windows

Уровень аппаратных абстракций (HAL) представляет собой слой кода, изолирующий ядро и исполнительный уровень от аппаратной части. Это позволяет операционной системе работать на широком спектре аппаратных платформ, обеспечивая совместимость и стабильность.

Частью уровня аппаратных абстракций является гипервизор, который загружается до основного ядра Windows. Таким образом, после запуска гипервизора хостовая система является как бы нулевой виртуальной машиной, но с прямым доступом к аппаратным ресурсам через HAL без дополнительного слоя виртуализации.

Исполнительный уровень включает в себя основные службы операционной системы:

  • управление памятью;
  • управление процессами и потоками;
  • обеспечение безопасности;
  • ввод-вывод;
  • сетевые функции.

Драйверы устройств разделяются на аппаратные драйверы, предоставляющие функции перевода вызова функций ввода-вывода в запросы для конкретного аппаратного устройства, и неаппаратные драйверы, такие как драйверы файловой системы и сетевые драйверы.

Графический интерфейс предоставляет функции USER и GDI, предназначенные для работы с окнами, элементами управления пользовательского интерфейса и графикой.

Все перечисленные компоненты, подобно ядру и драйверам Linux, работают в режиме ядра.

В пользовательском режиме работают следующие компоненты:

  • системные процессы;
  • службы;
  • пользовательские приложения.

Приложения в пользовательском режиме взаимодействуют с функциями ядра через подсистемы окружения.

Если касаться контейнеров, то они управляются подсистемой гипервизора и запускаются в пользовательском режиме.

Windows vs Linux

Исходя только из этого простого описания основных компонент Windows видно, насколько ее архитектура сложнее архитектуры Linux. Отсюда и возникли основные концептуальные различия в контейнеризации:

  • Ввиду несамостоятельности ядра, образ контейнера для Windows имеет неприлично большой объем: 7 ГБ для образа Windows Core против 3 МБ для образа микроконтейнера Linux Alpine.
  • При запуске Windows-контейнера запускаются все компоненты, необходимые для поддержания работы пользовательской среды:
    • Процесс инициализации системы wininit.exe.
    • Сервер подсистемы локальной аутентификации lsass.exe.
    • Диспетчер управления службами services.exe.
    • Службы, необходимые для функционирования среды.
    • И наконец, само пользовательское приложение.

    В то время как в Linux запускается только указанное пользовательское приложение.

    Дерево процессов контейнера Windows с 1С

    На рисунке представлено дерево процессов контейнера Windows с 1С, полученное с помощью утилиты procexp на хосте. Процесс wininit.exe является инициализирующем процессом в контейнере.

  • Часть пользовательского функционала внутри Windows-контейнера не доступно наружу. Например, WMI контейнера не доступен, из-за того, что хостовая система занимает те же порты.
  • Стоит упомянуть и про файловую систему NTFS, с которой docker общается через встроенный драйвер windowsfilter. Данный драйвер позволяет создавать для контейнера только обычный том без различных расширенных опций. Также доступны точки монтирования хостовых директорий. Функции монтирования различных внешних томов не доступны.
  • Как говорилось ранее, функционал Linux распределен по различным мелким утилитам. Каждая утилита делает что-то одно, но очень хорошо. Язык bash является связующим звеном между этими утилитами.

    В Windows используется язык PowerShell, и он содержит часть функционала для работы с системой. Однако, имеются различные утилиты, которые либо дублируют, либо содержат другую часть функционала для работы с теми же компонентами системы. Все это создает лишнюю путаницу и усложняет сопровождение системы.

  • Отдельную боль в настройке контейнеров приносит подсистема безопасности доступа к файлам.

Если в Linux все предельно ясно:

  • Доступ к файлу или директории характеризуется набором трёх чисел:

    chmod 766 /opt/1cv8/x86_64/8.3.27.1786/ragent
  • Владелец файла легко и понятно изменяется:

    chown -R usr1cv8:grp1cv8 /var/1C/licenses
  • Пользователя и группу можно создать с определенным идентификатором:

    groupadd -r grp1cv8 --gid="123"
    useradd -r -g grp1cv8 --uid="234" --home-dir=/home/usr1cv8 --shell=/bin/bash usr1cv8

То для Windows все гораздо сложнее:

  • Пользователь создается со своим уникальным идентификатором. Его нельзя никак определить заранее.
  • Для пользователя нужно задать не просто права к папкам, но и включить его в локальные политики безопасности.
  • Из-за того, что пользователь каждый раз создается с новым идентификатором, то после обновления образа при запуске контейнера необходимо переназначать права на все рабочие папки в томах.
  • Настройка доступа к файлам довольна запутана, содержит комбинации групп, пользователей и прав.
Get-Acl G:\1c\8_3_24_1540\conf\conf.cfg | fl
Path   : Microsoft.PowerShell.Core\FileSystem::G:\1c\8_3_24_1540\conf\conf.cfg
Owner  : BUILTIN\Администраторы
Group  : RARUS-SAMARA\Пользователи домена
Access : BUILTIN\Администраторы Allow  FullControl
         NT AUTHORITY\СИСТЕМА Allow  FullControl
         BUILTIN\Пользователи Allow  ReadAndExecute, Synchronize
Audit  :
Sddl   : O:BAG:DUD:(A;ID;FA;;;BA)(A;ID;FA;;;SY)(A;ID;0x1200a9;;;BU)

Поэтому проще всего держать в системе какой-нибудь файл с эталонными правами и копировать его на остальные файлы:

$ACL = Get-Acl "C:\Program Files\1cv8\srvinfo\1cv8wsrv.lst"
Get-ChildItem "$SrvinfoPath" -r | Set-Acl -AclObject $ACL
Get-ChildItem "$LogPath" -r | Set-Acl -AclObject $ACL

Все это не добавляет ясности в настройке инфраструктуры.

Применимость контейнеров Windows

Не смотря на все сложности, нельзя сказать, что технология контейнеризации у Windows не удалась.

Настроенные нами контейнеры для 1С и MSSQL (см. по пути github.com/agibalovsa/-1C_DevOps/tree/master/docker) вполне себе функционируют. За время их эксплуатации не было замечено каких-либо крупных сбоев или внеплановых нагрузок. Сложность в настройке компенсируется их устойчивостью.

Таким образом, если говорить о границах применимости контейнеризации Windows, то это прежде всего:

  • Изоляция устанавливаемых приложений от хостовой системы.
  • Удобство в обслуживание: установка, обновление, удаление.

    Известно, что Windows «складирует» артефакты установки, тем самым увеличивается в размерах, с контейнеризацией, это проблема решается.

  • Удобство в тестировании различных приложений.

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

Windows WSL

Осознавая трудности контейнеризации, Windows подстраховалась и чтобы не отставать от Linux, в том же 2016 году в ней была представлена технология WSL (Windows Subsystem for Linux), позволяющая запускать исполняемые файлы Linux в Windows без необходимости в традиционной виртуальной машине.

В первой версии WSL не было настоящего ядра Linux, вместо этого осуществлялся перевод системных вызовов, что периодически приводило к ошибкам. В 2019 году данный слой был заменен настоящим ядром Linux, работающим в легкой виртуальной машине.

Таким образом все ограничения контейнеризации Windows были решены простым запуском Linux-контейнеров в подсистеме WSL. Как говорится: «Шах и мат». А для подтверждения серьезности намерений по развитию в этом направлении был выпущен MSSQL для Linux, образ которого опубликовали на Docker Hub.

Глава пятая, в которой мы пытаемся изготовить Docker-образ по файлу с рецептами

Среда исполнения

Итак, перейдем непосредственно к магии подготовки docker-контейнеров.

Сначала давайте подготовим среду исполнения. Для этого необходимо выполнить следующие действия:

  • Подготовить виртуальную машину с Linux (или реальную, как удобней).
  • Открыть консоль.
  • Установить docker engine следуя инструкции docs.docker.com/engine/install/.
  • Установите редактор nano для работы с dockerfile:

    apt update && apt install nano -y
  • Создать каталог в удобном месте:

    mkdir -p /srv/docker/training
  • Перейти в данный каталог:

    cd /srv/docker/training

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

Dockerfile

Что такое dockerfile? Если сказать кратко, то это файл описания построения docker-образа. Но это не просто скрипт с командами что-где-взять-как-установить. Если мы будем сравнивать docker-образ с многослойным тортом, то dockerfile — это рецепт приготовления торта, состоящий из секций, где каждая секция создает свой слой образа.

Сборка образа запускается из каталога с dockerfile с помощью команды:

docker build -t ИмяОбраза:ВерсияОбраза .

Где:

  • Флаг «-t» задает тег нового образа, состоящий из имени и версии, разделенных «:».
  • Параметр «.» — это контекст сборки образа. Точка означает текущий каталог, можно указывать относительные текущего каталога пути и абсолютные пути.

Секция FROM

FROM — секция определения базового образа.

Это первая секция, с которой начинается любой dockerfile. В данной секции указывается путь к базовому образу, определяемый за основу для нового образа:

FROM alpine

В данном примере в качестве основы мы взяли образ ОС Linux Alpine. По умолчанию образы загружаются из реестра Docker Hub.

Редактирование dockerfile будем выполнять с помощью команды:

nano dockerfile

Выполним эту команду и вставим в файл описываемую секцию, после чего запустим сборку образа.

docker build -t training:1 .

[+] Building 49.4s (5/5)
=> [internal] load build definition from
=> => transferring dockerfile
=> [internal] load metadata for docker.io/library/alpine:latest
=> [internal] load .dockerignore
=> => transferring context: 2B
=> [1/1] FROM docker.io/library/alpine:latest@sha256:4b7ce07002c69e8f3d704a9c5d6fd3053be500b7f1c69fc0d80990c2ad8dd412
=> => resolve docker.io/library/alpine:latest@sha256:4b7ce07002c69e8f3d704a9c5d6fd3053be500b7f1c69fc0d80990c2ad8dd412
=> => sha256:4b7ce07002c69e8f3d704a9c5d6fd3053be500b7f1c69fc0d80990c2ad8dd412 9.22kB / 9.22kB
=> => sha256:85f2b723e106c34644cd5851d7e81ee87da98ac54672b29947c052a45d31dc2f 1.02kB / 1.02kB
=> => sha256:706db57fb2063f39f69632c5b5c9c439633fda35110e65587c5d85553fd1cc38 581B / 581B
=> => sha256:2d35ebdb57d9971fea0cac1582aa78935adf8058b2cc32db163c98822e5dfa1b 3.80MB / 3.80MB
 => => extracting sha256:2d35ebdb57d9971fea0cac1582aa78935adf8058b2cc32db163c98822e5dfa1b
=> exporting to image
 => => exporting layers
=> => writing image sha256:706db57fb2063f39f69632c5b5c9c439633fda35110e65587c5d85553fd1cc38
=> => naming to docker.io/library/training:1

При выполнении команды сборки видно, что образ был получен из внешнего репозитория. Мы фактически с ним ничего не сделали, просто задали новый тег. Выполним команду:

docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
alpine latest 706db57fb206 13 days ago 8.32MB
training 1 706db57fb206 13 days ago 8.32MB

Видно, что для одного и того же ImageID нашего образа заданы разные теги.

Секция COPY

COPY – секция передачи файлов в dockerfile.

В данной секции указываются какие файлы необходимо передать в новый образ. Файлы берутся из каталога контекста сборки, который указывается при запуске построения образа.

FROM alpine
COPY [ "hello_oc.sh", "/" ]

В данном примере файл hello_oc.sh передается из каталога контекста сборки в корневой каталог ОС образа. Поэтому создадим файл hello_oc.sh в текущем каталоге:

nano hello_oc.sh

и вставим содержание:

#!/bin/sh
echo "Hello 1C!"

После чего сохраним изменения в файле и выйдем из режима редактирования. Проверим, что файл у нас появился в текущем каталоге рядом с dockerfile.

ls -al
total 16
drwxr-xr-x 2 root root 4096 Oct 23 09:56 .
drwxr-xr-x 5 root root 4096 Oct 21 23:45 ..
-rw-r--r-- 1 root root   12 Oct 21 23:46 dockerfile
-rw-r--r-- 1 root root   29 Oct 23 09:56 hello_oc.sh

При копировании в образ вместе с файлом передаются данные о его владельцах и о правах на этот файл. Поэтому в секции COPY можно задать новые права исполнения файла и новые данные о владельцах.

FROM alpine
COPY --chown=usr1cv8:grp1cv8 --chmod=766 [ "hello_oc.sh", "/" ]

При этом стоит напомнить, что файлы не добавляются в базовый образ, а образуют второй слой поверх базового образа.

Выполним сборку образа dockerfile с первым вариантом секции COPY без задания прав файла:

docker build -t training:2 .
*****
=> CACHED [1/2] FROM docker.io/library/alpine
=> [2/2] COPY [ hello_oc.sh, / ]
*****

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

Слои образа можно посмотреть с помощью команды:

docker history --no-trunc training:2
IMAGE CREATED CREATED BY SIZE COMMENT
sha256:5511f03ffb*** About a minute ago COPY hello_oc.sh / # buildkit 29B buildkit.dockerfile.v0
<missing> 2 weeks ago CMD ["/bin/sh"]  0B buildkit.dockerfile.v0
<missing> 2 weeks ago ADD alpine-minirootfs-3.22.2-x86_64.tar.gz / # buildkit 8.32MB buildkit.dockerfile.v0

Мы видим, что вместо двух слоев у нас три слоя, и команды создания первых двух слоев не те, которые в dockerfile. Первые 2 слоя пришли с секцией FROM, как мы увидим дальше, это очень важный момент, что слои не соединяются в один конечный, а передаются как часть истории создания образа.

Секция RUN

RUN — секция выполнения команд для создания нового слоя образа.

SHELL — секция определения оболочки выполнения команд из секции RUN.

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

Команды всегда выполняются не сами по себе, а с помощью оболочки выполнения shell. Кроме задания самой оболочки можно задать дополнительные ключи исполнения. Значение по умолчанию для секции SHELL для Linux ["/bin/sh", "-c"]. Хорошим тоном оформления dockerfile является обязательное указание секции SHELL.

FROM alpine
SHELL ["/bin/sh", "-o", "pipefail", "-c"]
RUN set -eux; \
         addgroup -S grp1cv8 -g "991"; \
         adduser usr1cv8 -D -G grp1cv8 -u "991" -h /home/usr1cv8 -s /bin/sh
COPY --chown=usr1cv8:grp1cv8 --chmod=766 [ "hello_oc.sh", "/" ]

В данном примере в секции RUN создаются группа и пользователь (для Linux Alpine свои команды создания пользователей), после чего в секции COPY их можно указать как владельцев скопированного файла.

В секции SHELL для оболочки sh мы добавили дополнительный ключ «-o pipefail», который следит за выполнением цепочки команд вида cmd1 | cmd2 | cmd3 и сигнализирует об ошибке, если одна из команд в цепочке завершилась аварийно.

Зафиксируем данные изменения в dockerfile и запустим сборку:

docker build -t training:3 .

В протоколе сборки можно будет увидеть лог выполнения команд временного контейнера.

Если выполнить команду:

docker history training:3

То слой секции RUN будет содержать информацию секции SHELL:

RUN /bin/sh -o pipefail -c set  -eux;        addgroup -S grp1cv8 -g  "991";        adduser usr1cv8  -D -G grp1cv8 -u "991" -h /home/usr1cv8 -s /bin/sh 

Хочется заострить внимание еще раз на понятии слоя. Если мы после создания 3 версии образа удалим секции RUN и COPY, запустим сборку снова, то будет взят предыдущий слой базового образа. Не будет никаких обратных операций удаления файлов и удаления пользователей. Причем созданные слои RUN и COPY останутся в кэше. И если мы вернем секции обратно и опять запустим сборку, то ранее построенные слои будут взяты из кэша и наложены на базовый образ без промежуточных операций из секции RUN.

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

Очистку этих артефактов можно выполнить с помощью команды:

docker system prune

Что, порой, высвобождает довольно много места.

Внимание. С особой осторожностью следует выполнять команду:

docker system prune -a

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

Секция ENTRYPOINT

ENTRYPOINT — секция определения команд, которые выполняются при запуске контейнера.

Контейнер не может работать бесцельно, в его запуске всегда должен быть какой‑то смысл. Как раз этот смысл описывается с помощью данной секции.

В секции ENTRYPOINT можно указать:

  • Команду запуска утилиты внутри контейнера.
  • Скрипт для подготовки среды внутри контейнера и запуска утилиты из предыдущего пункта.

Как только работа утилиты прекратится, контейнер автоматически завершит свою работу.

Если запустить контейнер без указания какой-либо команды, то он завершится с ошибкой.

Поместим данную секцию в наш dockerfile:

FROM alpine
SHELL ["/bin/sh", "-o", "pipefail", "-c"]
RUN set -eux; \
         addgroup -S grp1cv8 -g "991"; \
         adduser usr1cv8 -D -G grp1cv8 -u "991" -h /home/usr1cv8 -s /bin/sh
COPY --chown=usr1cv8:grp1cv8 --chmod=766 [ "hello_oc.sh", "/" ]
ENTRYPOINT [ "hello_oc.sh" ]

И соберем 4‑ю версию образа:

docker build -t training:4 .

В итоге, мы получили минимальный рабочий пример образа.

Секция LABEL

LABEL – секция для заметок.

Я думаю, что мы очень довольны собой и своей работой, и чтобы оставить в нашем образе надпись «Киса и Ося были здесь ©», используется секция LABEL. Также в этой секции фиксируется другая полезная информация о контейнере.

Давайте внесем ее в наш dockerfile и соберем пятую версию образа:

FROM alpine
LABEL maintainer="FIO <docker@rarus.ru>" \
      version="5" \
      manufacturer="Rarus"
SHELL ["/bin/sh", "-o", "pipefail", "-c"]
RUN set -eux; \
      addgroup -S grp1cv8 -g "991"; \
      adduser usr1cv8 -D -G grp1cv8 -u "991" -h /home/usr1cv8 -s /bin/sh
COPY --chown=usr1cv8:grp1cv8 --chmod=766 [ "hello_oc.sh", "/" ]
ENTRYPOINT [ "hello_oc.sh" ]
docker build -t training:5 .

Подробную информацию о образе можно получить с помощью команды:

docker inspect training:5
[
    {
        "Id": "sha256:5e4e7ab4095044d803d12a19d1cd7cb44323a9d49f059a19dd4d27808a2c75fa",
        "RepoTags": [
            "training:5"
        ],
        "RepoDigests": [],
        "Parent": "",
        "Comment": "buildkit.dockerfile.v0",
        "Created": "2025-10-23T14:14:59.940608825+03:00",
        "DockerVersion": "",
        "Author": "",
        "Config": {
            "Hostname": "",
            "Domainname": "",
            "User": "",
            "AttachStdin": false,
            "AttachStdout": false,
            "AttachStderr": false,
            "Tty": false,
            "OpenStdin": false,
            "StdinOnce": false,
            "Env": [
                "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
            ],
            "Cmd": null,
            "Image": "",
            "Volumes": null,
            "WorkingDir": "/",
            "Entrypoint": [
                "hello_oc.sh"
            ],
            "OnBuild": null,
            "Labels": {
                "maintainer": "FIO <docker@rarus.ru>",
                "manufacturer": "Rarus",
                "version": "5"
            },
            "Shell": [
                "/bin/sh",
                "-o",
                "pipefail",
                "-c"
            ]
        },
        "Architecture": "amd64",
        "Os": "linux",
        "Size": 8325380,
        "GraphDriver": {
            "Data": {
                "LowerDir": "/var/lib/docker/overlay2/y9ennw8ulfk4t01n70giuecau/diff:/var/lib/docker/overlay2/2ab7937acdfdf0fd24c34246001f480c4f47ee9ee6973a960fd816d68034810f/diff",
                "MergedDir": "/var/lib/docker/overlay2/vhf0jlo4o38jus1nk6bfqnju1/merged",
                "UpperDir": "/var/lib/docker/overlay2/vhf0jlo4o38jus1nk6bfqnju1/diff",
                "WorkDir": "/var/lib/docker/overlay2/vhf0jlo4o38jus1nk6bfqnju1/work"
            },
            "Name": "overlay2"
        },
        "RootFS": {
            "Type": "layers",
            "Layers": [
                "sha256:256f393e029fa2063d8c93720da36a74a032bed3355a2bc3e313ad12f8bde9d1",
                "sha256:e01babf063887fa96acc599420e77fdf274a553525b51c9f86e2314a092f911b",
                "sha256:1309d7eba29ce708ae643798b9891da75e68653d5814771c3f2a9407b48a9f23"
            ]
        },
        "Metadata": {
            "LastTagTime": "2025-10-23T14:34:03.990543502+03:00"
        }
    }
]

С помощью данной команды можно посмотреть всю значимую информацию об образе.

Теперь давайте запустим наше чудо.

Глава шестая, в которой мы запускаем наш первый контейнер

Запуск контейнеров в интерактивном режиме

Команда запуска контейнера выглядит следующим образом:

docker run --name Name -it ImgTag

Здесь:

  • --name — имя контейнера.
  • -it — ключи запуска в интерактивном режиме.

Давайте запустим 3 версию образа, когда мы еще не указали секцию ENTRYPOINT:

docker run --name training -it training:3

В консоли выйдет командная строка внутри контейнера.

Чтобы выйти из контейнера, нужно в командной строке набрать «exit», контейнер тут же завершит свою работу.

Но как же так, спросите вы, ты же нам говорил, что без ENTRYPOINT контейнер выдаст ошибку. Но ранее было сказано, что контейнер выдаст ошибку без задания команды запуска.

А если мы еще раз посмотрим на историю образа:

docker history training:3

То увидим в ней слой с секцией CMD ["/bin/sh"].

Секция CMD — это тоже секция задания команд по умолчанию для запуска в контейнере. Она переопределяется секцией ENTRYPOINT.

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

С помощью интерактивного режима можно протестировать различные команды внутри запущенного контейнера «вживую», до переноса их в секцию RUN внутри образа.

Запуск контейнеров в режиме исполнения

Если не указывать ключ «-it», то контейнер запустится в режиме исполнения. Но запущенная таким образом просто командная оболочка не имеет смысла, наш контейнер просто завершит работу. Нужна какая-нибудь команда.

Любой набор команд выполнения можно указать в конце всех параметров запуска контейнеров docker run. В случае заданной секции CMD данные команд выполнения просто выполнятся вместо нее.

Зададим команду выполнения:

docker run --name training training:3 echo "Hello 1C!"

Может выйти ошибка:

docker: Error response from daemon: Conflict. The container name "/training" is already in use by container "67f46c2977ace44094cabe96f59878671bfe17c3eb19736ccb266a80046fcab9". You have to remove (or rename) that container to be able to reuse that name.

Это значит, что нужно удалить предыдущий контейнер, который остановился, но не удалился. Удаление выполняется командой:

docker rm training

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

Выполним:

docker run --rm --name training training:3 echo "Hello 1C!"

Выйдет сообщение «Hello 1C» и контейнер завершит работу. Данную команду можно выполнять сколько угодно раз, контейнер будет после каждого завершения удаляться, и ошибка конфликта имен контейнеров возникать не будет.

Вернемся теперь к образу №5, в котором указана секция ENTRYPOINT и запустим его:

docker run --rm --name training training:5

Выйдет ошибка:

docker: Error response from daemon: failed to create task for container: failed to create shim task: OCI runtime create failed: runc create failed: unable to start container process: error during container init: exec: "hello_oc.sh": executable file not found in $PATH: unknown

Возникла она из‑за того, что в секции ENTRYPOINT мы не указали путь к файлу hello_oc.sh, и путь к нему не был указан в переменных среды.

Данную оказию можно решить 2 способами:

  1. Указать абсолютный путь в секции ENTRYPOINT [ "/hello_oc.sh" ].
  2. Задать в переменной среды $PATH путь к скрипту.

Используем первый способ и подправим наш dockerfile:

FROM alpine
LABEL maintainer="FIO <docker@rarus.ru>" \
     version="5" \
    manufacturer="Rarus"
SHELL ["/bin/sh", "-o", "pipefail", "-c"]
RUN set -eux; \
     addgroup -S grp1cv8 -g "991"; \
     adduser usr1cv8 -D -G grp1cv8 -u "991" -h /home/usr1cv8 -s /bin/sh
COPY --chown=usr1cv8:grp1cv8 --chmod=766 [ "hello_oc.sh", "/" ]
ENTRYPOINT [ "/hello_oc.sh" ]

Выполним сборку шестой версии образа:

docker build -t training:6 .

И запустим наш контейнер:

docker run --rm --name training training:6

Выйдет снова сообщение "Hello 1C!".

Разница между CMD и ENTRYPOINT

Так в чем разница? Почему в одном месте используется CMD, а в другом ENTRYPOINT? Путаница какая-то...

Все очень просто.

ENTRYPOINT — задает команду запуска контейнера жестко и все команды выполнения, указанные в docker run, будут привязываться к команде ENTRYPOINT в качестве параметров выполнения.

Запустим:

docker run --rm --name training training:6 echo "Hello 1С:Element!"

Выйдет «Hello 1C!», так как наш файл hello_oc.sh никак не обрабатывает входящие параметры.

CMD — задает команду запуска контейнера по умолчанию и легко переопределяется командами выполнения в docker run.

Обычно ENTRYPOINT и CMD используют вместе.

В ENTRYPOINT указывают команду запуска контейнера, а в CMD параметры выполнения по умолчанию к этой команде запуска, которые можно переопределить командами выполнения в docker run.

Взаимосвязь CMD и ENTRYPOINT описана в таблице в официально документации docs.docker.com/reference/dockerfile/#understand-how-cmd-and-entrypoint-interact.

Давайте поиграем немного с ENTRYPOINT и поменяем файл hello_oc.sh:

#!/bin/sh
if [ -n "${1}" ]; then
    echo "${1}";
else
    echo "Hello 1C!";
fi

Выполним сборку седьмой версии образа:

docker build -t training:7 .

И запустим контейнер:

docker run --rm --name training training:7 "Hello 1С:Element!"

Выйдет "Hello 1С:Element!". Таким образом в качестве точки входа в ENTRYPOINT можно указывать целый скрипт, выполняющий те или иные действия в зависимости от указанных команд.

Работа с томами в контейнере

Для добавления тома в контейнер используется параметр запуска «-v».

Если том не создавать вручную (см. главу третью), а запустить контейнер с ключом «-v» сразу, то он создастся автоматически. Проверим это:

docker run --rm --name training -v training:/srv/training  training:7 "Hello 1С:Element!"

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

docker volume ls
local training

Узнать, где конкретно находятся данные тома, можно с помощью команды:

docker volume inspect training
[
    {
        "CreatedAt": "2025-10-26T18:59:03+03:00",
        "Driver": "local",
        "Labels": null,
        "Mountpoint": "/var/lib/docker/volumes/training/_data",
        "Name": "training",
        "Options": null,
        "Scope": "local"
    }
]

Проверим, что в томе сейчас не находится никаких данных:

ls -al "/var/lib/docker/volumes/training/_data"

Как говорилось ранее, если в томе нет никаких данных, то он инициализируется данными из образа docker.

Давайте проверим это. И откорректируем наш dockerfile:

FROM alpine
LABEL maintainer="FIO <docker@rarus.ru>" \
     version="5" \
     manufacturer="Rarus"
SHELL ["/bin/sh", "-o", "pipefail", "-c"]
RUN set -eux; \
     addgroup -S grp1cv8 -g "991"; \
     adduser usr1cv8 -D -G grp1cv8 -u "991" -h /home/usr1cv8 -s /bin/sh; \
     mkdir -p /srv/training; \
     chown usr1cv8:grp1cv8 /srv/training; \
     chmod 766 /srv/training;
COPY --chown=usr1cv8:grp1cv8 --chmod=766 [ "hello_oc.sh", "/srv/training" ]
WORKDIR "/srv/training"
ENTRYPOINT [ "./hello_oc.sh" ]

Здесь мы вносим следующие изменения:

  • Добавляем команду создания папки.
  • Добавляем 2 команды задания прав к папке.
  • Добавляем новую секцию WORKDIR.
  • И немного изменяем ENTRYPOINT, добавив в начале точку.
WORKDIR — это секция, изменяющая текущий каталог в контексте исполнения. По умолчанию текущий каталог является корневым «/».

А для того, чтобы обращаться к файлам в контексте текущего каталога необходимо указать «.» в начале пути.

Выполним сборку восьмой версии образа:

docker build -t training:8 .

И запустим контейнер:

docker run --rm --name training -v training:/srv/training training:8 "Hello 1С:Element!"

После чего опять посмотрим наш том с хоста:

ls -al "/var/lib/docker/volumes/training/_data"
total 12
drw-r-xr-x 2  991  991 4096 Oct 26 19:38 .
drwx-----x 3 root root 4096 Oct 26 19:27 ..
-rwxrw-rw- 1  991  991   80 Oct 26 18:54 hello_oc.sh

В нем обнаружится файлик hello_oc.sh. Обратите внимание, что владельцем файлика является указанный нами в dockerfile пользователь.

Удалим наш том и создадим новый такой же, но уже с какими-нибудь данными:

docker volume rm training
docker volume create training
echo "test" > "/var/lib/docker/volumes/training/_data/test.temp"
ls -al "/var/lib/docker/volumes/training/_data"
total 12
drwxr-xr-x 2 root root 4096 Oct 26 19:53 .
drwx-----x 3 root root 4096 Oct 26 19:53 ..
-rw-r--r-- 1 root root 5 Oct 26 19:53 test.temp

После чего запустим наш образ:

docker run --rm --name training -v training:/srv/training training:8 "Hello 1С:Element!"

Возникнет ошибка «./hello_oc.sh: no such file or directory». Все верно, наш том заменил область файловой системы в контейнере, и файлик hello_oc.sh был заменен файлом test.temp.

Очистим наш том и запустим контейнер снова:

rm -r /var/lib/docker/volumes/training/_data/*
docker run --rm --name training -v training:/srv/training training:8 "Hello 1С:Element!"
ls -al "/var/lib/docker/volumes/training/_data"
total 12
drw-r-xr-x 2  991 991 4096 Oct 26 19:38 .
drwx-----x 3 root root 4096 Oct 26 19:27 ..
-rwxrw-rw- 1 991 991 80 Oct 26 18:54 hello_oc.sh

Контейнер отработает без ошибок и файл hello_oc.sh появится снова.

Примечание. Между прочим, вы заметили, что в команде rm, если используется «*», то кавычки использовать не нужно? Это особенность поведения команд в Linux, wildcard символы не следует помещать в кавычки.

А если мы создадим пустой каталог на хосте и попробуем пробросить его в контейнер, что получится? Давайте попробуем:

mkdir temp_dir
ls -al
total 20
drwxr-xr-x 3 root root 4096 Oct 26 19:45 .
drwxr-xr-x 5 root root 4096 Oct 26 19:45 ..
-rw-r--r-- 1 root root  538 Oct 26 19:12 dockerfile
-rw-r--r-- 1 root root   80 Oct 26 18:54 hello_oc.sh
drwxr-xr-x 2 root root 4096 Oct 26 19:45 temp_dir
docker run --rm --name training -v /srv/docker/training/temp_dir:/srv/training training:8 "Hello 1С:Element!"

Выйдет снова ошибка «hello_oc.sh: no such file or directory». Даже в том случае, если папка хоста пустая, она не заполняется данными из образа контейнера. Это подтверждает разную реализацию и разное поведение томов и точек монтирования.

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

Внимание. Тома удаляются только с помощью команды docker volume rm. Не следует удалять каталог тома, в данном случае «/var/lib/docker/volumes/training/_data», напрямую. Это приведет к ошибкам при работе с контейнерами, из‑за того, что в системе останется запись о томе, а фактически, место хранения данных будет отсутствовать.

Если вы все же так поступили. Что можно сделать:

  • Попробовать удалить том через docker volume rm.
  • Создать каталог по удаленному пути (в данном случае «/var/lib/docker/volumes/training/_data»).

Часто контейнеры создают временные тома. Которые иногда могут занимать значительное место. Для удаления таких томов используется команда:

docker volume prune
Она удалит все неименованные тома, которые не созданы вручную через команду docker volume create, и по которым на данный момент нет запущенных контейнеров.

Внимание. С особой осторожностью следует выполнять команду:

docker volume prune -a

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

Секция USER

Наверное, у многих из вас возник вопрос, а зачем мы меняем пользователя для файла hello_oc.sh. Да, действительно, мы не добавили одну секцию: USER usr1cv8.

USER — это секция, определяющая текущего пользователя, под которым будут выполняться все последующие команды.

Давайте разберемся подробней, для чего нужна эта секция. И для этих целей поменяем наш файл hello_oc.sh:

#!/bin/sh
if [ -n "${1}" ]; then
    echo "${1}" >> hello.txt;
else
    echo "Hello 1C!" >> hello.txt;
fi
cat hello.txt

Соберем девятую версию образа, запустим наш контейнер и посмотрим состав тома:

docker build -t training:9 .
docker run --rm --name training -v training:/srv/training training:9 "Hello 1С:Element!"
ls -al "/var/lib/docker/volumes/training/_data"
total 12
drw-r-xr-x 2  991  991 4096 Oct 26 19:38 .
drwx-----x 3 root root 4096 Oct 26 19:27 ..
-rwxrw-rw- 1  991  991   80 Oct 26 18:54 hello_oc.sh

И никаких файлов в нашем томе не появилось... Как же так? Если посмотреть файл hello_oc.sh в томе, с помощью команды:

cat "/var/lib/docker/volumes/training/_data/hello_oc.sh"

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

Перенесем hello_oc.sh в dockerfile обратно в корневой каталог, уберем секцию WORKDIR:

FROM alpine
LABEL maintainer="FIO <docker@rarus.ru>" \
     version="5" \
     manufacturer="Rarus"
SHELL ["/bin/sh", "-o", "pipefail", "-c"]
RUN set -eux; \
    addgroup -S grp1cv8 -g "991"; \
    adduser usr1cv8 -D -G grp1cv8 -u "991" -h /home/usr1cv8 -s /bin/sh; \
    mkdir -p /srv/training; \
    chown usr1cv8:grp1cv8 /srv/training; \
    chmod 766 /srv/training;
COPY --chown=usr1cv8:grp1cv8 --chmod=766 [ "hello_oc.sh", "/" ]
ENTRYPOINT [ "/hello_oc.sh" ]

Откорректируем и файл hello_oc.sh:

#!/bin/sh
if [ -n "${1}" ]; then
    echo "${1}" >> /srv/training/hello.txt;
else
    echo "Hello 1C!" >> /srv/training/hello.txt;
fi
cat /srv/training/hello.txt

После чего соберем 10 образ, запустим контейнер, и посмотрим содержимое тома:

docker build -t training:10 .
docker run --rm --name training -v training:/srv/training training:10 "Hello 1С:Element!"
Hello 1С:Element!
ls -al "/var/lib/docker/volumes/training/_data"
total 16
drw-r-xr-x 2  991  991 4096 Oct 26 20:28 .
drwx-----x 3 root root 4096 Oct 26 19:53 ..
-rw-r--r-- 1 root root   19 Oct 26 20:28 hello.txt
-rwxrw-rw- 1  991  991   80 Oct 26 18:54 hello_oc.sh

На этот раз наш файлик появился. Как и остался старый файл hello_oc.sh, он теперь будет здесь постоянно, пока мы его не удалим руками. Все изменения, возникающие в контейнере уже после его запуска, как раз в том и попадают. Если после запуска контейнера будет изменен hello_oc.sh, то эти изменения также попадут в том. Однако сейчас нам интересно не это, обратите внимание на владельца файла hello.txt — это пользователь root.

Т. е. все действия, выполняющиеся в docker-контейнере, выполняются под пользователем root. И пользователь root в docker-контейнере совпадает с пользователем root хостовой системы. Для разворачивания сервисов в продуктиве, это, мягко сказать, весьма небезопасная настройка.

Как раз для повышения безопасности и служит секция USER, добавим ее в dockerfile:

FROM alpine
LABEL maintainer="FIO <docker@rarus.ru>" \
      version="5" \
      manufacturer="Rarus"
SHELL ["/bin/sh", "-o", "pipefail", "-c"]
RUN set -eux; \
      addgroup -S grp1cv8 -g "991"; \
      adduser usr1cv8 -D -G grp1cv8 -u "991" -h /home/usr1cv8 -s /bin/sh; \
      mkdir -p /srv/training; \
      chown usr1cv8:grp1cv8 /srv/training; \
      chmod 766 /srv/training;
COPY --chown=usr1cv8:grp1cv8 --chmod=766 [ "hello_oc.sh", "/" ]
USER usr1cv8
ENTRYPOINT [ "/hello_oc.sh" ]

Соберем 11 версию тома и запустим наш контейнер:

docker build -t training:11 .
docker run --rm --name training -v training:/srv/training training:11 "Hello 1С:Element!"

Возникнет ошибка can't create /srv/training/hello.txt: Permission denied. Так как этот файл уже создан под привилегированным пользователем, то наш пользователь не имеет к нему доступа.

Исправим владельца файла в томе:

chown 991:991 /var/lib/docker/volumes/training/_data/hello.txt

Так как мы пользователей из образа на хосте не создавали, то указываем их идентификаторы.

После чего запустим контейнер еще раз:

docker run --rm --name training -v training:/srv/training training:10 "Hello 1С:Element!"
Hello 1С:Element!
Hello 1С:Element!

На этот раз контейнер отработал без запинки.

Примечание. Обратите внимание, что сообщение вышло 2 раза, так как мы в файле скрипта hello_oc.sh указали оператор потока дозаписи в файл «>>».

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

Параметры daemon.json

Во время работы с образами часто требуются следующие настройки:

  • тома контейнеров должны располагаться на отдельном диске;
  • в docker root пользователь должен отличаться от root пользователя хоста;
  • и прочее.

Для этого используется файл настроек docker /etc/docker/daemon.json.

Чтобы выполнить указанные настройки нужно в него добавить следующие опции:

{
"data-root": "/ПутьККаталогуДанныхDocker",
"userns-remap": "default"
}

Для применения настроек обязательно следует перезапускать службу docker.

Глава седьмая, в которой мы собираем образ для сервиса СЛК (SLC)

Определение условий рабочей среды

Друзья, мы достаточно потренировались перед тем, как создать настоящий рабочий контейнер. Теперь, давайте создадим образ для работы сервиса лицензирования СЛК.

Для того, чтобы подготовить образ нам нужно определить следующие пункты

  • Какая базовая система?
  • Какие основные параметры системы?
  • Какие основные условия установки?
  • Какие основные параметры работы службы?
  • Где хранятся изменяемые файлы службы?
  • По каким портам мы сможем работать со службой?

Для сбора данной информации нам нужно прочитать инструкцию по установке СЛК на официальном сайте (prom.licencecenter.ru/), исходя из которой можно понять, как мы создадим dockerfile, и как запустим контейнер.

По прочтению инструкции становится понятно:

  • Для установки на Linux СЛК предлагает deb пакеты, значит нам подойдут базовые образы Debian или Ubuntu.
  • Для корректной работы на Linux нужно установить библиотеку LibUSB.
  • СЛК управляет файлами лицензий и является российским продуктом, следовательно, нам важно настроить русский язык и местное время.
  • Для начала работы сервиса нам подойдут настройки по умолчанию.
  • Настроечный файл и файлы лицензий находятся в каталоге /var/1C/licence/.
  • Подключение к сервису происходит по порту 9099.

Этой информации нам достаточно, для построения dockerfile.

Описание dockerfile

Сначала зададим блок описания системы:

mkdir slc
cd slc
nano dockerfile
FROM debian:bookworm-slim
ARG DEBIAN_FRONTEND=noninteractive \
    DEBCONF_NONINTERACTIVE_SEEN=true \
    SLC_VERSION=3.0.39.12400
LABEL maintainer="FIO <docker@rarus.ru>" \
      version="${SLC_VERSION}" \
      manufacturer="Katran Soft"
SHELL ["/bin/bash", "-o", "pipefail", "-c"]

RUN apt-get update \
        && apt-get install -yq --no-install-recommends locales wget unzip; \
    apt-get clean \
        && rm -rf /var/lib/apt/lists/*; \
    sed -i -e "s/# ru_RU.UTF-8.*/ru_RU.UTF-8 UTF-8/" /etc/locale.gen \
        && dpkg-reconfigure --frontend=noninteractive locales \
        && update-locale LANG=ru_RU.UTF-8;
ENV LANG=ru_RU.UTF-8 \
    LANGUAGE=ru_RU.UTF-8 \
    LC_ALL=ru_RU.UTF-8

Давайте поясним некоторые параметры и новые секции:

  • За базовый образ мы выбираем debian:bookworm-slim, он довольно легкий и продуктивный.
  • В debian используется клиент менеджера пакетов apt, для его корректной работы в docker мы задаем секцию аргументов сборки ARG. С помощью данной секции указываются переменные среды в техническом контейнере на момент сборки. Также с помощью данной секции фиксируется версия СЛК.
  • Заданная переменная версии СЛК через ARG теперь доступна в секции LABEL и во всех других секциях через ссылку на параметр «${SLC_VERSION}».
  • В первой секции RUN мы устанавливаем три пакета:
    • locales — для определения параметров русского языка в системе.
    • wget — утилиты для получения файлов по url. Она нам пригодится в дальнейшем.
    • unzip — утилита для распаковки zip архивов.

    После установки пакетов мы сразу настраиваем параметры русского языка в системе.

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

Теперь зададим блок установки самой службы:

RUN apt-get update \
        && apt-get install -yq --no-install-recommends libusb-1.0-0; \
    apt-get clean \
        && rm -rf /var/lib/apt/lists/*;
RUN mkdir -p /tmp/install_deb; \
    wget --no-check-certificate --progress=dot:giga --timeout=30 -O "/tmp/install_deb/licence.deb.zip" "https://licencecenter.ru/downloads/licence/3.0/${SLC_VERSION}/licence-${SLC_VERSION}.deb.amd64.zip"; \
    unzip "/tmp/install_deb/licence.deb.zip" -d /tmp/install_deb; \
    shopt -s globstar nullglob; \
    for file_deb in /tmp/install_deb/*.deb; do dpkg -i "${file_deb}"; done; \
    rm -r "/tmp/install_deb";
COPY --chmod=766 [ "entrypoint.sh", "/" ]
ENTRYPOINT [ "/entrypoint.sh" ]

Поясним процесс установки самого сервиса:

  • В первой секции мы устанавливаем необходимые библиотеки.
  • Во второй секции мы:
    • скачиваем архив по url ссылке;
    • распаковываем архив;
    • устанавливаем все deb пакеты, находящиеся в архиве.

    Обратите внимание, что в url мы также используем переменную, определенную в секции ARG в начале dockerfile.

  • Так как СЛК устанавливается для пользователя root, мы не стали создавать отдельного пользователя приложений. Однако для продуктива следует задать настройку userns-remap в файле daemon.json.

В итоге получаем общий dockerfile:

FROM debian:bookworm-slim
ARG DEBIAN_FRONTEND=noninteractive \
    DEBCONF_NONINTERACTIVE_SEEN=true \
    SLC_VERSION=3.0.39.12400
LABEL maintainer=" FIO <docker@rarus.ru>" \
      version="${SLC_VERSION}" \
      manufacturer="Katran Soft"
SHELL ["/bin/bash", "-o", "pipefail", "-c"]

RUN apt-get update \
        && apt-get install -yq --no-install-recommends locales wget unzip; \
    apt-get clean \
        && rm -rf /var/lib/apt/lists/*; \
    sed -i -e "s/# ru_RU.UTF-8.*/ru_RU.UTF-8 UTF-8/" /etc/locale.gen \
        && dpkg-reconfigure --frontend=noninteractive locales \
        && update-locale LANG=ru_RU.UTF-8;
ENV LANG=ru_RU.UTF-8 \
    LANGUAGE=ru_RU.UTF-8 \
    LC_ALL=ru_RU.UTF-8
RUN apt-get update \
        && apt-get install -yq --no-install-recommends libusb-1.0-0; \
    apt-get clean \
        && rm -rf /var/lib/apt/lists/*;
RUN mkdir -p /tmp/install_deb; \
    wget --no-check-certificate --progress=dot:giga --timeout=30 -O "/tmp/install_deb/licence.deb.zip" "https://licencecenter.ru/downloads/licence/3.0/${SLC_VERSION}/licence-${SLC_VERSION}.deb.amd64.zip"; \
    unzip "/tmp/install_deb/licence.deb.zip" -d /tmp/install_deb; \
    shopt -s globstar nullglob; \
    for file_deb in /tmp/install_deb/*.deb; do dpkg -i "${file_deb}"; done; \
    rm -r "/tmp/install_deb";
COPY --chmod=766 [ "entrypoint.sh", "/" ]
ENTRYPOINT [ "/entrypoint.sh" ]

При проектировании dockerfile важно понимать один принцип: «Чем больше слоев, тем сложнее образ контейнера в обслуживании». Самые тяжелые слои у нас создают секции RUN. Правильно группировать команды по секциям исходя из их логической связи. В нашем случае мы выделили 3 секции RUN:

  • Настройка системы и установка общих системных пакетов.
  • Установка зависимых пакетов для службы, настройка начальных параметров.
  • Установка основных пакетов для службы.
  • Донастройка системы после установки (в нашем случае ее не было).

Также если бы мы использовали для передачи deb пакетов в образ не команду wget, а секцию COPY, то наш образ утяжелился бы на размер этих пакетов, даже если мы их удалили бы потом из следующего слоя в секции RUN. Слой секции COPY никуда бы не делся. Об этом важном аспекте необходимо всегда помнить, если вы хотите создавать легкие пакеты.

Описание файла entrypoint.sh

Согласно инструкции сервер лицензирования запускается довольно простой командой «/opt/1C/licence/3.0/licenceserver -r». Однако, вне зависимости от простоты запускаемой команды контейнера ее всегда рекомендуется оборачивать в файл entrypoint.sh.

Давайте опишем наш вариант файла:

nano entrypoint.sh
#!/usr/bin/env bash
set -Eeo pipefail
if [ "$1" == "sh" ]; then
    exec sh
elif [ "$1" == "bash" ]; then
    exec /bin/bash
elif [ "$1" = "slc" ]; then
    exec /opt/1C/licence/3.0/licenceserver -r 2>&1
else
    exec /bin/bash
fi
exit 0

В скрипте мы анализируем входящий параметр $1 и определяем, что запустить: командную оболочку или сервер лицензирования (вспомним, что входящие параметры в docker run передаются в самом конце конструкции). Таким образом мы определяем режим запуска контейнера: интерактивный или режим исполнения.

Такой способ описания скрипта entrypoint.sh очень удобен для работы с контейнером. Интерактивный режим нам может понадобиться для отладки работы контейнера. Например, нам нужно зайти внутрь контейнера, когда основная служба запущена. Если в файле entrypoint.sh не будет вариативности, то мы не сможем просто открыть оболочку bash.

Для того, чтобы мы были в курсе всех ошибок с помощью конструкции «2>&1» поток сообщений об ошибках сервера лицензирования перенаправляется в основной поток сообщений работы программы, который логируется системой контейнеризации.

Запуск службы СЛК

Для начала, соберем наш образ:

docker build -t slc:3.0.39.12400 .

В теге контейнера правильно указывать версию устанавливаемого в него приложения.

Во время сборки система будет выводить сжатый протокол установки. Сам процесс установки может быть довольно длинным. Поэтому, если нам нужно посмотреть весь лог установки, то сборку можно запустить с помощью ключа "--progress=plain":

docker build --progress=plain -t slc:3.0.39.12400 .

Теперь можно приступать к запуску контейнера. Какие параметры запуска нам нужно указать?

  • Часовой пояс, в котором будет работать служба.
  • Тома для данных службы.
  • Порты для доступа к службе.
  • При перезапуске хоста требуется, чтобы служба запускалась автоматически.

Команда запуска контейнера будет иметь следующий вид:

docker run --restart=always -e TZ=Europe/Samara -p 9099:9099 -v slc_data:/opt/1C/licence -t slc:3.0.39.12400 slc

Опишем новые параметры запуска:

  • -e — указывает переменные среды запуска контейнера. Через переменные среды можно передавать как настройки системы, так и настройки самих сервисов, которые ориентируются на них. В нашем случае мы передали переменную среды местного часового пояса.
  • -p — данный параметр связывает порт контейнера и порт хоста. С помощью него по IP-адресу хоста и указанному порту можно связаться со службой внутри контейнера.
  • --restart=always — данный параметр указывает на перезапуск контейнера при перезапуске хоста. В противном случае контейнер останется остановленным.
  • --rm – данный параметр мы убрали специально, так как в случае аварийной остановки нам необходимо проанализировать логи нашего сервиса, поэтому контейнер автоматически не должен удаляться.

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

Для выхода из данного режима необходимо выполнить команду Ctrl+C, однако и работа контейнера также завершиться. Чтобы контейнер не блокировал консоль необходимо добавить ключ «-d», тогда он запустится в  фоновом режиме:

docker run -d --restart=always -e TZ=Europe/Samara -p 9099:9099 -v slc_data:/opt/1C/licence -t slc:3.0.39.12400 slc
1636d3773acae2d3de4b3245388ec3584c37b80d89d088213cec84bc9865cc68

Просмотреть список запущенных контейнеров в фоне можно с помощью команды:

docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS
1636d3773aca slc:3.0.39.12400 "/entrypoint.sh slc" 49 seconds ago Up 45 seconds 0.0.0.0:9099->9099/tcp, [::]:9099->9099/tcp

В тексте вывода команды можно получить ID контейнера.

Для получения логов работы контейнера необходимо выполнить команду:

docker logs IDКонтейнера

Для СЛК службы лог будет преимущественно пустым.

Чтобы вместо ID контейнера обращаться к нему по имени, в команду запуска необходимо добавить ключ --name.

Остановим, удалим контейнер:

docker stop IDКонтейнера
docker rm IDКонтейнера

И запустим новый:

docker run -d --restart=always --name slc -e TZ=Europe/Samara -p 9099:9099 -v slc_data:/opt/1C/licence -t slc:3.0.39.12400 slc

Соответственно команда получения логов будет иметь следующий вид:

docker logs slc

Чтобы посмотреть запущенные процессы внутри контейнера можно использовать команду:

docker top slc
UID PID PPID C STIME TTY TIME CMD
root 2645821 2645797 0 окт26 pts/0 00:00:07 /opt/1C/licence/3.0/licenceserver -r

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

Также можно использовать команды управления контейнером:

docker stop slc
docker start slc
docker restart slc

Проверим, что наша служба работает, и подключимся к ней из браузера по url http://IPХоста:9099/.

Появится главная страница нашего сервера:

Главная страница нашего сервера

Чтобы зайти в консоль контейнера и выполнить некоторые команды изнутри, проанализировать внутреннюю файловую систему, необходимо выполнить команду:

docker exec -it slc bash

Однако, стоит учитывать, что образ мы взяли минимальный, и многие утилиты не доступны. Их можно установить в запущенном контейнере, но, если контейнер удалить и запустить новый, то все утилиты нужно будет устанавливать заново. С одной стороны, это не совсем удобно, с другой стороны, хорошо проверенный контейнер и не должен иметь избыточного числа установленных пакетов. В любом случае dockerfile всегда можно дополнить утилитами, часто используемыми в работе.

Глава восьмая, в которой мы строим площадку для запуска контейнеров

Docker compose

Вы, наверное, заметили, как быстро у нас увеличилась в размере команда docker run. А ведь мы запустили самый легкий контейнер. Более сложные контейнеры имеют в два, три раза больше параметров. К тому же, часто сложные среды имеют по несколько взаимодействующих между собой контейнеров, работающих в одной подсети. Если описывать всю эту инфраструктуру с помощью docker run, то мы получим сложно читаемый скрипт, а хотелось бы описывать эти параметры более структурно и компактно.

В этом нам поможет дополнительное приложение для docker, называемое docker-compose.

С помощью docker-compose можно описать конфигурацию инфраструктуры в рамках одного хоста, состоящую из нескольких контейнеров. Задать настройки томов, сетей, настроек, секретов и все в рамках одного конфигурационного файла.

Файл docker-compose.yml для СЛК

Структура конфигурационного файла крайне проста, по своей функциональности она полностью повторяет параметры запуска docker run.

Давайте рассмотрим пример docker-compose.yml для СЛК:

nano docker-compose.yml
services:
  slc:
    container_name: slc
    restart: always
    image: slc:3.0.39.12400
    command: slc
    environment:
      TZ: Europe/Samara
    ports:
      - 9099:9099
    volumes:
      - slc_data:/var/1C/licence
volumes:
  slc_data:

Как видно, каждому параметру запуска docker run соответствует своя секция. Название секций интуитивно понятны и легко соотносятся с названиями параметров запуска.

Сохраним файл docker-compose.yml в нашем каталоге. Остановим и удалим предыдущий запущенный контейнер:

docker stop slc
docker rm slc

После чего запустим новый контейнер с помощью команды:

docker compose up -d
[+] Running 1/1
✔ Container slc Started

В итоге запустится точно такой же контейнер, но уже с более понятными настройками.

Остановить контейнер можно с помощью команды:

docker compose down
[+] Running 3/3
✔ Container slc_1      Removed

К данным настройкам docker-compose.yml мы можем добавить для примера еще один контейнер:

services:
  slc_1:
    container_name: slc_1
    restart: always
    image: slc:3.0.39.12400
    command: slc
    environment:
      TZ: Europe/Samara
    ports:
      - 9099:9099
    volumes:
      - slc_data_1:/var/1C/licence
  slc_2:
    container_name: slc_2
    restart: always
    image: slc:3.0.39.12400
    command: slc
    environment:
      TZ: Europe/Samara
    ports:
      - 9199:9099
    volumes:
      - slc_data_2:/var/1C/licence
volumes:
  slc_data_1:
  slc_data_2:

Для данного контейнера нам необходимо определить другое имя, другое имя тома, указать другой внешний порт. Команда запуска точно та же:

docker compose up -d
Network   slc_default   Created   0.6s
Container   slc_2   Started   2.6s
Container   slc_1   Started  

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

Посмотреть лог выполнения контейнера можно с помощью команды:

docker compose logs

Описание настроек в docker-compose.yml

Рассмотрим структуру файла docker compose немного подробней. Файл имеет формат yml, это значит, что критично важны отступы элементов нижнего уровня относительно родительских элементов. При понижении уровня элемента, он будет отступать на 2 пробела больше от левого края относительно положения родительского элемента.

В docker compose допустимы следующие элементы первого уровня:

  • Services — в нем описываются параметры контейнера.
  • Volumes — в нем описываются параметры томов.
  • Networks — в нем описываются параметры сетей.
  • Configs — в нем описываются конфигурационные файлы.
  • Secrets — в нем описываются пароли.

Вторым уровнем указываются внутренние имена объектов, которые будут созданы при разворачивании среды и получат тип соответственно элементам первого уровня.

Третьи уровнем описываются параметры этих именованных объектов, которые будут им присвоены при создании.

Соответственно, если мы рассмотрим наш последний файл docker-compose.yml, то в нем указанно следующее:

  • Необходимо создать 2 контейнера с внутренним именем slc_1 и slc_2.
  • Необходимо создать 2 тома с внутренним именем slc_data_1 и slc_data_2.
  • Контейнер slc_1 связан с томом slc_data_1.
  • Контейнер slc_2 связан с томом slc_data_2.

Docker compose будет использовать внутреннее имя в качестве ссылки на уровне настроечного файла.

Для всех верхних элементов доступны следующие общие параметры:

  • name — задает внешнее имя объекта, как он будет отображаться в системе. Если его не указать, объекты будут созданы с автогенерируемым именем на основе внутренних имен.
  • external:true (кроме элемента Services) — означает, что объекты с таким параметром автоматически создаваться не будут. Docker compose будет искать объекты по имени среди уже созданных, если не найдет, то выдаст ошибку.

Интересным элементом является «config», который наслаивает описанный конфигурационный файл поверх файловой системы контейнера. Как раз для сервиса СЛК можно задать специальные настройки в конфигурационном файле licenceserver.conf.

Немного изменим наш файл, чтобы зафиксировать в нем проговоренные опции:

services:
  slc_1:
    container_name: slc_1
    hostname: slc_1
    restart: always
    image: slc:3.0.39.12400
    command: slc
    environment:
      TZ: Europe/Samara
    ports:
      - 9099:9099
    volumes:
      - slc_data_1:/var/1C/licence
    configs:
      - source: licenceaddin
        target: /var/1C/licence/3.0/licenceserver.conf
  slc_2:
    container_name: slc_2
    hostname: slc_2
    restart: always
    image: slc:3.0.39.12400
    command: slc
    environment:
      TZ: Europe/Samara
    ports:
      - 9199:9099
    volumes:
      - slc_data_2:/var/1C/licence
volumes:
  slc_data_1:
    external: true
    name: slc_data_1
  slc_data_2:
    external: true
    name: slc_data_1
configs:
  licenceaddin:
    content:  |
      [Console]
      Authorization=0
      UserName=admin
      Password=admin
      LocalAccessOnly=0

Теперь перед запуском среды необходимо создать тома:

docker volume create slc_data_1
docker volume create slc_data_2
docker compose up -d

Если после запуска контейнеров посмотреть конфигурационный файл в томе, то он будет изменён согласно нашим настройкам:

cat /var/lib/docker/volumes/slc_data_1/_data/3.0/licenceserver.conf

Более подробную информацию о настройке docker-compose можно получить на официальном сайте docs.docker.com/reference/compose-file/.

Заключение

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

В этой части мы попытались рассказать об основах контейнеризации. Очень частая ситуация, что настроенную среду специалисты боятся трогать по прошествии некоторого времени: часть настроек забыта, часть настроек громоздка, оно как-то работает — и то хорошо. Так вот, контейнеризация через docker помогает не только автоматизировать рутинные операции по настройке, но и документировать параметры инфраструктуры. Эта технология призвана помочь не бояться сворачивать и разворачивать инфраструктуру по многу раз.

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

Спасибо за внимание!

Вы читаете статью из рубрики:
От экспертов «1С-Рарус»

Читайте первыми статьи от экспертов «1С‑Рарус»

Вы можете получать оповещения по электронной почте

Поле является обязательным

Или получайте уведомления в телеграм-боте

Есть вопросы по статье? Задайте их нам!

Рассылка «Новости компании»

Узнавайте первыми о новых статьях, мероприятиях и спецпредложениях.

Посмотреть все рассылки «1С‑Рарус»

Заинтересованы в сотрудничестве?
Нужна консультация?
Свяжитесь с нами!