КДПВ: NORA – реестр артефактов на Rust
КДПВ: NORA реестр артефактов на Rust

Нужен был реестр артефактов. Показать студентам цепочку поставки софта: сборка, тесты, push в реестр, деплой. Стандартная задача, казалось бы. "Вошли и вышли, приключение на 20 минут."

Растянулось на несколько месяцев.

В итоге написал свой реестр. Один бинарник. 7 форматов. 12 МБ RAM. Без базы данных.

Кладбище альтернатив

Nexus. Администрировал его годами на разных площадках. Знаю хорошо, где blob store хранит метаданные, как не убить OrientDB после апгрейда, а если окирпичилась – как откачать, куда смотреть, когда cleanup policy молча сожрала нужный артефакт.

С версии 3.71 Sonatype выкинули OrientDB и переделали линейку: Core (OSS, EPL, H2 embedded), Community (бесплатная, PostgreSQL, проприетарная лицензия, лимит 200K запросов/день), Pro (HA, replication, SSO). Миграция между версиями – это то ещё отдельное извраприключение.

4 гига RAM на Java-процесс. Образ 600 МБ.

Artifactory. OSS-версия формально существует (Apache 2.0), но поддерживает только Maven/Gradle/Ivy. Docker, npm, PyPI, Helm – всё платное. А платное начинается со слов «свяжитесь с отделом продаж». «Хьюстон, у нас проблема!» – «Окей, мы вас вычёркиваем.»

Harbor. Контейнерный реестр – Docker-образы, Helm charts, OCI-артефакты. npm, Maven, PyPI не завезли. PostgreSQL + Redis + 2 ГБ минимум и вот это всё.

GitLab Container Registry. Попробовал – завелось. А потом начался цирк. OCI artifacts и cosign формально работают, но через обходные решения. Buildx cache? Ставь provenance=false, иначе ломается. Мультиарх? С оговорками. Каждый нестандартный сценарий опять отдельный квест. Если бы мне платили за каждый квест...

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

Как появилась NORA

У меня дома жил бурундук. Маленький, шустрый, тащит всё к себе в нору. Когда понадобилось название для реестра – ну, вы поняли.

Задача на старте была скромная: Docker Registry v2, один бинарник, без внешних сервисов. Чтобы человек сделал docker run, получил рабочий реестр и через 3 секунды пушил образы.

Первая версия заработала. Push, pull, delete, каталог, теги – спецификация покрыта. А дальше случилось то, что случается с каждым «скромным» pet-проектом.

NORA написана на Rust, а крейты нужно где-то хранить. Поднимать отдельный Cargo registry ради этого не хотелось – проще дописать поддержку прямо в NORA. Sparse index по RFC 2789, config.json, cargo publish – готово.

Два формата есть. npm, Maven, PyPI? Аппетит приходит во время еды. У каждого формата свои сюрпризы. Cargo: RFC говорит «отдавай JSON по GET». На деле – 4 паттерна URL и молчаливый «crate not found». Сидишь, разговариваешь с tcpdump о вечном. Но каждый следующий формат добавлялся быстрее – архитектура настоялась.


Сейчас поддерживается 7 типов:

Реестр

Что хранит

Путь

Docker v2 + OCI

Образы, Helm charts

/v2/

Maven

JAR, WAR, POM

/maven/

npm

Node.js-пакеты

/npm/

PyPI

Python-пакеты

/pypi/

Cargo

Rust-крейты

/cargo/

Go

Go-модули

/go/

Raw

Что угодно

/raw/

Без базы данных

Нет внешней БД. Ни PostgreSQL, ни Redis, ни Elasticsearch – ничего рядом не крутится. Метаданные лежат на файловой системе рядом с артефактами.

Небольшая команда, один CI/CD, сотни артефактов – скрипач не нужен. Файловая система на SSD справляется. Каталог образов собирается обходом директории, метаданные читаются как JSON с диска.

Не надо поднимать Postgres, создавать базу, накатывать миграции.

Консистентность обеспечивается атомарным переименованием. S3-бэкенд работает. БД и HA , когда дойдёт до enterprise. Пока отлаживаем фундамент.

nora (34 МБ, Rust, 450+ тестов)
├── /v2/       — Docker Registry v2 + OCI (образы, Helm charts)
├── /maven/    — Maven (JAR, WAR, POM)
├── /npm/      — npm (Node.js-пакеты)
├── /pypi/     — PyPI (Python-пакеты)
├── /cargo/    — Cargo (Rust-крейты, sparse index RFC 2789)
├── /go/       — Go Modules
├── /raw/      — Raw (что угодно)
├── /metrics   — Prometheus
├── /health    — K8s liveness probe
├── /ready     — K8s readiness probe
├── /api-docs  — Swagger
└── /data/     — хранилище (FS или S3)

