Приватный Docker Registry в 2026: развёртывание, хранение на NAS и ZFS, интеграция с Kubernetes | AdminWiki

Приватный Docker Registry в 2026: развёртывание, хранение на NAS и ZFS, интеграция с Kubernetes

11 сентября 2026 17 мин. чтения
Содержание статьи

Рабочий реестр образов поднимается за 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 HubDocker Registry 2.x/3.xHarbor
Аутентификацияаккаунты сервисаhtpasswd, token-сервис, mTLS через reverse proxyLDAP, OIDC, RBAC
Веб-интерфейсестьнетесть
Сканирование уязвимостейна платных тарифахнет, подключается извнеTrivy из коробки
Потребление ресурсоввнешний сервис1 vCPU, 512 МБ RAM2 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 и сеть 10GbE300-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)Что ограничивает
Локальный NVMe900 МБ/с1000+ МБ/сCPU при сжатии слоёв
ZFS на SATA SSD, lz4500-800 МБ/с600-900 МБ/сскорость сжатия и ARC
NFS по 10GbE300-600 МБ/с400-700 МБ/скэш и диски NAS
NFS по 1GbE100-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 authorityCA отсутствует в trust store клиента или kubeletopenssl 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 перед запуском; метрики и алерт по свободному месту; проверенное восстановление из бэкапа; описанный порядок перевода реестра в режим только чтения на время обслуживания.

Поделиться:
Сохранить гайд? В закладки браузера