Руководство по развёртыванию¶
Production-ready развёртывание системы OKTO: edge-сервис, центральный локальный сервер (далее — ЦЛС), PostgreSQL, центральная консоль, reverse-proxy, мониторинг, бэкапы и миграции.
Аудитория: DevOps, SRE, системные администраторы. Предпосылки: базовое знание Linux, systemd, Docker, PostgreSQL.
Полный A-to-Z сценарий развёртывания¶
От чистого Linux-сервера до 51 работающего шкафа — каждый шаг по имени, со временем и ответственным.
Глоссарий: что такое "factory URL"¶
Во всех примерах ниже и в cabinet-stamp.sh встречается https://factory.customer.ru — это плейсхолдер, подставляемый на конкретный customer. Это DNS-имя (или LAN IP) сервера, на котором админ запустил install-server.sh. Реальные значения могут быть любыми:
| Ситуация у заказчика | Значение factory URL |
|---|---|
| Есть публичный DNS с Let's Encrypt | https://factory.mars-russia.ru |
| Только внутренний DNS на площадке | https://okto-factory.mars.local |
| Только LAN IP, без DNS | https://192.168.10.50 (self-signed cert) |
Значение фиксируется один раз, на шаге 2 мастера первого запуска — мастер спрашивает "на каком хостнейме шкафы будут подключаться к серверу?" и записывает это в /etc/okto/server.env → OKTO_PUBLIC_HOSTNAME. После этого:
- Caddy автоматически выпускает TLS-сертификат на этот хостнейм.
- Дашборд в диалоге "Add Terminal" подставляет этот URL в генерируемую команду
curl. - OKTO-сборка прошивает тот же URL через
cabinet-stamp.sh --factory-url ….
То есть "factory URL" — это то, что скажет сам заказчик при первичной настройке сервера; OKTO перед сборкой получает его от заказчика и использует в stamper'е.
Переназначение шкафа (unpair / reassign)¶
На странице Rollout, в карточке любого шкафа, у которого есть привязанный deviceId, доступна кнопка Unpair cabinet. Это:
- Отзывает device JWT того шкафа на стороне factory-server.
- Отправляет
DeprovisionCmdпо WebSocket в шкаф (best-effort — если шкаф офлайн, всё равно всё очищается на сервере). - На шкафу edge-service очищает локальный токен, перезапускает
PairingClientService— на kiosk'е через несколько секунд появляется свежий PIN. - Слот в rollout переходит в статус
BACK_ON_BENCH,deviceId = null. - Шкаф появляется в панели "Cabinets waiting to pair" — админ либо даёт ему авто-восстановиться на тот же слот (default когда шкаф OKTO-stamped), либо вручную пэйрит его на другой слот через панель ожидания.
Сценарии, когда это нужно: - Шкаф ошибочно stamped'нут на неправильный слот на сборке. - Перестановка шкафа между линиями по решению заказчика. - Резервный шкаф временно занимает слот упавшего, после замены возвращается на свою стоянку. - Диагностика: "отцепить и перепривязать заново, посмотреть, проходит ли pairing чисто".
Endpoint: POST /api/v1/rollout/cabinets/{slotId}/unpair с JSON { "reason": "..." } (только ADMIN). См. RolloutService.unpair.
Шаг 0 · OKTO assembly (один раз за каждый шкаф)¶
Ответственный: OKTO engineer на сборочной линии. Время: ~5 секунд на шкаф.
sudo packaging/cabinet-stamp.sh \
--slot LUZ-DRY-03 \
--factory-url https://factory.customer.ru \
--print-label
Пишет /etc/okto/cabinet-slot + /etc/okto/factory-url на образе шкафа перед отгрузкой. Больше никакой уникальной конфигурации на шкаф — один stamp на сборке, всё.
Шаг 1 · Установка factory-server (один раз на площадку)¶
Ответственный: DevOps / IT-администратор заказчика. Время: ~15 минут от момента "положили флешку" до "открыли мастер в браузере".
Два пути на выбор:
1A. Cloud-init autoinstall USB (новый, полностью unattended):
# На рабочей станции OKTO — собираем загрузочную флешку один раз:
./packaging/cloud-init/build-usb.sh ubuntu-24.04-live-server-amd64.iso
sudo dd if=dist/okto-factory-autoinstall.iso of=/dev/sdX bs=4M
Флешка едет к заказчику. Админ вставляет её в чистый сервер, включает питание, через ~12 минут сервер сам завершает установку Ubuntu → перезагружается → выполняет install-server.sh → показывает на логин-экране (/etc/issue) URL мастера + QR-код. Ноль SSH-сессий, ноль команд в терминале.
1B. Одна команда на живом Ubuntu (существующий путь):
В 1B админ сам устанавливает Ubuntu и вручную запускает команду. Быстрее, если Ubuntu уже стоит; требует SSH.
Шаг 2 · Мастер первого запуска (5 кликов, ~3 минуты)¶
Ответственный: admin заказчика. Время: ~3 минуты.
Открыть URL из Шага 1 → мастер проводит через: пароль админа → публичный хостнейм + TLS → токен OKTO Cloud → метаданные площадки → финиш. Мастер self-seal'ится, OTP инвалидируется, маршрут 404ит.
Опционально: импорт CSV rollout-манифеста, если какие-то шкафы НЕ были stamped на шаге 0 (кнопка Import manifest (unstamped) на странице Rollout).
Шаг 3 · Физический монтаж шкафа (ноль кликов)¶
Ответственный: полевой инженер заказчика. Время: ~10 секунд на шкаф, включая путь к шкафу.
- Подключить питание + Ethernet.
- Уйти.
Edge-service читает /etc/okto/cabinet-slot → представляется factory-server'у как LUZ-DRY-03 → factory-server автоматически апрувит pairing → kiosk открывается с operator UI. Админ в офисе видит в дашборде, как карточка LUZ-DRY-03 переходит PLANNED → ENROLLED за 3-5 секунд.
Шаг 4 · Эксплуатация (1 клик на релиз)¶
Ответственный: admin заказчика. Время: <1 минута на релиз.
- FirmwareAutoUpdateService в factory-server каждые 5 минут опрашивает OKTO Cloud, скачивает новые релизы, показывает баннер в дашборде.
- Админ нажимает Deploy, выбирает canary (1-2 шкафа), observe-окно (по умолчанию 30 мин) → factory-server отправляет
UpdateFirmwareCmdна canary → если canary ок, автоматически промотит на остальной парк; если нет, автоматически откатывает canary (AUTO_ROLLED_BACK). - На любом шкафу в любой момент — кнопка Rollback в карточке релиза: factory-server шлёт
RollbackFirmwareCmd→ edge промотит/opt/okto/edge-service.jar.previousобратно → restart.
Суммарное человеческое внимание за весь rollout¶
| Роль | Действий за весь rollout |
|---|---|
| OKTO engineer (сборка) | 51 × cabinet-stamp.sh ≈ 4 минуты |
| Customer admin (сервер) | 1 × dd USB + 5-step wizard ≈ 9 минут |
| Customer admin (шкафы) | 0 (авто-pair) |
| Field engineer (шкафы) | 51 × подключил 2 кабеля ≈ 10 минут |
| Customer admin (firmware) | ~1 клик на релиз |
| ИТОГО | ~25 минут живого внимания на 51 шкаф |
Важно: дефолтный путь — zero-touch (три модели)¶
Этот документ описывает advanced / air-gapped сценарий с полной ручной настройкой. В типовой установке используется одна из трёх zero-touch моделей:
1. Шкаф OKTO с установленным seal (рекомендуется для 51-шкафного rollout)¶
OKTO стампует /etc/okto/cabinet.seal на этапе сборки на своём производстве (см. packaging/cabinet-seal.sh). Шкаф приезжает к заказчику готовый: подключили питание + Ethernet → edge-service читает seal → auto-enroll в factory-server → киоск открывается.
# На стенде OKTO при сборке шкафа:
sudo ./packaging/cabinet-seal.sh \
--cabinet-id LUZ-DRY-01 \
--factory-url https://factory.customer.ru \
--enrollment-key $(cat /secure/luz-enrollment.key) \
--variant DRY --site LUZ --industry tobacco
Первая загрузка шкафа на площадке: ноль ввода, ноль QR-кодов, ноль curl-ов. См. edge-service/.../CabinetSealService.kt.
2. Vanilla Ubuntu + installer-OTP из консоли (для добавления в существующий парк)¶
Когда у заказчика появляется 52-й шкаф не из поставки OKTO:
# В центральной консоли → «Устройства» → «+ Add Terminal» → QR/curl one-liner
curl -sSL https://<server>/i/ot_XXXXXXXX | sudo bash
3. Standalone без factory-server (для мелких клиентов)¶
# На отдельной Ubuntu-машине:
docker run -d -p 80:80 -v terminal-data:/app/data okto/terminal:latest
# → открыть http://localhost → Настройки → Активация → вход в OKTO Cloud
Сервер (factory-server) — один раз на площадку¶
curl -sSL https://get.okto.ru/server | sudo bash
# → печатает URL мастера первого запуска с одноразовым OTP
Zero-touch сценарии покрывают 95% того, что описано в этом документе (установка Docker, генерация секретов, compose + Caddy + auto-TLS, Flyway миграции, systemd unit, enrollment устройств, kiosk-автозапуск, hardware auto-detect, staged firmware rollout + rollback + auto-update из OKTO Cloud).
Единая концепция rollout — slot-name-as-identity¶
Самый простой путь для 51-шкафного парка. Один концепт вместо двух:
- Имя шкафа в софте == имя rollout-слота в дашборде (например
LUZ-DRY-03). - OKTO-сборка один раз за шкаф выполняет
sudo packaging/cabinet-stamp.sh --slot LUZ-DRY-03 --factory-url https://factory.customer.ru. - На площадке заказчика: воткнули в розетку + Ethernet, шкаф phones home с именем
LUZ-DRY-03, factory-server автоматически апрувит pairing, потому что такой slot уже есть в плане.
Ноль действий от админа заказчика. Ноль CSV. Ноль секретов в шкафу. Ноль ручного типирования где-либо.
Flow в деталях:
- OKTO на своей сборочной линии запускает stamper на каждом шкафу — два текстовых файла:
/etc/okto/cabinet-slot(= имя rollout слота, e.g.LUZ-DRY-03) и/etc/okto/factory-url(HTTPS URL factory-server'а заказчика). - Шкаф едет к заказчику. Ничего секретного внутри: только имя слота + URL, оба напечатаны на наклейке на корпусе.
- Заказчик разворачивает шкаф на линии, подключает Ethernet + питание.
- Edge-service читает
/etc/okto/cabinet-slot→ представляется factory-server'у какLUZ-DRY-03. - Factory-server видит, что слот
LUZ-DRY-03уже в плане rollout'а, аdeviceId = null→ автоматически апрувит pairing, выдаёт device JWT, переводит слот вENROLLED. - Дашборд в Rollout Command Center показывает живой прогресс — карточка слота меняется с
PLANNEDнаENROLLEDза 3-5 секунд, без единого клика админа.
Fallback пути (если stamp не выполнен или шкаф переименован в поле):
- QR-скан — на kiosk'e шкафа есть QR-код рядом с PIN-кодом, админ сканирует с телефона, открывается дашборд с предзаполненной формой pairing, один тап.
- PIN — шкаф показывает 6-значный PIN, админ видит карточку в "Cabinets waiting to pair" на Rollout-странице, пару кликов.
- Hardware-id CSV (unstamped) — кнопка "Import manifest (unstamped)" на Rollout странице принимает CSV со строками
slot_id,hardware_idдля шкафов, которые не были stamped на сборке.
Все три fallback пути не требуют никакой настройки сверх factory-server'а — они работают из коробки.
MARS Rollout Command Center¶
Открывается в консоли по ссылке Развёртывание в левой панели (/rollout). Специализированный экран под контракт OKTO × MARS: 51 шкаф через LUZ / NOV / MIR / RND × DRY / WET × PLC / UPS_ONLY.
Что видит инженер эксплуатации:
- KPI сверху — всего шкафов / % в production / подключено / требует внимания (FAILED, BACK_ON_BENCH).
- Карточки по площадкам — прогресс-бар «в production / всего», разрез по DRY/WET, клик по карточке фильтрует список ниже.
- Phase timeline — F4_PILOT / F5_WAVE_1 / F5_WAVE_2 / F5_WAVE_3 с долями «done / in-flight / planned».
- Список шкафов — фильтры (площадка, вариант, статус), live-статус из
DevicesTable+ текущая firmware-версия, кнопка «Advance» одним кликом переводит в следующий статус. - Drawer по шкафу — полный lifecycle-timeline (sealed → shipped → powered_on → enrolled → observer → cutover → production), журнал событий (кто, когда, что сказал), кнопки всех возможных переходов, кнопка «Open in Fleet» (ведёт на
/devices/{id}когда шкаф уже подключён).
Data-модель: RolloutCabinetsTable + RolloutEventsTable. Seed из 51 шкафа выполняется автоматически при первом старте factory-server (RolloutService.seedIfEmpty); повторные рестарты — no-op. Переходы статусов валидируются whitelist'ом в RolloutService.allowedTransitions — нельзя случайно перескочить из PLANNED сразу в PRODUCTION.
Firmware auto-update + staged rollout + rollback¶
Ключевая функциональность для 51-шкафного парка (§4.3 технического предложения):
- Авто-получение релизов —
FirmwareAutoUpdateServiceопрашиваетhttps://releases.okto.ru/edge-service/manifest.jsonкаждые 5 минут, проверяет SHA-256, загружает артефакт в локальный factory-server, на дашборде появляется баннер «New firmware available». - Staged rollout — админ выбирает canary-устройства (1-2 шкафа), остальные идут автоматически после observe-окна (по умолчанию 30 мин). Если canary не прошёл threshold — автоматический rollback canary-среза. См.
FirmwareService.stagedDeploy. - Rollback — кнопка «Rollback» на карточке релиза в консоли. Edge-service промотирует
/opt/okto/edge-service.jar.previous(бэкап, которыйokto-edge-swap-firmwareсохраняет на каждом обновлении) черезokto-edge-rollback-firmware— без повторной загрузки артефакта. Полный flow: командаRollbackFirmwareCmd→ edge-helper → systemctl restart okto-edge → готово.
Когда нужны остальные разделы ниже¶
- нужна собственная схема хранения секретов (vault, Kubernetes Secrets, AWS SSM);
- требуется тонкая настройка PostgreSQL / PgBouncer / read-replica;
- нужен air-gapped deploy без доступа к
get.okto.ruиreleases.okto.ru; - требуется интеграция с существующим observability-стэком (Grafana Cloud и т. п.);
- делаете миграцию со старой системы или восстановление из бэкапа.
Ключевые файлы реализации zero-touch (для обзора — не меняйте вручную без необходимости):
Bootstrap:
- install-server.sh — bootstrap сервера
- install.sh — bootstrap терминала (OTP-вариант)
- packaging/cabinet-seal.sh — стампер cabinet-seal для сборки OKTO
- docker/docker-compose.server.yml — production compose-стэк
- docker/Caddyfile — reverse-proxy + auto-TLS
- packaging/systemd/okto-server.service — systemd unit сервера
Enrollment:
- factory-server/.../SetupService.kt — state machine мастера первого запуска
- factory-server/.../InstallerService.kt — генерация и разбор installer-OTP
- edge-service/.../CabinetSealService.kt — first-boot auto-enroll по cabinet seal
- management-dashboard/src/pages/Setup.tsx — UI мастера /setup
- management-dashboard/src/ui/components/AddTerminalDialog.tsx — диалог «+ Add Terminal»
Firmware rollout + rollback:
- factory-server/.../FirmwareService.kt — deploy, stagedDeploy, rollback
- factory-server/.../FirmwareAutoUpdateService.kt — авто-поллинг OKTO Cloud manifest
- packaging/systemd/okto-edge-swap-firmware — супервизор swap + backup prev
- packaging/systemd/okto-edge-rollback-firmware — супервизор rollback с последнего backup
- management-dashboard/src/pages/Firmware.tsx — баннер нового релиза, staged UI, Rollback-диалог
Содержание¶
- Варианты развёртывания
- Системные требования
- Подготовка хоста
- Развёртывание ЦЛС
- PostgreSQL: установка и настройка
- Развёртывание edge-сервиса
- Reverse-proxy и TLS
- Центральная консоль (статика)
- Docker Compose: полный стек
- Кибербезопасность и hardening
- Наблюдаемость: Prometheus + Grafana + Loki
- Бэкапы и восстановление
- Миграции схемы
- Обновление (rolling update)
- Масштабирование
- Чек-лист приёмки
1. Варианты развёртывания¶
| Сценарий | Описание | Когда использовать |
|---|---|---|
| Standalone Terminal | Один edge-сервис в режиме DIRECT_CLOUD шлёт напрямую в облако |
1 терминал, простая интеграция, нет on-prem инфраструктуры |
| Factory Site | ЦЛС + N edge-сервисов в режиме VIA_LOCAL_SERVER |
Завод 3–500 терминалов, требование аудита, OTA, группового управления |
| Hybrid | Часть устройств DIRECT_CLOUD, часть VIA_LOCAL_SERVER |
Миграция / A-B rollout |
| High-Availability | Active-Passive ЦЛС + shared PostgreSQL | Критичные производства (роадмап) |
В этом документе описан второй сценарий — Factory Site — как наиболее востребованный.
2. Системные требования¶
Центральный локальный сервер¶
| Пункт | Минимум | Рекомендовано |
|---|---|---|
| CPU | 2 vCPU | 4 vCPU |
| RAM | 4 GB | 8 GB |
| Диск | 40 GB SSD | 200 GB NVMe (для логов устройств и прошивок) |
| ОС | Ubuntu 22.04 / Debian 12 / RHEL 9 | Ubuntu 24.04 LTS |
| Сеть | 100 Mbps | 1 Gbps |
| Java | OpenJDK 17 | OpenJDK 17 (Temurin) |
| PostgreSQL | 14 | 16 |
Edge-сервис (граничное устройство)¶
| Пункт | Минимум | Рекомендовано |
|---|---|---|
| CPU | 2 vCPU | 2 vCPU |
| RAM | 1 GB | 2 GB |
| Диск | 8 GB | 32 GB |
| ОС | Debian 11, Ubuntu 20.04, RHEL 8 | Debian 12 |
| Java | OpenJDK 17 | OpenJDK 17 |
Сетевые порты¶
| Сервис | Порт | Direction | Примечание |
|---|---|---|---|
| ЦЛС HTTP | 8081 | Listen | В продакшне за reverse-proxy на 443 |
| ЦЛС Prometheus | 9091 | Listen | Только loopback / VPN |
| ЦЛС PostgreSQL | 5432 | Outbound к БД | |
| Edge HTTP | 8080 | Listen | Локальный UI оператора |
| Edge → ЦЛС | 443 | Outbound | WSS + HTTPS |
| Edge → OKTO Cloud | 443 | Outbound | В DIRECT_CLOUD режиме |
| Принтеры / Сканеры | Specific | TCP in/out | Обычно 9100, 4001 и т.п. |
| Modbus TCP | 502 | Outbound | PLC |
3. Подготовка хоста¶
3.1 Пользователь и директории¶
sudo useradd --system --home /var/lib/okto --shell /usr/sbin/nologin okto
sudo mkdir -p /opt/okto /var/lib/okto /var/log/okto /etc/okto
sudo chown -R okto:okto /opt/okto /var/lib/okto /var/log/okto
sudo chmod 750 /etc/okto
3.2 OpenJDK 17¶
# Ubuntu / Debian
sudo apt-get update
sudo apt-get install -y openjdk-17-jre-headless
# Проверка
java -version
# openjdk version "17.0.x"
3.3 Файервол (ufw пример)¶
sudo ufw default deny incoming
sudo ufw default allow outgoing
# Доступ по SSH только с bastion host
sudo ufw allow from 10.0.0.5 to any port 22
# HTTPS (через reverse-proxy)
sudo ufw allow 443/tcp
# Prometheus — только VPN
sudo ufw allow from 10.8.0.0/24 to any port 9091
sudo ufw enable
3.4 Временна́я зона и NTP¶
Крайне важно: JWT и аудит полагаются на правильное время. Расхождение > 60 с может инвалидировать токены.
4. Развёртывание ЦЛС¶
4.1 Установка JAR¶
# Загрузите артефакт из CI
sudo -u okto curl -L -o /opt/okto/factory-server.jar \
https://releases.okto.ru/factory-server/1.2.3/factory-server.jar
sudo chown okto:okto /opt/okto/factory-server.jar
4.2 Конфигурация¶
/etc/okto/factory-application.yaml:
server:
host: "127.0.0.1" # reverse-proxy завернёт публичный трафик
port: 8081
enableCors: false
database:
host: "127.0.0.1"
port: 5432
database: "okto_factory"
username: "okto"
password: "${OKTO_DB_PASSWORD}"
maxPoolSize: 20
minIdle: 5
cloudSync:
enabled: true
cloudServerUrl: "https://app.okto.ru/api/v1"
syncIntervalMs: 10000
batchSize: 100
maxRetries: 10
retryDelayMs: 5000
authToken: "${OKTO_CLOUD_AUTH_TOKEN}"
auth:
jwtSecret: "${OKTO_JWT_SECRET}"
jwtIssuer: "okto-factory"
jwtAudience: "okto-edge"
tokenExpirationMs: 86400000
deviceEnrollmentKey: "${OKTO_ENROLLMENT_KEY}"
allowAutoEnrollment: false # в продакшне — false
firmware:
storageDir: "/var/lib/okto/firmware"
maxArtifactSizeBytes: 268435456
management:
defaultCommandTimeoutMs: 30000
maxGroupSize: 500
heartbeatIntervalSeconds: 25
observability:
metricsEnabled: true
metricsPort: 9091
tracingEnabled: true
otlpEndpoint: "http://127.0.0.1:4317"
serviceName: "okto-factory"
Секреты — в /etc/okto/factory-server.env (режим 640, owner okto):
OKTO_DB_PASSWORD=<random 40-char>
OKTO_JWT_SECRET=<random 64-char base64>
OKTO_ENROLLMENT_KEY=<random 32-char>
OKTO_CLOUD_AUTH_TOKEN=<from OKTO Cloud dashboard>
Генерация:
4.3 systemd unit¶
/etc/systemd/system/okto-factory.service:
[Unit]
Description=OKTO Central Local Server (factory-server)
After=network-online.target postgresql.service
Wants=network-online.target
Requires=postgresql.service
[Service]
Type=simple
User=okto
Group=okto
EnvironmentFile=/etc/okto/factory-server.env
Environment=JAVA_OPTS="-Xms512m -Xmx2g -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -Djava.net.preferIPv4Stack=true"
ExecStart=/usr/bin/java $JAVA_OPTS -jar /opt/okto/factory-server.jar /etc/okto/factory-application.yaml
WorkingDirectory=/var/lib/okto
StandardOutput=append:/var/log/okto/factory-server.log
StandardError=append:/var/log/okto/factory-server.log
Restart=always
RestartSec=5
LimitNOFILE=65536
# Security hardening
NoNewPrivileges=yes
ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes
ReadWritePaths=/var/lib/okto /var/log/okto
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectControlGroups=yes
RestrictSUIDSGID=yes
[Install]
WantedBy=multi-user.target
Запуск:
sudo systemctl daemon-reload
sudo systemctl enable --now okto-factory
sudo systemctl status okto-factory
sudo journalctl -u okto-factory -f
4.4 Смена admin-пароля¶
JWT=$(curl -sX POST https://factory.okto.ru/api/v1/auth/login \
-H 'Content-Type: application/json' \
-d '{"username":"admin","password":"admin123"}' | jq -r .data.token)
curl -X POST https://factory.okto.ru/api/v1/auth/change-password \
-H "Authorization: Bearer $JWT" -H 'Content-Type: application/json' \
-d '{"currentPassword":"admin123","newPassword":"<new-strong>"}'
5. PostgreSQL: установка и настройка¶
5.1 Установка¶
5.2 Создание БД и пользователя¶
sudo -u postgres psql <<SQL
CREATE USER okto WITH PASSWORD '<OKTO_DB_PASSWORD>';
CREATE DATABASE okto_factory OWNER okto ENCODING 'UTF8' LC_COLLATE 'en_US.UTF-8' LC_CTYPE 'en_US.UTF-8';
GRANT ALL PRIVILEGES ON DATABASE okto_factory TO okto;
SQL
5.3 Тюнинг (/etc/postgresql/16/main/postgresql.conf)¶
Для 4 vCPU / 8 GB RAM:
shared_buffers = 2GB
effective_cache_size = 6GB
work_mem = 32MB
maintenance_work_mem = 512MB
wal_buffers = 16MB
min_wal_size = 1GB
max_wal_size = 4GB
max_connections = 100
random_page_cost = 1.1 # SSD
# Логирование
log_min_duration_statement = 1000 # 1 s slow queries
log_checkpoints = on
log_connections = on
log_disconnections = on
log_line_prefix = '%t [%p] %q%u@%d '
log_temp_files = 0
/etc/postgresql/16/main/pg_hba.conf — localhost по паролю:
local all all peer
host okto_factory okto 127.0.0.1/32 scram-sha-256
host okto_factory okto ::1/128 scram-sha-256
Перезапуск:
5.4 Ролевой аудит¶
5.5 Схема¶
При первом запуске Database.init() создаст все таблицы через SchemaUtils.create(...). См. §13 Миграции для эволюции.
6. Развёртывание edge-сервиса¶
6.1 Установка¶
sudo -u okto curl -L -o /opt/okto/edge-service.jar \
https://releases.okto.ru/edge-service/1.2.3/edge-service.jar
6.2 Конфигурация /etc/okto/edge-application.yaml¶
server:
host: "0.0.0.0"
port: 8080
corsHosts: ["http://localhost:3000","http://localhost:5180"]
database:
path: "/var/lib/okto/edge.db"
device:
identifier: "edge-01"
name: "Line 1 terminal"
companyId: "acme"
productionLineId: "line-1"
factoryServer:
host: "factory.okto.ru"
port: 443
useSsl: true
enrollmentKey: "${OKTO_ENROLLMENT_KEY}"
deviceName: "Line 1 terminal"
companyId: "acme"
productionLineId: "line-1"
cloud:
host: "app.okto.ru"
port: 443
useSsl: true
apiKey: "${OKTO_CLOUD_API_KEY}" # для DIRECT_CLOUD
connection:
defaultMode: "VIA_LOCAL_SERVER"
allowOverride: false
sync:
intervalMs: 15000
batchSize: 50
maxRetries: 10
retryDelayMs: 5000
printers:
- id: "videojet-1"
type: "VIDEOJET"
host: "10.0.1.10"
port: 9100
scanners:
- id: "honeywell-1"
type: "TCP"
host: "10.0.1.11"
port: 4001
logging:
level: "INFO"
file: "/var/log/okto/edge-service.log"
maxSizeMb: 100
maxBackups: 10
6.3 systemd unit /etc/systemd/system/okto-edge.service¶
См. полный файл в packaging/systemd/okto-edge.service.
Важно:
Environment=OKTO_FIRMWARE_STAGING_DIR=/var/lib/okto/firmware/staging
ExecStart=/usr/bin/java -jar /opt/okto/edge-service.jar /etc/okto/edge-application.yaml
Restart=always
ReadWritePaths=/var/lib/okto /var/log/okto
6.4 OTA-support unit'ы¶
sudo cp packaging/systemd/okto-edge-update.service /etc/systemd/system/
sudo cp packaging/systemd/okto-edge-swap-firmware /usr/local/bin/
sudo chmod 755 /usr/local/bin/okto-edge-swap-firmware
sudo cp packaging/sudoers/okto /etc/sudoers.d/okto
sudo visudo -c -f /etc/sudoers.d/okto # проверка синтаксиса
6.5 Запуск¶
Первая итерация: edge обращается к POST /api/v1/devices/edge-01/token с X-Enrollment-Key, получает device JWT, сохраняет в /var/lib/okto/edge.db → device_auth.
7. Reverse-proxy и TLS¶
7.1 Caddy (рекомендуется для простоты)¶
/etc/caddy/Caddyfile:
factory.okto.ru {
encode gzip zstd
# REST API
reverse_proxy /api/* 127.0.0.1:8081
# Центральная консоль (статика)
handle_path /* {
root * /var/www/okto-console
try_files {path} /index.html
file_server
}
# WebSocket
@ws path /ws/*
reverse_proxy @ws 127.0.0.1:8081 {
transport http {
versions 1.1
read_buffer 65536
}
}
# Security headers
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains"
X-Content-Type-Options "nosniff"
Referrer-Policy "strict-origin-when-cross-origin"
X-Frame-Options "DENY"
}
# Скрываем device JWT из логов
log {
output file /var/log/caddy/factory.log
format filter {
wrap json
fields {
request>uri query_redact
}
}
}
}
7.2 nginx (альтернатива)¶
server {
listen 443 ssl http2;
server_name factory.okto.ru;
ssl_certificate /etc/letsencrypt/live/factory.okto.ru/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/factory.okto.ru/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
client_max_body_size 256m;
location /api/ {
proxy_pass http://127.0.0.1:8081;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_read_timeout 300s;
}
location /ws/ {
proxy_pass http://127.0.0.1:8081;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_set_header Host $host;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
}
location / {
root /var/www/okto-console;
try_files $uri /index.html;
}
# Скрыть device JWT из access log
log_format custom '$remote_addr - $remote_user [$time_local] "$request_method $uri $server_protocol" '
'$status $body_bytes_sent "$http_referer" "$http_user_agent"';
access_log /var/log/nginx/factory.access.log custom;
}
Важно: избегайте $request в log_format для WS-эндпоинтов — он содержит query-string с device JWT.
8. Центральная консоль (статика)¶
cd management-dashboard
npm ci
VITE_API_BASE=/api/v1 VITE_WS_BASE=/ws npm run build
sudo mkdir -p /var/www/okto-console
sudo cp -r dist/* /var/www/okto-console/
sudo chown -R www-data:www-data /var/www/okto-console # или caddy:caddy
Vite собирает SPA в dist/. Reverse-proxy раздаёт с fallback на index.html (SPA routing).
9. Docker Compose: полный стек¶
docker/docker-compose.prod.yml:
services:
postgres:
image: postgres:16-alpine
environment:
POSTGRES_USER: okto
POSTGRES_PASSWORD_FILE: /run/secrets/db_password
POSTGRES_DB: okto_factory
volumes:
- pgdata:/var/lib/postgresql/data
- ./postgres/postgresql.conf:/etc/postgresql/postgresql.conf:ro
secrets:
- db_password
command: ["postgres","-c","config_file=/etc/postgresql/postgresql.conf"]
healthcheck:
test: ["CMD-SHELL","pg_isready -U okto -d okto_factory"]
interval: 10s
timeout: 5s
retries: 5
restart: unless-stopped
factory:
image: okto/factory-server:1.2.3
depends_on:
postgres:
condition: service_healthy
environment:
OKTO_DB_PASSWORD_FILE: /run/secrets/db_password
OKTO_JWT_SECRET_FILE: /run/secrets/jwt_secret
OKTO_ENROLLMENT_KEY_FILE: /run/secrets/enrollment_key
OKTO_CLOUD_AUTH_TOKEN_FILE: /run/secrets/cloud_auth_token
volumes:
- ./factory/application.yaml:/etc/okto/application.yaml:ro
- factory_firmware:/var/lib/okto/firmware
- factory_logs:/var/log/okto
secrets:
- db_password
- jwt_secret
- enrollment_key
- cloud_auth_token
ports:
- "127.0.0.1:8081:8081"
- "127.0.0.1:9091:9091"
restart: unless-stopped
caddy:
image: caddy:2
depends_on: [factory]
ports: ["80:80","443:443"]
volumes:
- ./caddy/Caddyfile:/etc/caddy/Caddyfile:ro
- ./console/dist:/var/www/okto-console:ro
- caddy_data:/data
- caddy_config:/config
restart: unless-stopped
volumes:
pgdata:
factory_firmware:
factory_logs:
caddy_data:
caddy_config:
secrets:
db_password:
file: ./secrets/db_password
jwt_secret:
file: ./secrets/jwt_secret
enrollment_key:
file: ./secrets/enrollment_key
cloud_auth_token:
file: ./secrets/cloud_auth_token
Генерация секретов:
mkdir -p secrets
openssl rand -base64 48 | tr -d '\n' > secrets/jwt_secret
openssl rand -hex 20 | tr -d '\n' > secrets/enrollment_key
openssl rand -base64 32 | tr -d '\n' > secrets/db_password
echo -n "<token-from-okto-cloud>" > secrets/cloud_auth_token
chmod 600 secrets/*
Запуск:
10. Кибербезопасность и hardening¶
Checklist¶
- TLS: только TLSv1.2+, HSTS включён, сертификаты автообновляются (Let's Encrypt / внутренний CA).
- JWT secret хранится в env / vault, не в git.
- Admin-пароль изменён сразу после первого запуска.
- Auto-enrollment выключен (
auth.allowAutoEnrollment: false) после первичной регистрации всех устройств. - Firewall: порт 8081 не открыт снаружи; только 443 через reverse-proxy.
- Rate-limit на
/api/v1/auth/login(nginxlimit_req_zoneили Ktor plugin). - fail2ban реагирует на
LOGIN_FAILEDсобытия (паттерн в/var/log/okto/factory-server.log). - Prometheus слушает только на loopback / VPN.
- Access-логи reverse-proxy стрипают query-string для
/ws/*(device JWT!). - sudoers настроен минимально (только 4 команды).
- SELinux/AppArmor профиль для
okto-factoryиokto-edge(опционально). - Обновления:
unattended-upgradesвключены для security patches. - SSH: login by key only, отключён root, port-knocking или VPN.
- Container image scanning: Trivy / Grype перед каждым релизом.
- Ed25519-подписание прошивок включено (см. SERVER_MANAGEMENT.ru.md §10.5).
Поворот секретов¶
| Секрет | Периодичность | Процедура |
|---|---|---|
auth.jwtSecret |
90 дней | Изменить env var → restart factory → все JWT invalidated, пользователи логинятся заново |
auth.deviceEnrollmentKey |
1 год / при компрометации | Изменить env var → передать новый ключ всем edge (через push_config + restart_service) |
cloudSync.authToken |
по правилам OKTO Cloud | Request new token в OKTO Cloud → update env → restart |
| admin-пароль | 90 дней | UI → Настройки аккаунта → Сменить пароль |
| PostgreSQL password | 180 дней | ALTER ROLE okto WITH PASSWORD '…' → update env → restart |
11. Наблюдаемость: Prometheus + Grafana + Loki¶
11.1 Prometheus¶
/etc/prometheus/prometheus.yml:
global:
scrape_interval: 15s
scrape_configs:
- job_name: 'okto-factory'
static_configs:
- targets: ['127.0.0.1:9091']
- job_name: 'okto-edge'
static_configs:
- targets: ['edge-01:9092','edge-02:9092']
- job_name: 'node'
static_configs:
- targets: ['127.0.0.1:9100']
- job_name: 'postgres'
static_configs:
- targets: ['127.0.0.1:9187']
11.2 Grafana дашборды¶
Рекомендуемые панели:
- Fleet health:
okto_devices_online+ heartbeat lag + CPU / memory по устройствам. - Cloud queue:
okto_cloud_sync_queue_size{status}+ throughput. - Commands:
rate(okto_commands_total[5m]) by (type,status). - Firmware: табло по rollout — % устройств на каждой версии.
- HTTP latency:
histogram_quantile(0.95, …)для/api/v1/syncи/api/v1/devices/{id}/commands. - PostgreSQL: connections, slow queries, replication lag.
- JVM: heap, GC pause, threads.
11.3 Loki для логов¶
clients:
- url: http://loki:3100/loki/api/v1/push
scrape_configs:
- job_name: okto-factory
static_configs:
- targets: [localhost]
labels:
job: okto-factory
__path__: /var/log/okto/factory-server.log
- job_name: okto-edge
static_configs:
- targets: [localhost]
labels:
job: okto-edge
__path__: /var/log/okto/edge-service.log
11.4 Alerting¶
Рекомендованные правила:
groups:
- name: okto.rules
rules:
- alert: FactoryServerDown
expr: up{job="okto-factory"} == 0
for: 1m
labels: { severity: P1 }
annotations:
summary: "Central local server is down"
- alert: CloudQueueDeadLetter
expr: okto_cloud_sync_queue_size{status="DEAD_LETTER"} > 0
for: 5m
labels: { severity: P2 }
- alert: DevicesOfflineSurge
expr: delta(okto_devices_online[5m]) < -5
labels: { severity: P1 }
- alert: CommandTimeoutsHigh
expr: rate(okto_commands_total{status="TIMEOUT"}[5m]) > 0.1
for: 5m
labels: { severity: P2 }
- alert: PostgresHighConnections
expr: pg_stat_activity_count > 80
for: 5m
labels: { severity: P3 }
12. Бэкапы и восстановление¶
12.1 Что бэкапить¶
- PostgreSQL
okto_factory(полностью). - Каталог прошивок
/var/lib/okto/firmware(артефакты с SHA). - Конфигурация
/etc/okto/*.yaml(без секретов — они в env / vault). - Audit log можно экспортировать отдельно для long-term retention.
- Edge SQLite (
/var/lib/okto/edge.db) — критично для операций, ещё не синхронизированных в ЦЛС.
12.2 PostgreSQL: логический бэкап¶
# Ежедневно в 02:00
0 2 * * * pg_dump -Fc -U okto okto_factory | gpg --encrypt --recipient backup@okto.ru \
> /backup/factory-$(date +\%Y\%m\%d).dump.gpg
# Weekly: rsync в offsite
Retention: 7 daily + 4 weekly + 12 monthly.
12.3 PostgreSQL: физический бэкап (PITR)¶
Настройте WAL archiving:
archive_mode = on
archive_command = 'test ! -f /backup/wal/%f && cp %p /backup/wal/%f'
wal_level = replica
И базовый бэкап раз в неделю pg_basebackup -D /backup/base-$(date +%F) -Ft -z -P.
12.4 Восстановление¶
# Logical restore
sudo -u postgres createdb okto_factory
gpg --decrypt /backup/factory-20260417.dump.gpg | pg_restore -U okto -d okto_factory
# PITR
pg_basebackup restore + recovery.conf с restore_command + target time
После restore: рестарт ЦЛС, проверка /api/v1/health, smoke-тест GET /api/v1/devices.
12.5 Edge SQLite¶
Копирование файла во время работы — небезопасно. Используйте:
Cron-задача раз в час. Retention 48 часов локально + sync на ЦЛС.
13. Миграции схемы¶
Текущая модель¶
При старте ЦЛС вызывает SchemaUtils.create(DevicesTable, ...) — создаёт отсутствующие таблицы, но не меняет существующие.
Для продакшна¶
Варианты:
- Flyway (рекомендуется): миграции в
factory-server/src/main/resources/db/migration/(V1__init.sql, V2__add_groups.sql, …), запуск доDatabase.init(). - Liquibase: changelog в XML/YAML.
- Ручные SQL-скрипты +
schema_versionтаблица, прогон черезpsqlв деплой-процедуре.
Пример добавления колонки¶
-- V12__add_device_label.sql
ALTER TABLE devices ADD COLUMN label VARCHAR(100);
CREATE INDEX idx_devices_label ON devices(label);
И параллельно обновление factory-server/src/main/kotlin/ru/okto/factory/persistence/Tables.kt:
Несовместимые изменения¶
При breaking change:
- Опубликуйте мажорную версию (2.0.0).
- Документируйте процедуру миграции в
CHANGELOG.md. - Включите флаг совместимости (
legacy.enableV1Sync=true) на переходный период.
14. Обновление (rolling update)¶
14.1 Центральный сервер¶
# 1. Снимите бэкап БД.
pg_dump -Fc -U okto okto_factory > /backup/pre-upgrade.dump
# 2. Скачайте новый JAR.
sudo -u okto curl -L -o /opt/okto/factory-server-1.3.0.jar \
https://releases.okto.ru/factory-server/1.3.0/factory-server.jar
# 3. Прогоните миграции (если Flyway).
java -jar /opt/okto/factory-server-1.3.0.jar --migrate /etc/okto/factory-application.yaml
# 4. Переключите symlink.
sudo ln -sfn /opt/okto/factory-server-1.3.0.jar /opt/okto/factory-server.jar
# 5. Рестарт.
sudo systemctl restart okto-factory
# 6. Проверка.
curl -sf https://factory.okto.ru/api/v1/health
Downtime: ~10 секунд (рестарт Ktor). Edge-устройства реконнектятся автоматически с backoff.
14.2 Edge-сервис¶
Используйте встроенный OTA-механизм:
- Upload новой версии через UI / API.
- Деплой на canary-группу (1–2 устройства).
- Мониторинг 30 минут: ошибки, метрики, logs.
- Деплой на
stableгруппу поэтапно (10% → 50% → 100%).
См. SERVER_MANAGEMENT.ru.md §10.
14.3 PostgreSQL major upgrade¶
# Ubuntu пример 15 → 16
sudo apt-get install postgresql-16
sudo pg_dropcluster 16 main --stop
sudo pg_upgradecluster 15 main
sudo pg_dropcluster 15 main
# Проверка версии
sudo -u postgres psql -c "SELECT version();"
15. Масштабирование¶
Вертикальное¶
До 500 edge-устройств на одной площадке ЦЛС справляется на 4 vCPU / 8 GB. При росте:
- Перейдите на NVMe-диск (IOPS критичны для cloud_sync_queue).
- Добавьте PgBouncer между ЦЛС и PostgreSQL (pool mode=transaction).
- Увеличьте
hikari.maxPoolSizeдо 50.
Горизонтальное (roadmap)¶
На данный момент не поддерживается: DeviceConnectionRegistry — in-memory. При необходимости:
- Sticky-роутинг по
deviceId(реверс-прокси → один из шардов). - Или внешний координатор (Redis Streams / Postgres NOTIFY) для маршрутизации команд между репликами.
Разделение нагрузки¶
- Read replica PostgreSQL для отчётов (
Dashboard*endpoints). - CDN для центральной консоли (статика).
- Dedicated artifact storage для прошивок (S3-compatible), если размеры > 100 MB.
16. Чек-лист приёмки¶
Перед выводом сайта в production:
Базовая доступность¶
-
GET /api/v1/healthвозвращает 200 -
GET /healthна edge возвращает 200 - Центральная консоль доступна по HTTPS
- TLS сертификат валиден > 30 дней
- Metric-endpoint
:9091/metricsдоступен только из VPN
Безопасность¶
- Admin-пароль изменён
- JWT secret уникален, не
change-me-in-production - Enrollment key уникален
-
auth.allowAutoEnrollment=false -
admin@okto.ruнастроен как получатель security alert - Firewall закрывает всё кроме 443 (+ SSH с bastion)
- Rate-limit на login активирован
- sudoers валиден (
visudo -c)
Функциональность¶
- Тестовое edge-устройство прошло enrollment
- WS-соединение в
/ws/deviceустановилось - Команда
force_syncзавершилась SUCCESS - Одна тестовая бутылка прошла путь edge → ЦЛС → OKTO Cloud
- Upload тестовой прошивки работает
- Деплой на canary-устройство успешен
- Центральная консоль отображает live-события
- Audit log содержит все выше действия
Наблюдаемость¶
- Prometheus собирает метрики ЦЛС и edge
- Grafana dashboard подключён
- Alerting-правила активны
- Loki получает логи
- Логи едут с ротацией
Бэкапы¶
- Ежедневный pg_dump запланирован и проверен
- Восстановление из бэкапа выполнено на тест-стенде
- Offsite-копия настроена
- Runbook «Disaster recovery» опубликован
Документация¶
- IP-адреса и имена хостов в internal wiki
- Кто на on-call в эскалационном листе
- Контакт OKTO Cloud support
Обновлено: апрель 2026. Связанные документы: OPERATIONS.ru.md, SERVER_MANAGEMENT.ru.md, DEVELOPER_GUIDE.ru.md.