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 и заблокирует деплои.