Ноль управления через веб

Кто-то зашёл в админку Nexus, поправил настройки проксирующего репозитория, переключил layout, добавил routing rule – и ни одна живая душа не знает, что изменилось. По логам восстановить, кто вчера сломал docker-group, можно, но, честно говоря, сомнительное удовольствие.

В NORA нет админки для изменения конфигурации. Совсем. Вся настройка – переменные окружения или config.toml. Нужен новый upstream для Docker? Поменял NORA_DOCKER_UPSTREAMS, перезапустил. Изменение попадает в git, в историю деплоя, в audit trail CI/CD.

Web UI есть, но read-only: просмотр артефактов, поиск, статистика. Как говорится, рыбов показываем, но не продаём – конфигурация только через файлы и CLI.

Безопасность

Аутентификация поддерживает два механизма:

htpasswd – классика из мира Apache/nginx. Файл с хешами, работает из коробки.

API-токены с RBAC. Три роли: read, write, admin. Токены отзываемые. Утёк ключ CI-раннера – отозвал за секунду. В Nexus есть локальные роли, но для нормального RBAC всё равно тянут LDAP.

Режим anonymous read: pull без авторизации, push с токеном.

TLS вешаю на reverse proxy (nginx, Caddy, Traefik) – стандартная схема. NORA слушает чистый HTTP.

Что ещё внутри

Логи. Структурированный JSON: метод, путь, статус, время, пользователь. Льёшь в Loki, OpenSearch, ClickHouse – видишь, кто, что, когда пушил. В Nexus для этого придётся ковырять access log + audit log + парсинг на коленке.

Метрики и ops. /metrics — Prometheus из коробки. /health, /ready — K8s probes. /api-docs — Swagger. Один scrape_config в Prometheus и дашборд готов.

Rate limiting. По эндпоинтам, через переменные окружения. 50 pipeline'ов одновременно дёргают docker pull и никто не страдает.

Audit log. Лог действий в формате JSONL: кто, что, когда. Пишется на диск, изменить задним числом нельзя.

GC. nora gc --dry-run – покажет осиротевшие Docker-блобы. Без --dry-run подчистит. Для остальных форматов пока в roadmap.


Бэкап. nora backup -o backup.tar.gz – один файл, полная копия. nora restore для восстановления. Ну а бэкап Nexus – это blob store + БД + надежда, что всё сойдётся.


"Отечественные"-сборки. Docker-образы на базе Astra Linux SE и RED OS в каждом релизе. Для площадок, где без бумажки ты никто.

Цифры

NORA

Nexus OSS

Artifactory

Docker-образ

34 МБ

600+ МБ

1+ ГБ

Холодный старт

~3 сек

30-60 сек

30-60 сек

RAM

12 МБ

2-4 ГБ

2-4 ГБ

Язык

Rust

Java

Java

Внешние сервисы

0

H2 (Core) / PostgreSQL (Community/Pro)

PostgreSQL + Tomcat

Лицензия

MIT

EPL-1.0 (Core) / Proprietary (Community, Pro)

Apache 2.0 (OSS, только Maven) / Proprietary

Форматов

7

20+

30+

Под нагрузкой

VM с двумя ядрами, 4 ГБ RAM, Proxmox. docker pull образа 268 МБ за 6 секунд, docker push за 19 секунд. Push и pull одновременно не мешают друг другу. 0 ошибок, 0 таймаутов. RAM под нагрузкой в районе 250 МБ (idle 12 МБ).

Вектор развития

v0.5 (сейчас)

v1.0 (ближе к лету)

GC вручную (nora gc)

Online GC, политики хранения

Single-node

Multi-node с distributed locking

7 форматов

+ NuGet, RubyGems, Conan

htpasswd + API-токены

Управление токенами через веб

Пока нет replication, LDAP/OIDC, NuGet и RubyGems.

Поднять


docker run -d \
  -p 8080:8080 \
  -v nora-data:/data \
  getnora/nora:0.5.0

Всё. Реестр на порту 8080. Веб-интерфейс, API, все 7 реестров.

Docker login + push

docker login localhost:8080 -u admin
docker tag myapp:latest localhost:8080/myapp:latest
docker push localhost:8080/myapp:latest

npm publish

npm config set registry http://localhost:8080/npm/
npm publish

