Рабочий реестр образов поднимается за 15 минут: контейнер registry:3, файл config.yml, каталог на ZFS-датасете или NFS-шаре, Nginx с TLS-сертификатом и htpasswd-аутентификацией. Основные сложности начинаются дальше: права на запись в шару, безопасный запуск garbage collection и настройка kubelet, который должен скачивать образы без ручного docker login.
Ниже полный маршрут от выбора бэкенда до работающего в продакшене реестра: конфигурации, команды, цифры по производительности и таблица ошибок с диагностикой. Конфигурации проверены на Distribution 3.x и 2.8, Kubernetes 1.29+ и containerd 1.7+/2.x.
Зачем нужен приватный Docker Registry и какой сценарий выбрать
Приватный реестр закрывает четыре задачи: хранит сборки с закрытым кодом, ускоряет загрузку внутри сети, изолирует контур от публичных реестров и становится единой точкой контроля доступа. Docker Registry 2.x (актуальная ветка Distribution 3.x) реализует Docker Registry HTTP API V2 и почти ничего сверх этого: ни веб-интерфейса, ни ролей, ни сканирования. Минимализм оплачивается предсказуемостью: 30-50 МБ RAM в простое, отсутствие внешней базы данных, вся конфигурация в одном YAML.
Минимальный комплект: образ registry:3, config.yml, каталог хранения, TLS-сертификат и файл htpasswd. Этого хватает, чтобы команда из 20 человек собирала и раздавала образы, а kubelet в Kubernetes 1.29+ скачивал их без выхода в интернет.
Чем self-hosted Registry отличается от Docker Hub и Harbor
Docker Hub удобен для публичных образов и плохо подходит как единственное хранилище продакшен-сборок: бесплатный тариф ограничивает число pull с одного IP, платный стоит денег, данные лежат на чужой инфраструктуре. Harbor добавляет поверх Registry проекты, RBAC, LDAP/OIDC, репликацию и сканирование Trivy, но требует PostgreSQL, Redis и 2-4 ГБ RAM.
| Критерий | Docker Hub | Docker Registry 2.x/3.x | Harbor |
|---|---|---|---|
| Аутентификация | аккаунты сервиса | htpasswd, token-сервис, mTLS через reverse proxy | LDAP, OIDC, RBAC |
| Веб-интерфейс | есть | нет | есть |
| Сканирование уязвимостей | на платных тарифах | нет, подключается извне | Trivy из коробки |
| Потребление ресурсов | внешний сервис | 1 vCPU, 512 МБ RAM | 2 vCPU, 4 ГБ RAM, база данных |
| Политики хранения | нет | скрипты и cron | правила в интерфейсе |
| Стоимость владения | подписка | только инфраструктура | инфраструктура и обслуживание |
Выбор сводится к двум сценариям: нужны роли, аудит и сканирование для большой команды, берите Harbor; нужен быстрый реестр на 1-5 ТБ с минимумом движущихся частей, берите Docker Registry. Сравнение всех трёх вариантов с требованиями к продакшену разобрано в отдельной статье про развёртывание Docker Registry, Harbor и GitLab Container Registry.
Критерии выбора хранилища: локальный диск, NAS, ZFS, S3
Бэкенд определяет скорость push, стоимость бэкапа и частоту запуска garbage collection. На практике встречаются четыре варианта:
- Локальный диск (ext4 или XFS). Минимальная задержка записи, простая настройка. Отказоустойчивости нет: выход диска из строя означает потерю всех образов, если не настроены снапшоты.
- NAS с NFS или SMB. Данные живут на отдельном устройстве, объём расширяется добавлением дисков, бэкап уже настроен администратором. NFS предпочтительнее SMB: у SMB бывают проблемы с правами и блокировками на мелких файлах манифестов.
- ZFS-датасет. Контрольные суммы блоков, мгновенные снапшоты, сжатие lz4, настраиваемый recordsize. Работает локально или как датасет на TrueNAS, отданный по NFS или iSCSI.
- S3-совместимое хранилище (MinIO, Ceph RGW, облачный S3). Слои пишутся через multipart upload, доступны репликация и erasure coding, объём масштабируется горизонтально.
Если вы выбираете бэкенд под запрос «docker registry S3 хранилище», учитывайте: S3 не даёт снапшотов уровня файловой системы, а версионирование бакета заменяет их лишь частично и оплачивается хранением всех версий объектов.
| Вариант | Стоимость | Скорость push/pull | Надёжность | Сложность |
|---|---|---|---|---|
| Локальный NVMe | цена диска | 800-1000 МБ/с | RAID или ничего | низкая |
| NFS на NAS | цена NAS и сеть 10GbE | 300-600 МБ/с | зависит от NAS | низкая |
| ZFS-датасет | диски плюс 1 ГБ RAM на 1 ТБ | 500-800 МБ/с на SSD | контрольные суммы, снапшоты | средняя |
| S3 (MinIO, Ceph) | от трёх узлов | 200-500 МБ/с | репликация, erasure coding | высокая |
Практическое правило: команда до 20 человек и объём 1-5 ТБ - ZFS-датасет на локальных SSD или на TrueNAS. Несколько площадок и требование хранить данные отдельно от вычислений - S3-совместимое хранилище. Когда собственное железо держать не хочется, реестр и бакет проще разместить в облаке, например в Timeweb Cloud, где доступны серверы, объектное хранилище и Kubernetes.
Развёртывание Docker Registry: базовая конфигурация и TLS
Развёртывание укладывается в пять шагов: каталоги, config.yml, htpasswd, контейнер, Nginx с сертификатом. Последовательность ниже проверена на Ubuntu 24.04 и Debian 13.
mkdir -p /srv/registry/{data,auth}
docker pull registry:3
Конфигурация config.yml: storage, auth, http
Файл /srv/registry/config.yml. Параметр delete: enabled: true обязателен: без него API не даст удалить манифест, и garbage collection останется без работы.
version: 0.1
log:
level: info
fields:
service: registry
storage:
filesystem:
rootdirectory: /var/lib/registry
delete:
enabled: true
cache:
blobdescriptor: inmemory
maintenance:
uploadpurging:
enabled: true
age: 168h
interval: 24h
http:
addr: :5000
headers:
X-Content-Type-Options: [nosniff]
health:
storagedriver:
enabled: true
interval: 10s
auth:
htpasswd:
realm: registry
path: /etc/docker/registry/auth/htpasswd
Значение каждого ключа: rootdirectory задаёт каталог внутри контейнера, cache: blobdescriptor: inmemory ускоряет проверку уже загруженных слоёв, uploadpurging удаляет незавершённые загрузки старше 168 часов (защита от мусора, остающегося после обрыва push), health: storagedriver включает проверку бэкенда по адресу /debug/health.
Для S3 бэкенда секция storage выглядит иначе:
storage:
s3:
accesskey: REGISTRY_S3_KEY
secretkey: REGISTRY_S3_SECRET
region: ru-1
bucket: registry-prod
rootdirectory: /registry
v4auth: true
chunksize: 5242880
multipartcopychunksize: 33554432
multipartcopymaxconcurrency: 100
multipartcopythresholdsize: 33554432
delete:
enabled: true
Учётные данные S3 не храните в открытом виде: контейнер принимает их через переменные окружения REGISTRY_STORAGE_S3_ACCESSKEY и REGISTRY_STORAGE_S3_SECRETKEY, а в config.yml остаются только bucket, region и параметры multipart. chunksize 5 МБ и multipartcopythresholdsize 32 МБ дают заметный прирост скорости копирования больших слоёв.
Настройка TLS и reverse proxy на Nginx
server {
listen 443 ssl;
http2 on;
server_name registry.example.com;
ssl_certificate /etc/letsencrypt/live/registry.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/registry.example.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
client_max_body_size 0;
chunked_transfer_encoding on;
location / {
proxy_pass http://127.0.0.1:5000;
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_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 900;
proxy_request_buffering off;
}
}
client_max_body_size 0 снимает ограничение на размер образа, proxy_request_buffering off не даёт Nginx копить тело запроса на диске (иначе push образа на 5 ГБ съест место в /var/lib/nginx), proxy_read_timeout 900 выручает при медленном S3-бэкенде. Сертификат Let's Encrypt выпускается через certbot; во внутреннем контуре чаще используют self-signed или корпоративный CA. Требование к client_max_body_size и валидности сертификата жёсткое: kubelet и containerd доверяют только CA из своего trust store, иначе pull падает с ошибкой x509: certificate signed by unknown authority.
Аутентификация через htpasswd и проверка доступа
htpasswd -Bbn deploy 'S3cr3t-P@ss' > /srv/registry/auth/htpasswd chmod 640 /srv/registry/auth/htpasswd docker run -d --name registry --restart always \ -p 127.0.0.1:5000:5000 \ -v /srv/registry/config.yml:/etc/docker/registry/config.yml:ro \ -v /srv/registry/auth:/etc/docker/registry/auth:ro \ -v /srv/registry/data:/var/lib/registry \ registry:3
Порт публикуется только на 127.0.0.1: наружу реестр смотрит через Nginx. Проверка доступа:
curl -u deploy:'S3cr3t-P@ss' https://registry.example.com/v2/ docker login registry.example.com
Ответ {} с кодом 200 означает, что Basic Auth работает. Код 401 при верных логине и пароле указывает на неверный путь к htpasswd или отсутствие секции auth в config.yml. Файл читается один раз при старте контейнера: после правки конфигурации нужен docker restart registry.
Хранение образов на NAS и в ZFS-датасете
Путь к данным задаётся в config.yml, всё остальное определяется тем, что смонтировано в /var/lib/registry. Два сценария различаются тем, где живёт файловая система: на NAS или на локальном пуле ZFS.
Монтирование NFS-шары и права доступа
Строка в /etc/fstab:
nas.example.com:/export/registry /mnt/registry nfs vers=4.2,hard,noatime,_netdev,rsize=1048576,wsize=1048576 0 0
Опции vers=4.2 и hard дают стабильную семантику ввода-вывода: операция не завершается ошибкой при кратком разрыве сети, клиент дожидается восстановления. noatime убирает лишние записи метаданных при чтении слоёв, rsize и wsize по 1 МБ снижают накладные расходы на RPC.
Основная причина ошибок прав - root squash на стороне NAS. Официальный образ registry работает от root (UID 0), и при включённом root_squash сервер подменяет его на nobody, поэтому запись в шару падает с permission denied. Рабочие решения:
- запустить контейнер от непривилегированного пользователя: docker run --user 1000:1000, а на шаре заранее выполнить chown 1000:1000 для каталога реестра;
- настроить экспорт с all_squash,anonuid=1000,anongid=1000, тогда все клиенты пишут от одного UID;
- включить no_root_squash для конкретного экспорта. Вариант простой и самый рискованный: любой клиент с root получит root на шаре.
Проверка перед запуском реестра:
sudo -u '#1000' touch /mnt/registry/.probe && ls -la /mnt/registry/.probe
Файл создался - права настроены, пробный файл можно удалить. Отдельно проверьте, что NFS-клиент переживает перезагрузку NAS: после отключения экспорта операции начинают возвращать stale file handle (ESTALE), и реестр отвечает 500 на push до перемонтирования.
Создание и тюнинг ZFS-датасета под Registry
zfs create \ -o recordsize=1M \ -o compression=lz4 \ -o atime=off \ -o logbias=throughput \ tank/registry zfs set xattr=sa tank/registry zfs set snapdir=hidden tank/registry
recordsize=1M рассчитан на крупные слои: образы пишутся длинными последовательными потоками, и большой блок сокращает число операций с метаданными. Мелкие файлы манифестов датасет не раздувают: ZFS хранит их в блоках подходящего размера, recordsize задаёт только верхнюю границу. compression=lz4 экономит 1.3-2x на слоях с текстовыми данными: исходники, node_modules, jar-архивы, JSON-конфиги. logbias=throughput снижает давление на ZIL и подходит для последовательной записи.
Снапшоты по расписанию и защита от случайного удаления:
zfs snapshot tank/registry@daily-2026-09-11 zfs hold keep tank/registry@daily-2026-09-11 zfs list -t snapshot -o name,used,refer tank/registry
Флаг hold запрещает zfs destroy -r удалить снапшот, пока не выполнен zfs release. Рабочая политика: 7 ежедневных, 4 недельных и 12 месячных снапшотов плюс отдельная задача с zfs send на резервный пул. В docker run датасет монтируется тем же томом: -v /tank/registry:/var/lib/registry.
Сравнение производительности: локальный диск, NFS, ZFS, S3
Цифры получены на слое около 250 МБ с одного клиента. При десяти параллельных push от раннеров разрыв между бэкендами сужается, но порядок сохраняется.
| Бэкенд | Запись (push) | Чтение (pull) | Что ограничивает |
|---|---|---|---|
| Локальный NVMe | 900 МБ/с | 1000+ МБ/с | CPU при сжатии слоёв |
| ZFS на SATA SSD, lz4 | 500-800 МБ/с | 600-900 МБ/с | скорость сжатия и ARC |
| NFS по 10GbE | 300-600 МБ/с | 400-700 МБ/с | кэш и диски NAS |
| NFS по 1GbE | 100-110 МБ/с | 110 МБ/с | пропускная способность сети |
| S3 (MinIO, три узла) | 200-500 МБ/с | 300-600 МБ/с | multipart и задержка HTTP |
Для CI/CD критична скорость записи: сборка из десяти образов по 400 МБ через NFS по 1GbE тратит минуты только на передачу. Для Kubernetes важнее чтение, особенно когда 30 подов одновременно тянут обновлённый базовый образ. Здесь помогает не столько бэкенд, сколько кэш слоёв на узлах и одинаковые теги, не заставляющие перекачивать данные заново.
Если задача в том, чтобы ужать сами образы и снизить нагрузку на хранилище, приёмы многоэтапной сборки, минимальных базовых образов и автоочистки разобраны в материале про сокращение размера образов и автоматическую очистку реестра.
Garbage collection: очистка неиспользуемых слоёв
Удаление манифеста через API не освобождает место. Манифест исчезает из выдачи, его слои остаются в каталоге blobs до запуска garbage collection. Порядок такой: удалить манифесты, перевести реестр в режим только чтения, запустить сборщик.
Удаление манифестов через API
DELETE принимает digest, а не тег. Получить digest можно из заголовка ответа на HEAD-запрос:
curl -sI -u deploy:'S3cr3t-P@ss' \
-H 'Accept: application/vnd.docker.distribution.manifest.v2+json' \
https://registry.example.com/v2/team/api/manifests/v1.4.2 | grep -i docker-content-digest
curl -s -o /dev/null -w '%{http_code}\n' -u deploy:'S3cr3t-P@ss' -X DELETE \
-H 'Accept: application/vnd.docker.distribution.manifest.v2+json' \
https://registry.example.com/v2/team/api/manifests/sha256:9f1c8d0e5a7b4c1f
Код 202 означает, что манифест помечен удалённым. Тег может оставаться в списке /v2/_catalog до следующего прохода сборщика, это ожидаемое поведение. Массовое удаление по политике (например, всё старше 90 дней) удобнее делать скриптом с обходом каталога; готовые сценарии разобраны в руководстве по управлению образами в реестре и удалению манифестов.
Запуск garbage-collect и типовые ошибки
Во время сборки реестр должен быть недоступен для записи. В Distribution 3.x для этого есть штатный режим только чтения:
storage:
maintenance:
readonly:
enabled: true
delete:
enabled: true
После перезапуска push возвращает 503, pull продолжает работать. В ветке 2.8 такого режима нет: контейнер останавливают целиком. Дальше прогон без удаления и рабочий запуск:
docker exec -i registry /bin/registry garbage-collect --dry-run /etc/docker/registry/config.yml docker exec -i registry /bin/registry garbage-collect --delete-untagged /etc/docker/registry/config.yml
Флаг --delete-untagged удаляет манифесты без тегов, то есть те, что остались после DELETE через API. Без него сборщик освобождает только реально потерянные слои. Еженедельная задача в cron:
0 4 * * 0 /usr/bin/docker exec -i registry /bin/registry garbage-collect --delete-untagged /etc/docker/registry/config.yml >> /var/log/registry-gc.log 2>&1
Что ломается чаще всего:
- blob unknown. Сборщик нашёл манифест со ссылкой на отсутствующий слой. Причины: параллельная запись во время предыдущего GC, неполный бэкап, обрыв push. Лечится удалением битого манифеста и повторной сборкой образа.
- read-only file system. Реестр не переведён в режим только чтения и во время GC принял push, из-за чего часть слоёв осталась неучтённой. Сначала readonly или остановка контейнера, потом сборка.
- Долгий прогон. На 2 ТБ данных и 200 тысячах blob сбор занимает 40-90 минут. Заранее проверяйте --dry-run и планируйте окно обслуживания.
- Пустые каталоги. GC не удаляет дерево /docker/registry/v2/repositories/team/api/_manifests. Место они почти не занимают, но раздувают вывод df -i при большом числе файлов.
Полный сценарий от политики хранения до метрик Prometheus описан в статье про очистку реестра образов и удаление неиспользуемых данных.
Интеграция с Kubernetes: доступ к приватному реестру
Работают два способа: секрет с учётными данными в namespace или настройка containerd на узлах. Первый переносим между кластерами, второй избавляет от секретов в манифестах, но требует доступа к конфигурации узлов.
Создание imagePullSecret и использование в Pod
kubectl create secret docker-registry regcred \ --docker-server=registry.example.com \ --docker-username=deploy \ --docker-password='S3cr3t-P@ss' \ --docker-email=devops@example.com \ -n production
apiVersion: apps/v1
kind: Deployment
metadata:
name: api
namespace: production
spec:
replicas: 3
selector:
matchLabels:
app: api
template:
metadata:
labels:
app: api
spec:
imagePullSecrets:
- name: regcred
containers:
- name: api
image: registry.example.com/team/api:v1.4.2
ports:
- containerPort: 8080
Секрет можно привязать к ServiceAccount, тогда imagePullSecrets не нужен в каждом манифесте:
apiVersion: v1 kind: ServiceAccount metadata: name: default namespace: production imagePullSecrets: - name: regcred
Секреты типа docker-registry в etcd по умолчанию не шифруются. Если в них лежат пароли от продакшен-реестра, включите шифрование секретов на уровне кластера, иначе дамп etcd раскроет учётные данные.
Настройка containerd для работы с приватным реестром
С containerd 1.7 основной путь - каталог certs.d. Файл /etc/containerd/certs.d/registry.example.com/hosts.toml:
server = 'https://registry.example.com'
[host.'https://registry.example.com']
capabilities = ['pull', 'resolve']
ca = '/etc/containerd/certs.d/registry.example.com/ca.crt'
[host.'https://registry.example.com'.header]
authorization = 'Basic ZGVwbG95OlMzY3IzdC1QQHNz'
Значение authorization собирается из base64 от строки логин:пароль: printf 'deploy:S3cr3t-P@ss' | base64. В /etc/containerd/config.toml остаётся указать каталог с настройками:
[plugins.'io.containerd.cri.v1.images'.registry] config_path = '/etc/containerd/certs.d'
В containerd 1.7 секция называется [plugins.'io.containerd.grpc.v1.cri'.registry], а параметр insecure_skip_verify = false в блоке tls явно требует проверки сертификата. После правки: systemctl restart containerd, затем проверка на узле:
crictl pull registry.example.com/team/api:v1.4.2 journalctl -u containerd -n 50 --no-pager
Образ скачался - kubelet тоже его получит, он использует тот же CRI-плагин и не требует imagePullSecrets. Параметр insecure_skip_verify = true отключает проверку сертификата и открывает дорогу подмене трафика; применяйте его только в тестовом контуре без доверенного CA.
Интеграция с CI/CD: сборка и публикация образов
Пайплайн отличается от локальной сборки способом передачи учётных данных. Пароли держите в секретах CI: docker login с паролем в командной строке оставляет его в истории shell и в логах задания.
Пример пайплайна для GitLab CI
stages: [build, push]
variables:
IMAGE: registry.example.com/team/api
DOCKER_HOST: tcp://docker:2375
DOCKER_TLS_CERTDIR: ''
build-and-push:
stage: build
image: docker:27-cli
services:
- name: docker:27-dind
command: ['--tls=false']
before_script:
- echo "$REGISTRY_PASSWORD" | docker login -u "$REGISTRY_USER" --password-stdin registry.example.com
script:
- docker build -t $IMAGE:$CI_COMMIT_SHORT_SHA -t $IMAGE:latest .
- docker push $IMAGE:$CI_COMMIT_SHORT_SHA
- docker push $IMAGE:latest
rules:
- if: $CI_COMMIT_BRANCH == 'main'
Переменные REGISTRY_USER и REGISTRY_PASSWORD задаются в настройках проекта как masked. Для self-signed сертификата положите CA на runner в /etc/docker/certs.d/registry.example.com/ca.crt и перезапустите docker; внутри dind сертификат придётся смонтировать отдельным томом, иначе login завершится ошибкой x509.
Пример workflow для GitHub Actions
name: build
on:
push:
branches: [main]
jobs:
image:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
registry: registry.example.com
username: ${{ secrets.REGISTRY_USERNAME }}
password: ${{ secrets.REGISTRY_PASSWORD }}
- uses: docker/build-push-action@v6
with:
context: .
push: true
tags: registry.example.com/team/api:${{ github.sha }}
cache-from: type=registry,ref=registry.example.com/team/api:cache
cache-to: type=registry,ref=registry.example.com/team/api:cache,mode=max
Оба варианта требуют Docker daemon. Альтернативы без daemon: Kaniko собирает образ внутри обычного контейнера без привилегий, BuildKit (buildkitd или buildx с драйвером remote) даёт кэш между запусками и параллельную сборку стадий. Если политика запрещает privileged-контейнеры, Kaniko остаётся самым простым выбором для GitLab.
В пайплайн иногда добавляют генерацию release notes и разбор diff через LLM. Ключи к моделям удобно держать в одном шлюзе, например AiTunnel с единым API к GPT, Gemini и Claude.
Типовые ошибки и способы их устранения
Большая часть сбоев сводится к семи симптомам. Таблица связывает симптом с причиной и действием.
| Симптом | Причина | Что сделать |
|---|---|---|
| 401 Unauthorized при docker login | путь к htpasswd в config.yml не совпадает с томом, запись создана без флага -B | проверить секцию auth, пересоздать файл через htpasswd -Bbn, перезапустить контейнер |
| x509: certificate signed by unknown authority | CA отсутствует в trust store клиента или kubelet | openssl s_client -connect registry.example.com:443 -servername registry.example.com, затем положить CA в /etc/docker/certs.d и /etc/containerd/certs.d |
| connection refused | контейнер не запущен, порт 5000 не слушается, Nginx не проксирует | docker ps, ss -lntp | grep 5000, docker logs registry |
| denied: requested access to the resource is denied | неверное имя репозитория, реестр в режиме readonly | сверить путь образа и содержимое секции maintenance |
| blob unknown to registry | повреждённый или частично удалённый манифест | удалить манифест через API, пересобрать и запушить образ |
| no space left on device | слои копятся, GC не запускается, остались незавершённые загрузки | прогнать garbage-collect, проверить uploadpurging и df -h |
| manifest unknown | тег не существует или манифест удалён, в манифесте старая версия | посмотреть /v2/_catalog и список тегов через API |
Ошибки аутентификации и TLS
Код 401 при верных данных чаще всего означает расхождение пути: в config.yml указан /etc/docker/registry/htpasswd, а том смонтирован в /etc/docker/registry/auth. Проверьте формат файла: строки вида deploy:$2y$05$... с bcrypt-хешем, а не открытый пароль. Код 403 от S3-бэкенда (Access Denied) к аутентификации реестра отношения не имеет: это отказ объектного хранилища по ключам или политике бакета.
Диагностика сертификата:
openssl s_client -connect registry.example.com:443 -servername registry.example.com -showcerts /dev/null | openssl x509 -noout -subject -dates -ext subjectAltName
В subjectAltName должно быть точное имя хоста, которое используют docker и containerd. Сертификат на IP-адрес не подойдёт: клиенты обращаются по DNS-имени и проверяют совпадение. Истёкший сертификат даёт ту же ошибку x509, поэтому срок действия стоит выводить в мониторинг.
Ошибки хранилища и garbage collection
no space left on device приходит двумя путями: кончилось место (df -h) или исчерпаны inode (df -i). Второе типично для ZFS-датасета с миллионами мелких файлов манифестов и для NFS-шары со множеством снапшотов. Перед очисткой полезно понять, что именно занимает диск: реестр, логи Docker, снапшоты или осиротевшие тома. Методика разбора описана в материале про поиск утечек дискового пространства на серверах с Docker и ZFS.
read-only file system в логах реестра означает, что файловая система ушла в режим только чтения: типично для ZFS при ошибках записи или для NFS при потере экспорта. Проверьте zpool status и доступность шары, затем перезапустите контейнер. Оценка объёма перед сборкой:
du -sh /var/lib/registry/docker/registry/v2/blobs docker exec -i registry /bin/registry garbage-collect --dry-run /etc/docker/registry/config.yml | tail -5
Чек-лист проверки после развёртывания: реестр отвечает 200 на /v2/ с учётными данными; push и pull образа проходят с внешнего хоста; kubelet скачивает образ; garbage-collect с --dry-run завершается без строк blob unknown; снапшот хранилища снимается и восстанавливается на тестовом стенде.
Эксплуатация и безопасность в продакшене
Docker Registry не умеет разграничивать права: любой пользователь htpasswd может писать во все репозитории. Разграничение делают на уровне reverse proxy, отдельными реестрами на команду или переходом на Harbor. Остальные меры касаются бэкапов, наблюдаемости и защиты периметра.
Бэкап и восстановление реестра
Для ZFS бэкап сводится к снапшоту и отправке инкремента на резервный пул:
zfs snapshot tank/registry@backup-2026-09-11 zfs send -i tank/registry@backup-2026-09-10 tank/registry@backup-2026-09-11 | ssh backup-host zfs receive backup/registry
Для NFS и локального диска работает rsync с сохранением прав:
rsync -a --delete /mnt/registry/ /backup/registry/
Порядок восстановления: остановить реестр, вернуть данные, запустить контейнер, прогнать garbage-collect --dry-run и убедиться, что ссылки целы, только после этого открывать push. Снапшот ZFS даёт согласованное состояние файловой системы на момент снятия, но не гарантирует консистентность незавершённых загрузок: uploadpurging уберёт их при следующем старте. Бэкап без проверки восстановления бесполезен, поэтому раз в квартал разворачивайте его на тестовом стенде.
Мониторинг и healthcheck
http:
debug:
prometheus:
enabled: true
path: /metrics
Метрики доступны на /metrics и собираются стандартным scrape-заданием Prometheus. Полезные серии: registry_http_requests_total (по кодам и хендлерам), registry_http_request_duration_seconds (гистограмма задержек push и pull), registry_storage_action_seconds (работа бэкенда), плюс метрики рантайма go_gc_duration_seconds. Алерты разумно ставить на рост доли ответов 5xx, на p95 задержки storage и на свободное место в датасете ниже 15%.
Healthcheck опирается на /v2/: эндпоинт отвечает 200 без аутентификации и не требует данных. В Kubernetes это liveness-проба для самого реестра, если он развёрнут в кластере:
livenessProbe:
httpGet:
path: /v2/
port: 5000
initialDelaySeconds: 10
periodSeconds: 30
readinessProbe:
httpGet:
path: /v2/
port: 5000
Защита периметра складывается из ограничения доступа по IP на Nginx (allow и deny), rate limiting через limit_req_zone на уровне server, отключения листинга каталога (/v2/_catalog) для внешних адресов и регулярной ротации паролей htpasswd. Для больших команд с потребностью в ролях и аудите разумнее Harbor или GitLab Container Registry; Docker Registry оставляйте там, где нужны минимальные зависимости и предсказуемое поведение.
Финальный чек-лист продакшен-готовности: TLS с валидным сертификатом и автоматическим продлением; htpasswd с bcrypt; delete: enabled: true в конфигурации; хранилище на ZFS-датасете со снапшотами или на реплицируемом S3; расписание garbage-collect с --dry-run перед запуском; метрики и алерт по свободному месту; проверенное восстановление из бэкапа; описанный порядок перевода реестра в режим только чтения на время обслуживания.