UNRUIN
документация · v1.0

Эксплуатация

Развёртывание

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

Как держать Unruin в бою: один образ Docker (бинарник, restic, клиенты баз и docker CLI), конфигурация из git, постоянный том с состоянием и секреты, поданные при старте. Рядом читаются CONFIGURATION.md — каждое поле — и RESTORE.md — восстановление после аварии.

Упрощённое устройство (2026-07): секреты — встроенное хранилище kr:// и UNRUIN_MASTER_PASSPHRASE; ни Vaultwarden с bw, ни rclone больше нет. См. SIMPLIFICATION.md.


Образ

Dockerfile собирает один статический бинарник и кладёт рядом всё, что тот запускает: restic, клиенты баз (pg_dump/mysqldump) и docker CLI (для источников docker_volume). Ни rclone, ни Node/Bitwarden CLI в нём больше нет. Собственный restic образа — это же и тот бинарник, который Unruin доставляет на цели источников-путей, где restic нет. Состояние живёт в /var/lib/unruin, конфигурация читается из /config.

sh
docker build -t unruin:latest .

Быстрый старт (Docker Compose)

В репозитории лежит пример docker-compose.yml. Форма такая:

yaml
services:
  unruin:
    build: .                              # или image: ghcr.io/…/unruin:latest
    restart: unless-stopped
    command: ["serve", "/config/jobs.yaml"]
    volumes:
      - ./config:/config:ro               # ваш репозиторий с конфигурацией
      - unruin-state:/var/lib/unruin    # история, ключи SSH, known_hosts, хранилище kr://
      # - /var/run/docker.sock:/var/run/docker.sock:ro   # только для источников docker_volume
    environment:
      - UNRUIN_MASTER_PASSPHRASE=${UNRUIN_MASTER_PASSPHRASE}  # открывает секреты kr://
      - BREAKGLASS_RESTIC_PW=${BREAKGLASS_RESTIC_PW}            # секрет аварийного конверта, env://
      # - UNRUIN_LISTEN=0.0.0.0:8080      # чтобы выставить панель (ниже)
      # - UNRUIN_API_TOKEN=${UNRUIN_API_TOKEN}
    # ports: ["127.0.0.1:8080:8080"]       # публиковать только на loopback или за обратным прокси
volumes:
  unruin-state:

unruin validate /config/jobs.yaml --preflight, запущенный внутри контейнера, — это и есть решение «пускать или не пускать» перед тем, как отпустить планировщик: он разрешает каждый секрет, соединяется по SSH с каждым сервером и убеждается, что каждое хранилище достижимо.


Том с состоянием — сохраняйте его, это важно

В /var/lib/unruin (UNRUIN_STATE_DIR) лежат:

  • история запусков в SQLite — что было, расписания, здоровье по хранилищам;
  • закреплённые known_hosts: при host_key_policy: strict стёртый known_hosts означает, что каждое соединение SSH будет жёстко падать, пока ключи не запишут заново;
  • хранилище секретов kr:// (secrets.kr), если вы им пользуетесь.

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


Конфигурация: из git или из панели

Две модели, по одной на развёртывание:

  • Из git (в бою рекомендуется). Монтируйте /config только на чтение. jobs.yaml живёт в git; чтобы что-то изменить, делаете git pull и перезапуск (docker compose up -d). Панель только читает и запускает, правка из браузера возвращает 403. Это конфигурация как код: видно в истории, можно обсудить, ничего не меняется на живую.
  • Из панели. Монтируйте /config на запись и разрешите правку (loopback или заданный UNRUIN_API_TOKEN). Тогда задания, серверы и хранилища заводятся, меняются и удаляются из браузера; каждое изменение проверяется и пишется обратно в jobs.yaml. Хорошо для машины с одним оператором; коммитьте файл в git время от времени, чтобы осталась история.

Правка выключена всегда, когда задан UNRUIN_READONLY, независимо от монтирования и адреса.


Секреты в бою

Unruin держит ссылки, которые разрешает в момент запуска. Берите ту схему или те схемы, что вам подходят, — смешивать можно свободно. То, чем они открываются, подавайте переменными окружения (секретами Docker или k8s) и никогда не запекайте секреты в образ или в jobs.yaml.