pip install

pip install --index-url http://localhost:8080/pypi/simple/ mypackage

mvn deploy

mvn deploy -DaltDeploymentRepository=nora::default::http://localhost:8080/maven/

Прокси к upstream

Не только Docker Hub. NORA проксирует запросы ко всем основным источникам: Docker Hub, npmjs.org, Maven Central, pypi.org, proxy.golang.org. Образа или пакета нет локально – NORA сходит в upstream, скачает, закеширует, отдаст. Следующий запрос уже из кеша.

docker pull localhost:8080/nginx:latest        # → Docker Hub
npm install express --registry http://localhost:8080/npm/  # → npmjs.org

Для приватных upstream предусмотрен Basic Auth через конфиг.

Закрытый контур

Типовая схема: внешний контур (DMZ) с раннером, который тянет артефакты из публичных реестров, проверяет, например, по методике ФСТЭК (целостность, подпись, сканирование и др.), и перекладывает во внутренний контур. Стандартный подход – bash-скрипт на 200 строк, у каждой команды свой велосипед.

В NORA это встроено. Две инсталляции – внешняя и внутренняя – и команда переноса:

# Внешний контур: зеркалирование из upstream
nora mirror --format docker --packages "nginx:latest,redis:7" --output ./bundle

# Раннер переносит bundle во внутренний контур

# Внутренний контур: восстановление
nora restore --input ./bundle

Зеркалирование работает для Docker, npm, PyPI, Maven, Cargo. Целостность при restore проверяется по content digest (sha256).

Где мы сейчас

- Changelog | Compatibility matrix

- В Awesome Docker

- Работает в продакшене: CI/CD сборки моих проектов, учебные кластеры на Proxmox – docker push/pull, npm, maven ежедневно

- NORA хранит собственные Docker-образы и Rust-крейт nora-registry, CI тянет зависимости через NORA как proxy к crates.io

Демо: demo.getnora.io

Исходники: github.com/getnora-io/nora

Баги и пожеланияв Issues.

