ЛМ ЧЗ в Docker: запуск контейнера и инициализация через API

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

Порядок ниже относится к тегу 2.6.1-889-ubuntu22_amd64. Состояние исходного образа, конфигурация после первого запуска и настроенный экземпляр различаются. Для другого тега сначала сверьте конфигурацию и OpenAPI; HTTP 500 до настройки не следует автоматически считать нормой без совпадения описанного состояния и журнала.

Паспорт воспроизведения: сохраните имя и SHA-256 TGZ, тег и ID загруженного образа, архитектуру хоста, версию Docker, имена томов и публикацию порта. Для копии amd64, проверенной 17.09.2026, SHA-256: 16837cd8d5c8a7f737bd6095495d43c9cf0eb4b20ff6bee245ee1cd49b0c8ebf. Контрольная сумма файла не является ID образа. Повторное развёртывание и восстановление из резервной копии в рамках редакционной проверки не выполнялись.

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

Проблема не в том, что инструкции нет. Проблема в том, что штатный способ первичной настройки модуля — окно программы, а в контейнере окна нет. Установщик, который на Windows спрашивает логин, пароль и проводит регистрацию, здесь тоже не запускается: образ стартует голым. Всё, что на Windows делается мышью, в контейнере делается запросами к REST API.

Ниже — полная последовательность, проверенная на живой установке: от docker load до состояния ready. Вместе с четырьмя местами, где всё останавливается, и ни одно из них не описано у производителя.

Отдельно этот способ пригодится на Ubuntu 24.04. Обычный пакет модуля собран под 22.04 и требует libicu70 и libmozjs-78-0 — в 24.04 их нет, там другие версии, а libssl3 вдобавок переименован после перехода на 64-битный time_t. Контейнер эту проблему снимает: внутри него нужная система, а контроллер ЛМ ЧЗ ставится на хост обычным пакетом и прекрасно уживается с ней.

Что внутри образа

Разберём заранее, чтобы дальше было понятно, что мы настраиваем.

Внутри Ubuntu 22.04 и тот же самый deb-пакет, что распространяется отдельно. Рядом с модулем работает встроенная база — форк CouchDB под именем yenisei, она слушает 127.0.0.1:5984 и хранит всё, что модуль качает у ЦРПТ.

В описанном образе задана команда запуска и объявлен порт 5995; его публикацию на хосте задаёт docker run. Процесс работает с UID 1000. Для другого тега сначала прочитайте конфигурацию образа, затем готовьте права томов.

Раскладка внутри:

ЧтоГде
Конфигурация/etc/regime/local.ini
База кодов/opt/yenisei/data
Логи модуля/var/log/regime/regime.log
Логи базы/var/log/regime/yenisei.log
Сам модуль/opt/regime

Про имя regime вместо «ЛМ ЧЗ» — это внутреннее название продукта, оно же будет у службы и папки при обычной установке. Подробнее в статье про логи драйвера и модулей.

Загрузка образа

docker load -i regime_2.6.1-889-ubuntu22_amd64.tgz

В ответ придёт имя, под которым образ зарегистрировался:

Loaded image: registry.equiron.com/regime:2.6.1-889-ubuntu22_amd64

Тег повторяет имя файла, так что архитектура видна прямо в нём.

Проверьте архитектуру сервера заранее. docker load принимает любой образ, не сверяясь с платформой: загрузится и ARM-сборка на процессоре Intel. Несоответствие вылезет только при запуске, ошибкой exec format error. Узнать свою:

uname -m

x86_64 — нужен файл с amd64, aarch64 — с arm64.

Ловушка первая: том под базу и права на него

База кодов пишется в /opt/yenisei/data. Каталог этот VOLUME в образе не объявлен, то есть по умолчанию база живёт в самом контейнере и удаляется вместе с ним. Пересоздали контейнер — и закачка начинается с нуля, а это часы.

Значит, каталог надо вынести в том. И вот здесь ждёт неприятность: в образе этого каталога нет. Он создаётся модулем при первом запуске. Когда вы монтируете том в несуществующий путь, точку монтирования создаёт сам Docker — от имени root. А процесс внутри работает под UID 1000 и писать туда уже не может.

Поэтому владельца тома выставляют заранее, одноразовым контейнером от root:

docker volume create regime-data
docker run --rm -u 0 -v regime-data:/opt/yenisei/data registry.equiron.com/regime:2.6.1-889-ubuntu22_amd64 chown 1000:1000 /opt/yenisei/data

Со вторым томом, под конфигурацию, такой возни не нужно: каталог /etc/regime в образе есть, и Docker при первом монтировании перенесёт в том его содержимое вместе с правами.

