Эксплуатация
Свой реестр образов
Место, куда образ просто кладут: одно хранилище, две двери — читать всем, писать по паролю.
Где лежит образ, который вы ставите. 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 — это одно и то же место.
Разложить
# 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 и предел размера тела:
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; }
}
Пароль и запуск:
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 в его собственной конфигурации. Без этого прокси обрежет слой на своём пределе,
и отправка развалится на середине большого слоя. Наружу торчит только чтение:
публичного адреса на запись нет вовсе.
Положить образ
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 —
пароль ходит открытым текстом в заголовке.
- 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.
Убрать мусор
Удаление тега не освобождает место: слои остаются, пока их не соберут.
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. Меняйте разом.