Схема Как завести Чем открывается
kr:// (встроенная, по умолчанию, проще всего) unruin secret set <имя> (значение со stdin) или вставить в панели — см. TESTING.md. Хранилище лежит на томе состояния. Поддерживает kr://имя#поле. UNRUIN_MASTER_PASSPHRASE
env:// обычная переменная окружения она сама
file:// примонтированный файл-секрет (Docker, k8s)

У kr:// меньше всего движущихся частей: ни внешнего сервера, ни сессии. Задать UNRUIN_MASTER_PASSPHRASE, один раз положить ключи и пароли через unruin secret set — и всё, при каждом перезапуске хранилище открывается само. Размен обычный: кто может прочитать это окружение, тот может расшифровать хранилище.

Правило про аварию: ключ аварийного хранилища держите на env:// (BREAKGLASS_RESTIC_PW), никогда в kr:// — он обязан пережить потерю самого Unruin. И сохраняйте офлайн файл хранилища kr:// (secrets.kr в каталоге состояния) вместе с UNRUIN_MASTER_PASSPHRASE: вдвоём они главный корень ваших секретов, а потеря обоих означает ввод каждого секрета kr:// заново. См. RESTORE.md.

Плашка ⚠ в панели загорается сразу, как только мастер-фраза kr:// отсутствует или неверна, — чтобы плохое открытие было видно до того, как оно тихо уронит задания.


Как выставить панель наружу

По умолчанию безопасно: serve слушает 127.0.0.1:8080 и отказывается стартовать на адресе не-loopback без настроенного доступа. Чтобы дотянуться издалека:

  1. Выберите способ входа (адрес не-loopback устраивает любой из них):
    • Пароль (для людей). Задайте UNRUIN_AUTH_PASSWORD и, по желанию, UNRUIN_AUTH_USER (по умолчанию admin). Панель покажет экран входа и до входа не покажет ничего; сессия едет в подписанной куке HttpOnly. Смените пароль — старые сессии перестанут работать.
    • Ключ (для скриптов, командной строки, MCP). Задайте UNRUIN_API_TOKEN. Каждый запрос обязан нести Authorization: Bearer <token>.
    • Можно задать оба сразу: вход для людей, ключ для автоматики.
  2. Задайте UNRUIN_LISTEN=0.0.0.0:8080 (или привяжитесь к закрытому интерфейсу) и опубликуйте порт.
  3. Поставьте перед ним обратный прокси с TLS — ни кука, ни ключ не должны ехать открытым текстом. За прокси заголовок X-Forwarded-Proto: https сам делает куку сессии Secure.

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

Настроить с помощью ИИ: сервер MCP (unruin mcp) позволяет помощнику заводить серверы, хранилища и задания и вести восстановление обычными словами — см. MCP.md.


Надёжность без присмотра

Для машины, от которой вы уходите, задайте в jobs.yaml:

  • prune_schedule — демон сам применяет правила хранения (привилегированным доступом, в очереди по метке занятости); без этого хранилища растут вечно.
  • run_timeout и timeout у задания — зависшая цель будет убита и записана неудачной, вместо того чтобы держать задание (по умолчанию щедрые шесть часов).
  • notifications.webhook — иначе сбой останется красной строкой в панели, на которую вы перестали смотреть. Подпишите задания на failed и partial.
  • host_key_policy: strict для настоящих серверов: один раз засейте каждый хост через validate --preflight (доверие при первой встрече), после чего strict будет отвергать неизвестный или изменившийся ключ. Нужен постоянный known_hosts из раздела выше.

Наименьшие права

По умолчанию единственный доступ access у хранилища делает всё, включая уборку. Хотите строже — выдайте access только на дозапись (тогда взломанный VPS не сможет удалять точки восстановления) и задайте необязательный prune_access, который живёт только у оркестратора и на цель не уезжает, для forget и уборки. Права такими нужно сделать в вашем объектном хранилище; Unruin сам никогда не отдаёт цели доступ, умеющий удалять. Не задан prune_access — уборка идёт через access.


Переменные окружения