Если пропустить этот шаг, симптом будет невнятный: контейнер запустится, порт откроется, а любой запрос к API вернёт 500 с пустым телом.

Запуск

docker run -d --name regime --restart unless-stopped -p 127.0.0.1:5995:5995 -v regime-data:/opt/yenisei/data -v regime-etc:/etc/regime registry.equiron.com/regime:2.6.1-889-ubuntu22_amd64

Порт привязан к 127.0.0.1: к API обращаются только процессы на этом компьютере. Для схемы ЕСМ с контроллером ЛМ ЧЗ на том же хосте так и оставьте. Если кассовое ПО обращается к ЛМ напрямую с другой машины, настройка сетевого доступа разобрана ниже; открывать API на всех интерфейсах одной заменой на -p 5995:5995 не нужно.

И даже с заданным паролем открывать 5995 на всю сеть стоит с оглядкой. Метод /api/v2/status отвечает без авторизации, а в ответе — ИНН организации, идентификатор экземпляра и версия базы. Проверяется запросом без учётных данных:

curl -s http://127.0.0.1:5995/api/v2/status

Если кассовое ПО обращается через ЕСМ, контроллер ЛМ ЧЗ находится на том же хосте и порт можно оставить на loopback. Для прямого доступа с другого компьютера ниже показан только параметр привязки к LAN-IP, а не готовое правило фильтрации:

# Пример привязки только к LAN-адресу хоста Docker:
-p 192.168.1.10:5995:5995

Укажите LAN-адрес именно хоста Docker. Затем настройте ограничение клиентов с учётом сетевого backend Docker и проверьте доступ с разрешённой и посторонней машины; одного UFW для опубликованного порта недостаточно.

В этой инструкции используется bridge-сеть с явной публикацией одного порта. --network host меняет сетевую изоляцию и здесь не требуется. При этом процесс, привязанный к 127.0.0.1:5984, не становится доступным извне только из-за host network: доступность определяется фактическим адресом прослушивания и сетевыми правилами.

Ловушка вторая: два логина в одном файле

Откройте /etc/regime/local.ini — и увидите две пары учётных данных. Их легко перепутать, а последствия у ошибки разные.

Секция [api] содержит логин и пароль API. Закомментированные строки в исходном образе не доказывают состояние авторизации после запуска: модуль может записать собственные значения. Настройку выполняйте при доступе только с loopback, до подключения клиентов; задайте одну пару учётных данных:

[api]
port = 5995
login = ваш_логин
password = ваш_пароль

Секция [local], ключи db_login и db_password — служебные данные встроенной базы. Ими модуль ходит в собственное хранилище. Они уже заполнены, и менять их нельзя: тот же пароль прописан вторым концом в конфигурации базы, и правка в одном файле оставит модуль без доступа к своим данным.

Есть и третья тонкость. При старте модуль шифрует учётные данные и переписывает файл, добавляя префикс DMIYC_ к значению. Если на первом запуске [api] был пустым, модуль сгенерирует пару сам и допишет её в секцию. Ваши строки, добавленные позже, окажутся выше — а действует последняя. Внешне всё выглядит правильно, но API ждёт совсем другой пароль.

Править файл удобнее прямо в контейнере — редактор nano в образ уже включён:

docker exec -it -u 0 regime nano /etc/regime/local.ini

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

docker exec regime sed -n '/^\[api\]/,/^\[/p' /etc/regime/local.ini

И перезапустите контейнер — конфигурация читается только при старте:

docker restart regime

После перезапуска проверьте авторизацию по документированному защищённому методу вашей версии API. Успешный /status этого не подтверждает, поскольку он может быть публичным. При дублирующихся полях устраните их по конфигурации указанной сборки; не переносите правило выбора последней строки на другие версии без проверки.

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

Ловушка третья: пустой service_url

Самая дорогая по времени, потому что диагностируется хуже всех.

В секции [remote] адрес сервиса ЦРПТ закомментирован, а значения по умолчанию у модуля нет:

[remote]
;service_url =

При обычной установке этот адрес прописывает установщик. В образе не прописывает никто — и причина видна прямо в пакете.

Сценарий установки проверяет, работает ли в системе systemd, и только в этом случае задаёт вопросы: имя администратора, пароль и адрес сервиса с подстановкой https://rsapi.crpt.ru по умолчанию, а следом настройки прокси. Внутри контейнера systemd нет, ветка с вопросами не выполняется, а другого способа передать эти значения не предусмотрено.

То есть пустые login, password и service_url в образе — не забывчивость сборщика, а прямое следствие условия в установщике. Заодно это объясняет и предыдущую ловушку: обе пустые пары появляются по одной и той же причине.

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

