Self-hosted Docker registry в Kubernetes: настройка, аутентификация и очистка

Published: 2026-03-10

Self-hosted registry даёт полный контроль над доступностью образов и устраняет ограничения pull-rate DockerHub. Мы запускаем официальный registry:2 на голом NodePort — просто, без внешних зависимостей, работает в air-gapped установках.


Почему self-hosted

  • Air-gapped среды — production-ВМ без интернета тянут только из внутреннего реестра
  • Ограничения pull rate — DockerHub ограничивает анонимные pull до 100/6ч на IP
  • Задержка — .NET-образ на 500 МБ из DockerHub тянется минуты, из локального реестра — секунды
  • Аудит — вы сами решаете, какие образы доступны; никаких неожиданных удалений
  • Стоимость — нет оплаты за пользователей или объём хранения

Развёртывание

yamlapiVersion: apps/v1
kind: Deployment
metadata:
  name: registry
  namespace: registry
spec:
  replicas: 1
  selector:
    matchLabels:
      app: registry
  template:
    metadata:
      labels:
        app: registry
    spec:
      containers:
        - name: registry
          image: registry:2
          ports:
            - containerPort: 5000
          env:
            - name: REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY
              value: /var/lib/registry
            - name: REGISTRY_HTTP_SECRET
              valueFrom:
                secretKeyRef:
                  name: registry-secret
                  key: http-secret
            - name: REGISTRY_STORAGE_DELETE_ENABLED
              value: "true"
          volumeMounts:
            - name: registry-data
              mountPath: /var/lib/registry
          resources:
            requests:
              cpu: 100m
              memory: 128Mi
      volumes:
        - name: registry-data
          persistentVolumeClaim:
            claimName: registry-pvc
---
apiVersion: v1
kind: Service
metadata:
  name: registry
  namespace: registry
spec:
  type: NodePort
  selector:
    app: registry
  ports:
    - port: 5000
      targetPort: 5000
      nodePort: 30500

NodePort 30500 доступен с любой машины, которая может достучаться до ноды. REGISTRY_STORAGE_DELETE_ENABLED: "true" включает API удаления манифестов — необходимо для работы garbage collection.


Постоянное хранилище

yamlapiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: registry-pvc
  namespace: registry
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 50Gi
  storageClassName: local-path

Для продакшна можно использовать S3-совместимое хранилище:

yamlenv:
  - name: REGISTRY_STORAGE
    value: s3
  - name: REGISTRY_STORAGE_S3_BUCKET
    value: my-registry-bucket

Настройка Docker для HTTP-реестра

Docker отклоняет HTTP-реестры по умолчанию:

json// /etc/docker/daemon.json
{
  "insecure-registries": ["192.168.1.10:30500"]
}
bashsudo systemctl restart docker

Push образов

bashdocker tag myapp:latest 192.168.1.10:30500/myapp:latest
docker push 192.168.1.10:30500/myapp:latest

# Проверить теги
curl -s http://192.168.1.10:30500/v2/myapp/tags/list | jq .

Pull из реестра в Kubernetes-подах

yamlcontainers:
  - name: app
    image: 192.168.1.10:30500/myapp:abc1234
    imagePullPolicy: Always

Если нода обращается к реестру без аутентификации, imagePullSecret не нужен. Для защищённых реестров:

bashkubectl create secret docker-registry registry-creds \
  --docker-server=192.168.1.10:30500 \
  --docker-username=robot \
  --docker-password=TOKEN \
  --namespace=app
yamlspec:
  imagePullSecrets:
    - name: registry-creds

Garbage collection

API реестра не удаляет блобы физически при удалении тега. Запускайте GC периодически:

bashkubectl exec -n registry deploy/registry -- \
  registry garbage-collect \
    --delete-untagged \
    /etc/docker/registry/config.yml

CronJob для автоматизации:

yamlapiVersion: batch/v1
kind: CronJob
metadata:
  name: registry-gc
  namespace: registry
spec:
  schedule: "0 3 * * 0"   # каждое воскресенье в 3:00
  jobTemplate:
    spec:
      template:
        spec:
          restartPolicy: OnFailure
          containers:
            - name: gc
              image: registry:2
              command:
                - registry
                - garbage-collect
                - --delete-untagged
                - /etc/docker/registry/config.yml
              volumeMounts:
                - name: registry-data
                  mountPath: /var/lib/registry
          volumes:
            - name: registry-data
              persistentVolumeClaim:
                claimName: registry-pvc

Удаление старых тегов через API

bashDIGEST=$(curl -sI \
  -H "Accept: application/vnd.docker.distribution.manifest.v2+json" \
  "http://192.168.1.10:30500/v2/myapp/manifests/old-tag" \
  | grep -i Docker-Content-Digest | awk '{print $2}' | tr -d '\r')

curl -X DELETE "http://192.168.1.10:30500/v2/myapp/manifests/${DIGEST}"

После этого запустите GC для физического освобождения места на диске.


Pull-through кэш для DockerHub

yamlenv:
  - name: REGISTRY_PROXY_REMOTEURL
    value: "https://registry-1.docker.io"
  - name: REGISTRY_PROXY_USERNAME
    value: "${DOCKERHUB_USER}"
  - name: REGISTRY_PROXY_PASSWORD
    value: "${DOCKERHUB_TOKEN}"

Настройте Docker-ноды так, чтобы они тянули образы через 192.168.1.10:30500: после первого pull образ кэшируется, последующие запросы выполняются быстро и не учитываются в лимитах DockerHub.