Комментарии (24)


  1. chemtech
    11.04.2026 16:15

    А где ссылка на репозиторий? Думаю стоит расписать побольше о проекте


    1. ikashapov
      11.04.2026 16:15

      Похоже вот этот: https://github.com/getnora-io/nora


  1. atatarn
    11.04.2026 16:15

    Проксировать в апстрим всё, чего не нашлось локально - потенциально небезопасно. Разработчик опечатался и вот он уже тянет заботливо подложенный в апстрим mycorp-comon вместо локального mycorp-common. Приходится анализировать по маскам "похоже на внутренние - ищи локально или бросай 404".


    1. devitway_pavel Автор
      11.04.2026 16:15

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


      1. atatarn
        11.04.2026 16:15

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

        Успехов в Вашем начинании!


        1. devitway_pavel Автор
          11.04.2026 16:15

          Спасибо!
          Добавил в роудмапу.


  1. Xelld
    11.04.2026 16:15

    Отличный проект!

    Если добавите еще APT и RPM - цены вам не будет :)


    1. devitway_pavel Автор
      11.04.2026 16:15

      Спасибо за обратную связь!
      Будут, скорее всего, но чуть позже.


  1. Derevtso
    11.04.2026 16:15

    И nuget было бы приятно.


  1. TyurinAS
    11.04.2026 16:15

    Спасибо за статью, и за проект.

    Возник вопрос - как масштабировать?

    Правильно ли я понял что приложение масштабируется просто - кол-вом инстансов + rwm хранилище? Какие вообще сценарии масштабирования тестировались?


    1. devitway_pavel Автор
      11.04.2026 16:15

      RWM с несколькими инстансами не поддерживается, можете попробовать пока масштабирование чтения через S3 + CDN/кэш перед NORA.
      HA в roadmap.


      1. TyurinAS
        11.04.2026 16:15

        А производительность тестировали?


        1. devitway_pavel Автор
          11.04.2026 16:15

          Формального нагрузочного тестирования пока не проводил, вот завёл ишуйку: https://github.com/getnora-io/nora/issues/130


          1. TyurinAS
            11.04.2026 16:15

            Класс! Спасибо за ответ, буду ждать новых постов.


  1. plastid
    11.04.2026 16:15

    Пробовал неделю назад 0.4 с docker и pypi, работало всё и правда шустро.

    Документация на сайте не поспевала до актуальной версии, но в целом не проблема.

    А вот S3 (RustFS) как storage у меня совсем не взлетел, сегодня попробую ещё раз и сделаю нормальный issue


    1. devitway_pavel Автор
      11.04.2026 16:15

      Спасибо за обратную связь. Вот здесь описал то, что у вас, скорее всего, было и как настроить https://getnora.dev/ru/configuration/s3-storage/


  1. Haiden
    11.04.2026 16:15

    Я правильно понял, что эту штуку можно развернуть локально и при некоторых настройках оно будет кэшировать то, что dockerfile пытается тащить из внешних сервисов?

    Тогда бы неплохо добавить поддержку mise


    1. devitway_pavel Автор
      11.04.2026 16:15

      Всё верно вот посмотрите описание конкретного функционала
      https://getnora.dev/ru/configuration/docker-proxy/
      По mise - пока сложно сказать, посмотрите в сторону Raw.


  1. AvastON
    11.04.2026 16:15

    А возможна ли в будущем поддержка следующих форматов?
    - Terrafrom registry (hosted и proxy);
    - Ansible galaxy (proxy).


    1. devitway_pavel Автор
      11.04.2026 16:15

  1. tarkhil
    11.04.2026 16:15

    Выглядит интересно. Хоть кто-то начал делать систему не от максимального количества свистелок


  1. abagnale
    11.04.2026 16:15

    Кладбище альтернатив

    Смело вы их всех похоронили. А вот ещё из живых:

    Мы недавно себе тоже выбирали, куда сбежать от JFrog Artifactory, которые каждый год стоимость подписки увеличивали на всю большую и большую сумму. Правда, дальше Gitea особо ничего не стали смотреть, потому что на бесплатном Free / Open Source self-hosted варианте все наши форматы пакетов есть, так что про остальных кандидатов из списка ничего сказать не могу, но выглядят они вполне бодро.


    1. devitway_pavel Автор
      11.04.2026 16:15

      Спасибо за дополнение, справедливое замечание.

      Gitea - отличный выбор, если команда уже на нём сидит и форматы совпадают. Для вашего кейса (уйти от JFrog и не платить) совершенно логичное решение.

      NORA про другой сценарий: отдельный реестр без Git-сервера вокруг.
      Пара вещей, которые в Gitea пока не завезли:
      - Проксирование upstream-реестров - NORA кэширует пакеты из Docker Hub, npmjs, PyPI локально. В Gitea это открытый тикет https://github.com/go-gitea/gitea/issues/21223 с 2022 года до сих пор не реализовано.

      - Большие образы - в Gitea пуш образов больше 800 МБ падает с ошибкой 500, и это актуально для свежей v1.25.5 (https://github.com/go-gitea/gitea/issues/36945).
      В NORA ограничение только по диску.

      - Очистка мусора - multi-arch образы в Gitea никогда не удаляются, пользователи сообщают о терабайтах мусора (https://github.com/go-gitea/gitea/issues/32053). Автоочистка без тегов - открытый тикет с 2022 года (https://github.com/go-gitea/gitea/issues/21673).


      Вишенка - архитектурное решение. Реестр в Gitea - это плагин внутри монолита. Авторизация через токен в CI до сих пор ломается (https://github.com/go-gitea/gitea/issues/23642). Контейнерный реестр нельзя сделать приватным (https://github.com/go-gitea/gitea/issues/24174). В отдельном реестре этих проблем нет по определению,соответственно, падение реестра не утащит за собой Git-сервер.


      Если Gitea у вас для кода и пакеты не нагружают, тогда нет вопросов, всё ок. Но если реестр под нагрузкой в CI, закономерно, что стоит держать его отдельно. NORA стартует за 3 секунды, ест 12 МБ памяти и не роняет ваш Git-сервер.

      По остальным из списка: Cloudsmith и Buildkite - облачные сервисы, другая категория. ProGet - больше Windows/.NET мир. RepoFlow - 2 тысячи убитых енотов в год.

      Добавлю Gitea в сравнение, спасибо за наводку.

      https://alternativeto.net/software/nora-registry/


      1. abagnale
        11.04.2026 16:15

        Да, там у Gitea (и, скорее всего, у Forgejo тоже), на самом деле, есть ещё ряд проблем, как-то: нельзя указать свой PGP ключ для подписи APT/deb пакетов (генерируется новый по умолчанию и поменять его никакого API нет), какие-то косяки с видимостью Conan пакетов для разных платформ в пределах одной и той же версии пакета, и другие особенности. Ну за бесплатно можно и потерпеть, да.

        Собственно, если бы в Норе были APT/deb и Conan, я бы уже бежал ставить, волосы назад.