curl: (52) Empty reply from server

А в логе:

(main) activation, request body: {"enableGreyList":false,...}
(main) terminated in state: not_configured with reason: badarg

То есть запрос сформирован, но отправлять его некуда. Лечится одной строкой:

docker exec -u 0 regime sed -i 's|^;service_url =.*|service_url = https://rsapi.crpt.ru|' /etc/regime/local.ini

Проверить, что строка встала и точка с запятой снята:

docker exec regime sed -n '/^\[remote\]/,$p' /etc/regime/local.ini

И перезапустить — без этого новое значение не подхватится:

docker restart regime

Остальное в этой секции трогать не нужно. Адреса баз и учётные данные для них приходят от ЦРПТ в ответе на инициализацию, и модуль дописывает их сам. Копировать их с другой, уже работающей машины — плохая идея: сервер репликации выдаётся под конкретную установку, и у двух модулей одной организации он может отличаться.

Ловушка четвёртая: /status падает до настройки

Пока модуль не сконфигурирован, метод GET /api/v2/status не работает. Не отдаёт ошибку — падает, с кодом 500 и пустым телом. В логе видна причина: обработчик берёт поле, которого в состоянии not_configured ещё нет.

Касается это только самого раннего состояния. Как только инициализация запущена, метод отвечает штатно и показывает initialization, а после завершения — ready.

Неприятность в том, что метод отказывает ровно тогда, когда он нужнее всего: сразу после запуска контейнера, когда хочется убедиться, что всё поднялось. Пустой ответ с кодом 500 легко принять за сломанный контейнер, неверный пароль или проблему с правами на томе — хотя модуль в этот момент исправен и просто ждёт настройки.

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

Инициализация

Нужен токен. Берётся он в личном кабинете «Честного знака»: вход по электронной подписи, затем создание токена в профиле пользователя. Это тот же токен, что используется при обычной настройке ТС ПИоТ.

Запрос:

curl -i -X POST http://127.0.0.1:5995/api/v2/init -u логин:пароль -H "Content-Type: application/json" -d '{"token":"ВАШ_ТОКЕН"}'

Ответ 200 OK с пустым телом означает, что инициализация принята и пошла.

Что ещё может прийти:

{"errorCode":5040,"reason":"application is busy, retry the request later"} — модуль не закончил стартовую процедуру. Обычная ситуация в первые секунды после перезапуска: подождите минуту и повторите.

Ошибка авторизации на стороне сервиса — токен не принят: истёк, отозван или выпущен для другой организации.

У метода есть и необязательные параметры: kpp либо fiasId (взаимоисключающие, уточняют торговую точку), а также флаги enableGreyList, clearEan, clearSold, clearTokens. По умолчанию все флаги выключены. Если на другой вашей установке «серый» список включён, а здесь нет, поведение проверок будет отличаться — сверьте заранее, повторная инициализация посреди закачки сбрасывает прогресс.

Как включить Swagger

У модуля есть собственное описание API, но по умолчанию оно выключено. Ключ называется коротко — swagger, а не swagger_enabled, как можно решить по внутренним именам в логах:

docker exec -u 0 regime sed -i 's/^\[api\]/[api]\nswagger = true/' /etc/regime/local.ini
docker restart regime

После этого доступны /api/v2/swagger-ui и /api/v2/openapi.json. Выгрузить спецификацию целиком:

curl -s -u логин:пароль http://127.0.0.1:5995/api/v2/openapi.json -o openapi.json

Если вместо неё приходит unknown resourse or incorrect request, значит ключ не подхватился. Проверьте, что строка попала именно в секцию [api], а не в соседнюю:

docker exec regime sed -n '/^\[api\]/,/^\[/p' /etc/regime/local.ini

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

Как следить за закачкой

Первое, что нужно знать: docker logs здесь бесполезен. Наружу выводится только баннер запуска, а всё содержательное модуль пишет в файл.

docker exec regime sh -c 'tail -f /var/log/regime/regime.log' | grep -a "(main)"

Что вы увидите по порядку: initial config received — ЦРПТ ответил; state changed: not_configured -> initialization — модуль перешёл к закачке; затем поток строк вида backup packet received, db: blocked_cis, count: 3/13.

Базы качаются по очереди, каждая своим числом пакетов, и по завершении каждой пишется initialization completed for db: .... Порядок и объём такие:

БазаПакетовЧто в ней
blocked_cis13коды, запрещённые к продаже
blocked_gtin1запрещённые товары
min_price_122минимальные цены, товарная группа 12
min_price_169минимальные цены, товарная группа 16