переменная зачем
UNRUIN_MASTER_PASSPHRASE открывает встроенное хранилище секретов kr://
UNRUIN_LISTEN адрес панели и API (по умолчанию 127.0.0.1:8080; не-loopback требует доступа)
UNRUIN_API_TOKEN требовать Authorization: Bearer <token>; для скриптов, командной строки и MCP; годится как доступ для адреса не-loopback
UNRUIN_AUTH_PASSWORD включает вход по имени и паролю; годится как доступ для адреса не-loopback
UNRUIN_AUTH_USER имя для входа (по умолчанию admin)
UNRUIN_API_URL (для unruin mcp) куда мост MCP ходит за serve (по умолчанию http://127.0.0.1:8080)
UNRUIN_READONLY запрещает любые правки конфигурации независимо от адреса
UNRUIN_STATE_DIR где состояние (в образе по умолчанию /var/lib/unruin)
UNRUIN_TICK шаг планировщика (по умолчанию 1m)
UNRUIN_RESTIC_BIN переопределить путь к restic у оркестратора
UNRUIN_NO_RESTIC_PUSH 1 — не доставлять restic на цели источников-путей
UNRUIN_RESTIC_PUSH_BIN какой бинарник restic отправлять на цели вместо собственного
то, на что ссылается env:// любые офлайновые секреты, например BREAKGLASS_RESTIC_PW

Здоровье и наблюдение

  • GET /api/health отдаёт количество заданий, упавшие задания, хранилища, до которых давно не доходит, и secrets_ok — снимайте его метрикой или наведите проверку доступности.
  • Состояния «до этого хранилища не доходит N запусков» и оговорочные или неудачные запуски уходят в настроенный вебхук.
  • Журнал идёт в stdout: по строке на каждый запуск по расписанию, ручной запуск и уборку.

Обновление

Бинарник ничего не держит за пределами тома состояния:

sh
docker compose pull   # или: docker build -t unruin:latest .
docker compose up -d  # контейнер пересоздаётся; том состояния остаётся

При старте Unruin приводит в порядок запуски, оставшиеся в состоянии running после падения (с проверкой pid владельца, поэтому живой запуск из другого процесса не трогается), и ничего разрушительного не делает. После правки конфигурации прогоняйте unruin validate --preflight.

Выпуск образа (Gitea Actions)

.gitea/workflows/release.yml собирает и публикует образ по тегу версии. Он берёт те же учётные данные реестра, что и остальные репозитории этого владельца, настроенные в Settings → Actions (на уровне репозитория или организации — организационные наследуются):

  • переменная DOCKER_REGISTRY — полный путь образа, например git.dreep.ru/smirnar/unruin;
  • секрет DOCKER_USERNAME — учётная запись, которой принадлежит ключ;
  • секрет DOCKER_TOKEN — её ключ или пароль (с правом записи пакетов).

Ещё нужны включённые Actions и зарегистрированный раннер с Docker. Дальше:

sh
git tag v1.2.3 && git push origin v1.2.3

Это публикует <gitea>/<владелец>/unruin:1.2.3 и передвигает :latest. На каждом сервере закрепите версию (или следите за :latest) в docker-compose.yml, затем docker compose pull && docker compose up -d. Том состояния переживает подмену. Одна архитектура, linux/amd64 — в образе лежит статический docker CLI под amd64.

Сайт с документацией собирается отдельным потоком (.gitea/workflows/site.yml) в собственный образ, со своими тегами; продукт и сайт выпускаются независимо.

Реестр внутри Gitea — не единственный вариант. Если нужно место, куда образ просто кладут, без гита вокруг, — раскладка на одном хранилище и nginx описана в REGISTRY.md; DOCKER_REGISTRY тогда указывает туда.


Список проверок перед боем

  • Состояние на долговечном томе, который вы копируете (история, known_hosts, kr://).
  • Секреты поданы окружением или хранилищем секретов, а не лежат в образе или jobs.yaml.
  • Ключ аварийного хранилища на env:// и сохранён офлайн.
  • Панель: либо loopback, либо доступ и TLS за прокси.
  • Заданы prune_schedule, run_timeout и notifications.webhook.
  • host_key_policy: strict, и каждый хост засеян.
  • Задан access (только на дозапись плюс отдельный prune_access, если хотите разделение).
  • validate --preflight проходит.
  • Восстановление проверено от начала до конца (RESTORE.md).

Бесплатно, целиком ваше, наружу ничего не уходит. Сайт статический: ни аналитики, ни куки, ни внешних запросов.