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

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

Свой реестр образов

Место, куда образ просто кладут: одно хранилище, две двери — читать всем, писать по паролю.

Где лежит образ, который вы ставите. Unruin не требует своего реестра — сгодится любой, включая реестр внутри Gitea. Но если хочется места, куда образ просто кладут, без гита, учётных записей и веб-интерфейса вокруг, — вот рабочая раскладка: одно хранилище, один nginx перед ним, одна папка на диске.

Читается рядом с DEPLOYMENT.md — там про сам образ и про то, как его собирает .gitea/workflows/release.yml.


Что получается

  • Читают — все и без пароля, чтобы docker pull работал у любого гостя.
  • Пишут — только по паролю, через отдельную дверь.
  • Лежит всё в одной папке; перенос — обычный cp -a, бэкап — тоже.

Ни базы, ни очередей, ни учётных записей: пароль на запись — один файл htpasswd.

Почему двери две

Реестр — это HTTP, и docker решает, слать ли пароль, по ответу на первый же запрос GET /v2/:

  • ответили 200 — клиент считает реестр открытым и пароль не пришлёт никогда, даже получив 401 на запись;
  • ответили 401 — клиент требует пароль, и анонимное чтение умирает вместе с этим.

Поэтому «читать всем, писать своим» на одном адресе не собирается. Попытка сделать это через limit_except в nginx выглядит правильной и не работает: docker login проходит, а docker push всё равно получает 401, потому что docker к этому моменту уже решил, что пароль не нужен. Так же ведёт себя и zot со встроенным htpasswd — он отвечает docker-клиенту 401 на /v2/ (различая клиентов по User-Agent: curl получает 200), и анонимное чтение ломается наглухо.

Отсюда устройство: два server в одном nginx на одно хранилище.

дверь порт кто что можно
читальная 5000 все GET и HEAD; на запись — 403
пишущая 5001 по паролю всё

Хранилище помнит имя репозитория, а не домен, через который его положили. Поэтому образ, отправленный в пишущую дверь на 127.0.0.1:5001, тянется снаружи как reg.example.org/unruin:1.0.0 — это одно и то же место.

Разложить

yaml
# docker-compose.yml
name: registry

services:
  registry:
    image: registry:3
    container_name: registry-store
    restart: unless-stopped
    environment:
      REGISTRY_STORAGE_DELETE_ENABLED: "true"   # без этого место не вернуть
    volumes:
      - ./data:/var/lib/registry
    networks: [back]

  front:
    image: nginx:alpine
    container_name: registry-front
    restart: unless-stopped
    depends_on: [registry]
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
      - ./htpasswd:/etc/nginx/htpasswd:ro
    ports:
      - "127.0.0.1:5001:5001"      # пишущая дверь — только с этой машины
    networks: [back, npm]

networks:
  back:
  npm:
    external: true
    name: npm_default              # сеть вашего обратного прокси

Существенное в nginx.conf — два server и предел размера тела:

nginx
client_max_body_size 0;            # слои приходят одним запросом
proxy_request_buffering off;
upstream registry { server registry:5000; }

server {                            # читальная
  listen 5000;
  location / {
    limit_except GET HEAD OPTIONS { deny all; }
    proxy_pass http://registry;
  }
}

server {                            # пишущая
  listen 5001;
  auth_basic           "push";
  auth_basic_user_file /etc/nginx/htpasswd;
  location / { proxy_pass http://registry; }
}

Пароль и запуск:

sh
docker run --rm httpd:2-alpine htpasswd -Bbn ci 'придумайте-пароль' > htpasswd
chmod 644 htpasswd     # nginx внутри контейнера читает файл не от root
docker compose up -d

Права важнее, чем кажется: с 600 nginx не откроет файл и ответит на docker login пятисотой, а не «неверный пароль». Внутри лежит bcrypt-хэш, не пароль.

На обратном прокси заведите узел на читальную дверь: домен, схема http, адрес registry-front, порт 5000, сертификат — и обязательно client_max_body_size 0 в его собственной конфигурации. Без этого прокси обрежет слой на своём пределе, и отправка развалится на середине большого слоя. Наружу торчит только чтение: публичного адреса на запись нет вовсе.

Положить образ

sh
echo 'пароль' | docker login 127.0.0.1:5001 -u ci --password-stdin
docker tag unruin:latest 127.0.0.1:5001/unruin:1.0.0
docker push 127.0.0.1:5001/unruin:1.0.0

Снаружи он теперь тянется обычным docker pull reg.example.org/unruin:1.0.0 — то самое имя, которое стоит в вашем docker-compose.yml и в блоке установки на сайте.

Выпуск из CI с другой машины

Тогда пишущей двери нужен адрес: второй узел на прокси, push.reg.example.org на registry-front:5001, с тем же пределом размера тела и обязательно с TLS — пароль ходит открытым текстом в заголовке.

yaml
- uses: docker/login-action@v3
  with:
    registry: push.reg.example.org
    username: ci
    password: ${{ secrets.REGISTRY_PASSWORD }}

Толкать в push.reg.example.org/unruin:1.0.0, тянуть из reg.example.org/unruin:1.0.0.

Убрать мусор

Удаление тега не освобождает место: слои остаются, пока их не соберут.

sh
curl -s https://reg.example.org/v2/_catalog
curl -s https://reg.example.org/v2/unruin/tags/list

# удалять — по digest и через пишущую дверь
D=$(curl -sI -H 'Accept: application/vnd.oci.image.manifest.v1+json' \
      https://reg.example.org/v2/unruin/manifests/1.0.0 |
    tr -d '\r' | awk '/[Dd]ocker-[Cc]ontent-[Dd]igest/{print $2}')
curl -u ci -X DELETE "http://127.0.0.1:5001/v2/unruin/manifests/$D"

docker exec registry-store registry garbage-collect \
  /etc/distribution/config.yml --delete-untagged

Что помнить

  • Читальная дверь публичная. Кто угадает имя репозитория, тот скачает образ. Секретов внутрь образов не класть — это верно и без реестра, но здесь особенно. Образ Unruin секретов не содержит: всё подаётся окружением и хранилищем секретов, см. DEPLOYMENT.md.
  • Бэкап реестра — это папка data и файл htpasswd. Она же — очевидный кандидат в задание Unruin: источник files, и реестр перестаёт быть тем единственным местом, где живут ваши образы.
  • Пароль ходит в заголовке. Пишущая дверь наружу — только через TLS.
  • Смена реестра меняет имя образа, а оно вписано не в одно место: пример docker-compose.yml, docs/DEPLOYMENT.md, страница установки на сайте, examples/ и оба рабочих процесса Gitea. Меняйте разом.

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