Self-hosted Docker registry внутри кластера k0s

Published: 2026-06-09

В этой конфигурации нет DockerHub, GHCR и ECR. Образы пушатся в Docker Registry v2, работающий как под внутри того же кластера k0s. Вот как это устроено и какие есть компромиссы.


Зачем свой registry

  • Нет внешней зависимости при деплое: скрипт пушит и кластер тянет образ с одного сервера.
  • Нет rate limit (DockerHub ограничивает анонимные pull до 100 за 6 часов).
  • Не нужны секреты для аутентификации — доступ только внутренний, закрыт файрволом.
  • Образы никогда не покидают VPS.
  • Нет расходов на egress при pull образов.

Очевидный минус: если под registry упадёт, деплоить станет невозможно. Но когда на single-node кластере падают поды, проблемы, скорее всего, серьёзнее.


Что запущено

05-setup-registry.sh разворачивает registry:2 в namespace registry. Одна реплика с hostPath-томом на /opt/registry-data:

yamlapiVersion: apps/v1
kind: Deployment
metadata:
  name: registry
  namespace: registry
spec:
  replicas: 1
  selector:
    matchLabels:
      app: registry
  template:
    spec:
      containers:
        - name: registry
          image: registry:2
          ports:
            - containerPort: 5000
          env:
            - name: REGISTRY_STORAGE_DELETE_ENABLED
              value: "true"
            - name: REGISTRY_HTTP_ADDR
              value: "0.0.0.0:5000"
          volumeMounts:
            - mountPath: /var/lib/registry
              name: data
      volumes:
        - name: data
          hostPath:
            path: /opt/registry-data
            type: DirectoryOrCreate

REGISTRY_STORAGE_DELETE_ENABLED: true необходим, чтобы скрипт деплоя мог удалять старые манифесты через HTTP API. Без него DELETE /v2/<name>/manifests/<digest> возвращает 405.


NodePort для доступа

Registry опубликован на порту 30500 через NodePort:

yamlapiVersion: v1
kind: Service
metadata:
  name: registry
  namespace: registry
spec:
  type: NodePort
  selector:
    app: registry
  ports:
    - port: 5000
      targetPort: 5000
      nodePort: 30500

С локальной машины push выглядит так:

bashdocker push 91.184.248.13:30500/antonnovikov-site:abc1234

Порт доступен только с IP деплой-машины — файрвол VPS разрешает 30500 только с неё.

Проверить содержимое registry:

bash# Список репозиториев
curl http://91.184.248.13:30500/v2/_catalog

# Теги для репозитория
curl http://91.184.248.13:30500/v2/antonnovikov-site/tags/list

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

Поскольку порт 30500 — plain HTTP (без TLS), Docker нужно разрешить этот registry как insecure.

На локальной машине (macOS Docker Desktop):

json// ~/.docker/daemon.json
{
  "insecure-registries": ["91.184.248.13:30500"]
}

Или через Docker Desktop → Settings → Docker Engine.

На ноде k0s containerd нужна аналогичная настройка. k0s обрабатывает это автоматически через секцию insecureRegistries в конфиге:

yaml# /etc/k0s/k0s.yaml
  containerd:
    config: |
      [plugins."io.containerd.grpc.v1.cri".registry.mirrors."91.184.248.13:30500"]
        endpoint = ["http://91.184.248.13:30500"]

Очистка старых образов

После каждого успешного деплоя 06-deploy-site.sh удаляет все теги, кроме текущего:

bashREGISTRY="91.184.248.13:30500"
IMAGE_NAME="antonnovikov-site"
IMAGE_TAG="$COMMIT_SHA"

TAGS=$(curl -s "http://${REGISTRY}/v2/${IMAGE_NAME}/tags/list" | jq -r '.tags[]?')

for tag in $TAGS; do
    if [ "$tag" != "$IMAGE_TAG" ]; then
        DIGEST=$(curl -s -I \
            -H "Accept: application/vnd.docker.distribution.manifest.v2+json" \
            "http://${REGISTRY}/v2/${IMAGE_NAME}/manifests/${tag}" \
            | grep -i "Docker-Content-Digest" | awk '{print $2}' | tr -d '\r')

        if [ -n "$DIGEST" ]; then
            curl -s -X DELETE \
                "http://${REGISTRY}/v2/${IMAGE_NAME}/manifests/${DIGEST}" \
                && echo "Deleted $tag ($DIGEST)" || echo "Failed to delete $tag"
        fi
    fi
done

Важно: API удаления registry только помечает манифест. Для реального освобождения места нужен сборщик мусора:

bashREGISTRY_POD=$(kubectl get pods -n registry -l app=registry -o name | head -1)

kubectl exec -n registry $REGISTRY_POD -- \
    registry garbage-collect /etc/docker/registry/config.yml

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

yamlapiVersion: batch/v1
kind: CronJob
metadata:
  name: registry-gc
  namespace: registry
spec:
  schedule: "0 3 * * *"  # каждую ночь в 3:00
  jobTemplate:
    spec:
      template:
        spec:
          containers:
            - name: gc
              image: registry:2
              command:
                - registry
                - garbage-collect
                - /etc/docker/registry/config.yml
              volumeMounts:
                - mountPath: /var/lib/registry
                  name: data
          volumes:
            - name: data
              hostPath:
                path: /opt/registry-data
          restartPolicy: OnFailure

Безопасность

HTTP без TLS здесь допустим, потому что:

  • Порт 30500 закрыт файрволом — доступен только деплой-машине.
  • Образы не передаются по публичному интернету.
  • Registry без аутентификации — добавление auth потребует TLS.

Для окружения с несколькими разработчиками добавить TLS и basic auth:

yamlenv:
  - name: REGISTRY_AUTH
    value: htpasswd
  - name: REGISTRY_AUTH_HTPASSWD_REALM
    value: "Registry Realm"
  - name: REGISTRY_AUTH_HTPASSWD_PATH
    value: /auth/htpasswd
  - name: REGISTRY_HTTP_TLS_CERTIFICATE
    value: /certs/tls.crt
  - name: REGISTRY_HTTP_TLS_KEY
    value: /certs/tls.key

Мониторинг диска

bash# Размер данных registry
du -sh /opt/registry-data/

# Использование диска ноды
df -h /opt/

Стоит добавить Prometheus-алерт на заполнение диска под hostPath registry — переполненный диск убьёт поды registry и заблокирует деплои.