Здесь стоит обратить внимание на расхождение имён: в журнале минимальные цены идут двумя отдельными базами по товарным группам, а в ответе /api/v2/status они схлопнуты в одну — min_price. Искать в статусе min_price_16 бесполезно, его там нет.

Последняя база завершается строкой initialization completed for db: min_price_16, all dbs ready, следом идёт state changed: initialization -> ready. После этого сразу начинают приходить строки update received — это уже штатная дозакачка изменений, она идёт постоянно.

Числами удобнее смотреть через API:

curl -s -u логин:пароль http://127.0.0.1:5995/api/v2/status

Ниже — объёмы из описанного автором запуска в сентябре 2026 года. Это иллюстрация, не порог готовности: количество записей и пакетов изменяется.

БазаДокументов
blocked_cisоколо 1,9 млн
min_priceоколо 1,5 млн
blocked_gtinоколо 2 000

Готовность — "status":"ready". Заодно в ответе появляется поле dbVersion, которого в состоянии initialization нет.

Контейнер во время закачки не перезапускайте.

Нагрузка на сервер

На инициализации модуль забивает процессор практически полностью: он заливает миллионы документов в базу, а это перестройка индексов на каждую порцию. Нагрузка разовая и держится всё время закачки — на проверенной установке около получаса. После ready остаётся периодическая синхронизация, и потребление падает.

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

docker update --cpus 2 regime

Либо мягче — снизить приоритет, не ставя жёсткого потолка:

docker update --cpu-shares 512 regime

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

Место, занятое объектами Docker, и свободное место файловой системы — разные показатели. Первая команда ниже показывает использование Docker, вторая — доступное место файловой системы, где контейнер видит том базы:

docker system df -v
docker exec regime df -h /opt/yenisei/data

Резервная копия

Оба тома нужны, и теряются они по-разному.

regime-etc хранит конфигурацию, идентификатор экземпляра и выданные ЦРПТ учётные данные. Потеря означает повторную регистрацию модуля.

regime-data хранит саму базу. Потеря означает повторную закачку со всеми часами ожидания.

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

docker stop regime
docker run --rm -v regime-etc:/from -v /root:/to alpine tar czf /to/regime-etc.tgz -C /from .
docker run --rm -v regime-data:/from -v /root:/to alpine tar czf /to/regime-data.tgz -C /from .
docker start regime

Архивы содержат конфигурацию и базу, но их распаковка не доказывает готовность новой установки. Не запускайте одновременно исходный и перенесённый экземпляры с одной идентичностью. После восстановления проверьте права, адреса контроллера/клиентов, статус ready, lastSync и результат проверки через штатный маршрут:

docker volume create regime-etc && docker volume create regime-data
docker run --rm -v regime-etc:/to -v /root:/from alpine tar xzf /from/regime-etc.tgz -C /to
docker run --rm -v regime-data:/to -v /root:/from alpine tar xzf /from/regime-data.tgz -C /to

Владельца после распаковки стоит проверить — ловушка с правами тут повторяется один в один:

docker run --rm -v regime-data:/d alpine ls -lan /d | head

Для описанного тега ожидаются UID/GID 1000:1000. После восстановления проверьте владельца, запуск, статус ready, lastSync и контрольный запрос. Немедленная готовность не гарантируется только успешной распаковкой архивов.

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

Подключение кассовой программы

Настраивается после того, как статус стал ready.

В 1С это отдельная форма настройки подключения к ЛМ «Честный знак». Проверенные значения:

ПолеЗначение
Формат обменаV2
Выполнять обменна локальном компьютере, не на сервере 1С
Адрес подключенияIP сервера, где работает контейнер
Порт5995
Логин и парольиз секции [api]
Таймаут60 секунд

Одного правила UFW здесь недостаточно. Docker обрабатывает опубликованные порты своими правилами, и такой трафик может обходить UFW — это описано в документации Docker. Если удалённый доступ действительно нужен, замените адрес из примера на LAN-адрес хоста, заранее задайте учётные данные API и ограничьте доступ на уровне правил, совместимых с выбранным сетевым backend Docker, либо внешнего межсетевого экрана. Привязка к LAN-IP ограничивает интерфейс, но не список клиентов. После пересоздания контейнера с сохранением томов проверьте доступ и с разрешённой, и с посторонней машины. Для локального контроллера сохраняйте 127.0.0.1:5995:5995.

Пока идёт закачка, программа показывает состояние модуля как «инициализация». Это не ошибка, а честный ответ API. Признаком успеха после ready будет сообщение о корректно настроенном подключении и дата последней синхронизации.

