На стенде интегратора Docker поднимает runtime за две минуты: тот же проект, тот же цикл 20 мс, Modbus с эмулятора, HMI в браузере. Заказчик видит и говорит: «На линии поставим так же, на промышленный ПК, без отдельного контроллера». На третьей смене после «некритичного» обновления хоста контейнер стартует, цикл плавает, USB-адаптер RS-485 переезжает на другой tty, NTP скачет, retain читается со слоя, который перезаписали. Смена видит нормы на экране и одновременно ручной режим на агрегате. Стенд врал не в логике программы - он врал в окружении.
Разбираем контейнер (Docker, Podman и аналоги) с softPLC-runtime на промышленном ПК: чем живой объект отличается от стенда, что делают обновления хоста, NTP, изоляция CPU, проброс USB и Ethernet, и когда оставляют обычный аппаратный контроллер. Не повторяем спор «виртуальный ПЛК вообще против шкафа» - здесь не про идею softPLC, а про упаковку runtime в контейнер на IPC и про то, где эта упаковка разваливается в эксплуатации. Не курс по Dockerfile и не сравнение оркестраторов.
Контейнер с runtime ПЛК на IPC работает там, где цикл не жёсткий, I/O идёт по сети без капризного USB-passthrough, хост заморожен по ядру и пакетам, NTP один на весь контур, CPU изолирован, а смена имеет регламент отката образа не хуже замены блока контроллера.
На стенде обычно нет антивируса, нет чужих контейнеров, нет патча ядра раз в месяц, нет оператора, который перетыкает кабель, нет температуры шкафа 48 °C и нет требования «не перезагружать до конца партии». Именно это ломает пуск: джиттер, потеря tty, съехавшие MAC/IP после обновления docker0, overlayfs вместо ожидаемого retain, рассинхрон журналов.
Обычный контроллер оставляют на контурах с предсказуемым циклом у механизма, на ПАЗ/SIL, в шкафу без ИТ-сопровождения и там, где отказ должен лечиться заменой изделия, а не разбором cgroups. Runtime в контейнере - CODESYS, MasterSCADA 4D или иной совместимый IEC 61131-3 - не делает IPC контроллером сам по себе; это тот же проект на чужом планировщике. Среды равнозначны как заказ runtime, но ни одна не отменяет дисциплины хоста.
На практике в образ кладут не «ПЛК», а процесс runtime, библиотеки, иногда веб-HMI, иногда шлюз. Хост - Linux на IPC: ядро, сеть, USB, диски, NTP, журналы. Контейнер видит то, что ему пробросили: PID и CPU через cgroups, файлы через volume, порты через NAT или host network, устройства через --device или privileged.
Путаница начинается со слова «изолирован». Изоляция удобна для доставки версии. Она не даёт детерминизма цикла. Планировщик хоста, прерывания сети, сборка мусора соседнего контейнера, trim SSD, обновление containerd - всё это снаружи образа и на стенде часто выключено.
Podman rootless на объекте выглядит безопаснее для ИБ и больнее для realtime: лишние слои, другие права на устройства, сюрпризы с портами ниже 1024. Docker с privileged «чтобы завелось» зачёркивает смысл контейнера как границы. Нужен минимум прав: конкретные устройства, конкретные порты, read-only корень, volume только под retain, логи и лицензии.
Лицензия runtime привязана к железу, MAC, ключу, файлу. Контейнер, который каждый раз получает новый MAC на bridge, «теряет» лицензию после рестарта. Это стенд не показывает, если лицензия trial или ключ лежит в том же каталоге, что и проект.
Стенд честен по прикладной логике и лжив по эксплуатации. На столе IPC один, контейнеров два, эмулятор I/O отвечает мгновенно, инженер сидит рядом и не ставит Windows Update на хост, потому что хост - Linux «под проект». Сеть - один коммутатор без шторма камер и без сканера ИБ.
На линии тот же IPC делит шкаф с управляемым коммутатором, питанием, иногда с камерой «для охраны», иногда с чужим ПО «поставьте ещё сбор логов». ИБ требует агента. IT требует патч. Энергетики требуют не трогать до плановой остановки. Смена требует, чтобы после мигания света всё поднялось само в правильном порядке: NTP, сеть, runtime, потом поле.
Типовые расхождения стенд / смена:
нагрузка CPU и IRQ, которой не было при эмуляторе; USB-последовательный адаптер, который перечисляется иначе после питания; время хоста, которое сначала «как получилось», потом прыгает на часы; обновление ядра, после которого не грузится модуль realtime или драйвер NIC; volume, который на стенде bind-mount, а в «боевом» compose - named volume и другой uid; сеть host vs bridge: на стенде host, на объекте bridge «для безопасности» и другой latency; второй контейнер (MQTT, Grafana, «маленький Python»), которого «не было в объёме работ».
Пока эти пункты не входят в программу FAT на целевом IPC с целевым образом хоста, стенд доказывает только то, что программа компилируется.
Контейнер обещает: обновили приложение, хост не трогаем. На объекте хост всё равно трогают. CVE в ядре, docker, openssl, агент резервного копирования. После патча «сервисы поднялись», а runtime нет: cgroup v1/v2, iptables/nftables, переименовался интерфейс, SELinux подрезал device.
Регламент, без которого контейнерный ПЛК нельзя сдавать:
заморозка версии ядра и runtime контейнера на период между плановыми остановами; стенд-клон хоста, на котором патч накатывают раньше линии; запрет автообновления; окно отката: старый kernel в grub, старый образ контейнера в registry на площадке, не «в облаке подрядчика»; проверка цикла и I/O после каждого патча, не только docker ps.
ИБ-служба услышит «не патчим» враждебно. Компромисс - патч по регламенту ОТ, как замена прошивки контроллера: план, окно, акт, не ночной silent update.
Антивирус и EDR на хосте с realtime - отдельный разговор. Сканер, который находит «новый» бинарник runtime после обновления образа и начинает его хешировать на лету, даёт пики CPU. На стенде агента нет, на объекте «так политика». Либо исключение каталогов с измеримым влиянием на джиттер, либо runtime не в контейнере на этом хосте.
IPC с контейнерами любит жить с часами VM и с hwclock «потом». Runtime пишет метки, MQTT/OPC шлюз пишет свои, SCADA - третьи. После патча chrony стартует позже контейнера: первые минуты после питания метки в прошлом или в будущем. Смена видит аварию «раньше пуска».
Правило то же, что для любого контура, только жёстче: хост синхронизируется до старта runtime. В systemd это не After=docker.service, а явное ожидание синхронизации (chrony wait, или хотя бы лимит шага) и запись в журнал «время зафиксировано». Контейнеру лучше отдавать тот же clock, не вводить второй NTP внутри образа «для удобства».
Retain и файлы проекта с метками времени разъезжаются, если volume на NFS с другим часовым поясом. Для ПЛК это экзотика, для «IPC в серверной» - нет. FAT должен включать отключение питания на 30 секунд и на 10 минут: что поднялось, какой порядок, какие метки в первом цикле.
SoftPLC на Linux RT имеет смысл, когда процесс прибит к ядрам, IRQ сети уведены, изолированные CPU не отданы Docker «всем подряд». Контейнер по умолчанию делится планировщиком с whoever. cpu_shares - не realtime. Нужны cpuset, иногда isolcpus/nohz_full на хосте, и запрет другим контейнерам садиться на те же ядра.
Privileged realtime в контейнере требует CAP_SYS_NICE, иногда CAP_IPC_LOCK, mlock, корректный chrt. Документация runtime это описывает для «голого» Linux. Слой Docker добавляет свои дефолты: CFS quota, которая внезапно режет процесс в конце квоты и даёт пик цикла раз в период. На стенде квоты нет (unlimited). На объекте compose из шаблона IT квоту ставит «чтобы не сожрал сервер».
Измеряют не средний цикл, а максимум за час под нагрузкой: опрос, архив, веб-HMI, рестарт соседнего контейнера, docker logs. Если 99-й перцентиль вылезает за уставку технологии - либо снимают соседей, либо уходят на аппаратный ПЛК.
CODESYS и MasterSCADA 4D как runtime на одном и том же IPC подчиняются одним законам хоста. Выбор среды не лечит джиттер Docker. Он лечит только то, как написан проект и как лицензируется.
USB-RS-485 на стенде - один адаптер, всегда /dev/ttyUSB0. На объекте после грозы и перетыкания появляется ttyUSB1, udev не успел, контейнер стартовал с неправильным портом, опрос «жив», ответы - чужого устройства или тишина. Симлинки udev по serial id обязательны; в контейнер пробрасывают стабильное имя, не номер. И даже тогда USB в шкафу с помехами - слабое место против встроенного порта IPC или против Ethernet-шлюза.
Ethernet passthrough (macvlan, sriov, USB-NIC в контейнер) ломается на обновлении cni-плагина, на конфликте MAC с сетью завода, на STP, который видит «новый» хост. Host network проще для цикла и хуже для ИБ. Компромисс часто такой: runtime в host network или с выделенным физическим NIC, шлюзы и дашборды - в bridge.
Нельзя пробрасывать «все USB» privileged, потому что на стенде так быстрее. На объекте в тот же IPC втыкают сервисную флешку - и runtime видит новый диск, иногда перемонтирует volume, иногда просто подвисает udev.
Полевая связь лучше уезжает в Ethernet: удалённые I/O, Modbus TCP, OPC. Тогда контейнеру не нужен USB. Это возвращает к архитектуре: контейнерный runtime на IPC хорошо живёт как шлюз и как некритичная логика с сетевым I/O. Плохо - как замена корзины модулей на проводе.
Сетевые привычки те же, что в промышленных Ethernet-сетях: кольцо, шторм, качество линка. Контейнерный NAT не должен стоять на пути цикла до частотника.
Слои образа read-only - плюс для воспроизводимости и минус, если наладчик пишет конфиг внутрь контейнера. После recreate конфиг исчезает. На стенде контейнер не пересоздают неделями. На объекте «пересоздали, чтобы очистить» - и пропали привязки.
Retain, рецепты, лицензии, журналы - только на volume вне слоя. Права uid внутри контейнера должны совпадать с хостом. Rootless Podman здесь регулярно подкладывает 100000+ mapping: runtime не может писать retain, стартует «с нуля», клапаны в безопасное, смена в панике.
Диск IPC в шкафу - часто eMMC или маленький SSD. Журнал Docker и json-file логов без ротации забивает раздел. Runtime при этом «не виноват»: диск 100%, цикл растёт, потом read-only filesystem. На стенде логи смотрят глазами и не копят.
Бэкап: снимок volume плюс тег образа плюс версия хоста. Версионирование проекта ПЛК не заменяется тегом Docker. В Git - исходник. На объекте должен быть акт: какой тег образа, какой commit проекта, какой checksum volume retain. Иначе «откатили контейнер» откатывает бинарник, но не состояние.
Сценарии, где это уже не игрушка:
FAT и постоянный стенд с тем же compose, что на линии, для регресса; агрегация, MQTT/OPC-шлюз, расчёт KPI, не цикл 5 мс; некритичный контур учёта, вентиляции склада, мониторинга энергии; пилотный алгоритм рядом с основным аппаратным ПЛК, с возможностью выключить контейнер рубильником; тиражирование одинаковых узлов, где ценность - одинаковый образ, а не экономия на контроллере.
Даже здесь хост - изделие: промышленный IPC, паспорт, температура, питание 24 В, watchdog железа, который перезапускает хост, если процесс умер. Watchdog Docker healthcheck не заменяет аппаратный.
Аппаратный ПЛК остаётся правильным выбором, когда отказ и джиттер стоят партии или безопасности.
Жёсткий цикл у привода и локальная корзина I/O. Контур, который должен пережить смерть серверной и жить в шкафу у насоса. Функциональная безопасность: контейнер на общем IPC не становится SIL «потому что runtime умеет». Эксплуатация без Linux-администратора: замена блока по ЗИП понятнее, чем podman rollback в три часа. Среда, где IT обязана патчить хост чаще, чем технология допускает останов. Проект, который уже на пределе технического долга legacy: перенос в контейнер не лечит кривую логику, только добавляет слой.
Первый старт среды на железе контроллера и документация интерфейсов - отдельная дисциплина; для CODESYS она собрана в маршруте первого запуска. Тот же проект в контейнере на IPC требует ещё паспорт хоста. MasterSCADA 4D на том же IPC - тот же паспорт хоста, другая лицензия точек, не «более высокий уровень».
| Фактор на объекте | Контейнер ок | Контейнер не ок | Что проверить до пуска |
|---|---|---|---|
| Требуемый цикл и джиттер | >50–100 мс, запас CPU | Жёсткие 1–20 мс у механизма | Максимум цикла 1 ч под нагрузкой, соседи по CPU |
| I/O | Ethernet, стабильный NIC | USB-RS-485 «как получится» | Имена устройств по id, рестарт питания, обмен после udev |
| Обновления хоста | Заморозка + стенд-клон | Автопатч, EDR без исключений | Сценарий патча на клоне, откат kernel, повтор FAT цикла |
| NTP | Хост синхронен до старта runtime | Часы VM, второй NTP в образе | Отключение питания, метки первых событий |
| Сеть контейнера | Выделенный NIC / осмысленный host | NAT на пути цикла, чужой bridge | Latency, MAC/лицензия после recreate |
| Retain и лицензии | Volume + стабильный uid | Запись в слой образа | Recreate контейнера, проверка retain и лицензии |
| Соседи на IPC | Запрещены на ядрах runtime | Grafana, агенты, «ещё Python» | Суммарный CPU/IRQ, рестарт соседа во время цикла |
| Эксплуатация | Регламент образа и отката | «Разберётся подрядчик удалённо» | Инструкция смены, ЗИП IPC, кто имеет право docker |
| ПАЗ / SIL | Нет, рядом отдельный контур | Runtime в контейнере как ПАЗ | Граница ответственности в ТЗ |
| Температура шкафа | IPC в паспорте, запас | Офисный ПК в шкафу | Тепловой режим, троттлинг CPU |
FAT и SAT для контейнерного runtime должны включать хост как изделие. Не только сценарии технолога.
Полный freeze хоста: версия ядра, docker/podman, образа, compose, checksum volume. Рестарт питания шкафа, не docker restart. Убийство процесса runtime без убийства хоста: watchdog, время восстановления, состояние выходов. Рестарт соседнего контейнера и docker pull запрещённый на линии - попытка, которая должна не пройти без окна. Смена USB/кабеля Ethernet и возврат. Патч на клоне с протоколом джиттера до/после. Откат на предыдущий тег образа силами того, кто будет дежурить, не автора Dockerfile.
Если подрядчик говорит «на стенде же работало» без этих пунктов - он сдаёт демо, не систему.
Сдать стендовый compose как боевой. На линии появляются квоты CPU, bridge и агент ИБ.
Privileged и --network host «пока наладка». Наладка заканчивается, флаги остаются, граница контейнера фиктивна.
ttyUSB0 в конфиге. После первого силового цикла опрос смотрит не туда.
Retain в слое образа. Recreate обнуляет контур.
Автообновление хоста. Ядро уехало - realtime и NIC не поднялись.
Второй контейнер «он же почти ничего не ест». Ест IRQ и диск в худший момент партии.
Нет образа на площадке. Интернет на ОТ закрыли, откатить нечем.
Путать среды. «CODESYS - это ПЛК, MasterSCADA - это SCADA, в контейнер кладём только нижний уровень». На IPC обе среды - равноправные runtime проекта; в контейнер кладут выбранный runtime, а не «уровень».
Docker или Podman?
Важно не логотип, а версия, политика rootless, registry на площадке и то, кто умеет откатывать в смену. Для realtime чаще проще предсказуемый Docker/cri с замороженными пакетами, чем «что поставил IT». Выбор фиксируют в паспорте хоста.
Можно ли оркестрировать Kubernetes?
Для цикла ПЛК на среднем заводе обычно нет: лишние демоны, лишние рестарты, лишние руки. Compose/systemd хватает. K8s оставляют IT-контуру, не такту клапана.
Что делать, если лицензия слетает после рестарта?
Искать, что изменилось: MAC, machine-id, путь volume, uid. Стабилизировать идентификатор хоста до пуска, не «привязать заново каждую смену».
Смена имеет право делать docker restart?
Только по карточке аварии, как reset контроллера: с записью в журнале, с пониманием retain и времени цикла на подъёме. Без карточки - звонок тому, кто в акте SAT.
Когда остановить эксперимент и поставить ПЛК?
Когда джиттер не лечится изоляцией ядер, когда USB остаётся единственным полем, когда патч-политика IT несовместима с окном технологии, когда нет человека на откат образа.
Если контур должен жить в шкафу как изделие с экраном, корзиной модулей и заменой блока по ЗИП, промышленный контроллер с Linux RT остаётся прямой альтернативой IPC с контейнером. В линейке СТАБУР runtime заказывают как CODESYS 3.5 или MasterSCADA 4D на одном железе - без иерархии «ПЛК против SCADA». Контейнер на чужом IPC эту развилку не упрощает: он добавляет слой хоста, который нужно паспортизовать отдельно.