Связка с кассой АТОЛ

Локальный модуль кассу не обслуживает напрямую — между ними работает ТС ПИоТ, а для оборудования АТОЛ эту роль выполняет модуль ЕСМ.

Контроллер ЛМ ЧЗ размещается на той же машине, где локальный модуль — это требование регламента, а не рекомендация: установка на отдельной машине не поддерживается. Применительно к контейнеру это означает хост: контроллер ставится рядом обычным пакетом, а модуль доступен ему на 127.0.0.1:5995, потому что контейнер слушает и петлю тоже.

Пакет лежит в дистрибутиве ЕСМ под именем esm-lm-controller_<версия>_amd64.deb. Зависимостей у него нет, вопросов при установке он не задаёт и сам регистрирует и запускает службу:

sudo apt install -y ./esm-lm-controller_1.6.4.0_amd64.deb
systemctl status esm-lm-controller

В журнале сразу видно, нашёл ли он модуль:

INF Сервер REST слушает порт 5063
INF Сервер gRPC LmVersion слушает порт 50063 (status=configuring)
ERR ошибка парсинга статуса ЛМ ЧЗ: unexpected end of JSON input
INF >>> Версия ЛМ ЧЗ: 2.6.1-889 <<<
INF Сервер gRPC v2.1 слушает порт 50063

Здесь стоит обратить внимание на две вещи.

Портов у контроллера два. REST на 5063 и gRPC на 50063 — это не альтернативы, хотя в руководстве они перечислены через точку с запятой и читаются как «или тот, или этот». Это два сервера в одном процессе, и в интерфейсе ЕСМ указывается именно gRPC — 50063.

Строка со словом «ошибка» здесь нормальная. Контроллер сначала пробует метод старой версии, /api/v1/status, не получает разбираемого ответа и переключается на /api/v2/status. Версия модуля в следующей строке означает, что связка установилась. Пугаться ERR в этом месте не нужно.

В настройках ЕСМ указывается адрес этого сервера и порт 50063. Порт 5995 принадлежит самому модулю, и вписывать его туда нельзя.

И главное, на чём спотыкаются чаще всего: в интерфейсе ЕСМ указывается 50063 — порт контроллера. Порт 5995 — это сам модуль, и вписывать его туда нельзя.

Частые вопросы

Можно ли распаковать tgz и поставить модуль вручную? Нет. Это не архив с программой, а слои контейнера. Для обычной установки берите deb-пакет под свою систему.

Обязательно ли монтировать том? Без тома всё работает, но база живёт внутри контейнера. Любое пересоздание контейнера — и закачка с нуля.

Контейнер запустился, но API отвечает 500 на все запросы. Проверьте владельца каталога командой docker run --rm -v regime-data:/d alpine ls -lan /d — должен быть UID 1000. Если там root, том смонтирован без подготовки прав.

Инициализация обрывается с «Empty reply from server». Не задан service_url в секции [remote]. В логе рядом будет строка terminated in state: not_configured with reason: badarg.

Модуль не принимает пароль, который я задал. В секции [api] две пары логина и пароля: ваша и сгенерированная модулем. Действует последняя. Уберите лишнюю и перезапустите контейнер.

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

Где взять токен? В личном кабинете «Честного знака»: вход по электронной подписи, создание токена в профиле пользователя.

Можно ли скопировать local.ini с работающей машины? Только service_url. Адреса баз, учётные данные и идентификатор экземпляра выдаются под конкретную установку.

Сколько идёт инициализация? В описанной автором установке — 33 минуты, 25 пакетов и указанный объём базы. Это измерение конкретного запуска, а не норматив готовности: состав базы и время меняются. Проверяйте ответ API, прогресс и актуальность синхронизации.

Коротко

Docker-образ загружается под именем registry.equiron.com/regime и запускается без параметров, но работать сразу после запуска не будет. Нужно четыре вещи: подготовить том под /opt/yenisei/data и отдать его UID 1000, задать логин и пароль в секции [api] и убедиться, что пара там одна, прописать service_url = https://rsapi.crpt.ru в секции [remote] и выполнить POST /api/v2/init с токеном из личного кабинета «Честного знака». Только после этого метод /api/v2/status начнёт отвечать, а кассовая программа сможет подключиться. Следить за закачкой — по /var/log/regime/regime.log и счётчикам dbState, но не по docker logs: туда ничего полезного не выводится.


Материал носит информационный характер и основан на разборе официального дистрибутива. Локальный модуль — продукт ЦРПТ, распространяется бесплатно. Сайт atoldriver.ru не является представительством компании АТОЛ или ЦРПТ.