Docker Registry Privado: Golden Images On-Premise & Clúster K8s
En despliegues corporativos bajo redes privadas aisladas (Air-Gapped), entornos de soberanía de datos o infraestructuras donde se requiere eliminar la dependencia de cuotas de red externas (Docker Hub, Quay o GHCR), Mañuti implementa un Docker Registry v2 Privado.
Este servicio centraliza el ciclo de vida de las Golden Images del framework, gestiona la persistencia de imágenes base del plano de control de Kubernetes v1.31 (etcd, CoreDNS, Calico CNI) y optimiza el aprovisionamiento de nodos mediante el runtime containerd.
Tradicional
flowchart LR
ClientT["🖥️<br/>CLIENT"]
FELogicT["🧊<br/>FRONT-END<br/>LOGIC"]
BELogicT["⚙️<br/>BACK-END<br/>LOGIC"]
SecT["🔒<br/>SECURITY"]
DbT[("🛢️<br/>DATABASE")]
ClientT ==> FELogicT
FELogicT --> BELogicT
BELogicT --> SecT
SecT --> DbT
Serverless
flowchart LR
subgraph ClientSide ["CLIENT-SIDE"]
ClientS["🖥️ 🧊<br/>FRONT-END<br/>LOGIC"]
end
subgraph CloudServices ["THIRD-PARTY SERVICES & BACKEND"]
SecS["🔒<br/>SECURITY"]
BELogicS["⚙️<br/>BACK-END<br/>LOGIC<br/>(FaaS / APIs)"]
DbS[("🛢️<br/>DATABASE<br/>(BaaS)")]
end
NoteS["USING CLIENT-SIDE LOGIC AND<br/>THIRD-PARTY SERVICES"]
ClientS ==> SecS
ClientS ==> BELogicS
ClientS ==> DbS
flowchart TD
subgraph REG ["Host Registry Dedicado (192.168.1.187)"]
DAEMON["daemon.json<br/>(insecure-registry)"]
ENGINE["Docker Engine<br/>Container registry:2"]
VOL["Volumen Persistente Físico<br/>/opt/docker-registry/data"]
DAEMON ~~~ ENGINE
ENGINE -->|":5000<br/>Storage Driver"| VOL
end
CTRL["k8s-control<br/>.182<br/>---<br/>containerd<br/>hosts.toml"]
W1["k8s-worker1<br/>.183<br/>---<br/>containerd<br/>hosts.toml"]
W2["k8s-worker2<br/>.184<br/>---<br/>containerd<br/>hosts.toml"]
REG -->|"HTTP Pull / Push (Port 5000)"| CTRL
REG -->|"HTTP Pull / Push (Port 5000)"| W1
REG -->|"HTTP Pull / Push (Port 5000)"| W2
1. Topología del Servidor de Registro y Clúster de Nodos
| Servidor / Host | IP Privada | Rol / Servicio | Runtime / Motor |
|---|---|---|---|
| Node-Registry | 192.168.1.187 |
Servidor de Registro Privado (Puerto 5000) | Docker Engine 27+ |
| k8s-control | 192.168.1.182 |
Master / Control Plane Kubernetes | containerd 1.7+ / CRI |
| k8s-worker1 | 192.168.1.183 |
Nodo de Cómputo / Worker Pods | containerd 1.7+ / CRI |
| k8s-worker2 | 192.168.1.184 |
Nodo de Cómputo / Worker Pods | containerd 1.7+ / CRI |
| k8s-worker3 | 192.168.1.185 |
Nodo de Cómputo / Worker Pods | containerd 1.7+ / CRI |
| k8s-worker4 | 192.168.1.186 |
Nodo de Cómputo / Worker Pods | containerd 1.7+ / CRI |
2. Aprovisionamiento del Servidor de Registro (192.168.1.187)
2.1 Instalación del Motor Docker
Ejecutar en el host designado para el almacén de artefactos:
sudo apt update && sudo apt upgrade -y
curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh get-docker.sh
sudo usermod -aG docker $USER
2.2 Despliegue del Contenedor con Persistencia Local
Se crea una partición persistente en /opt/docker-registry/data para garantizar que las capas de las imágenes no se destruyan si el contenedor o el servidor se reinician:
sudo mkdir -p /opt/docker-registry/data
sudo docker run -d \
-p 5000:5000 \
--name local-registry \
--restart always \
-v /opt/docker-registry/data:/var/lib/registry \
registry:2
(En distribuciones basadas en Podman o si Docker Hub restringe el acceso, puede utilizarse la imagen espejo quay.io/libpod/registry:2).
2.3 Verificación de Disponibilidad Local
# Validar que el contenedor está activo
docker ps -f name=local-registry
# Consultar el catálogo v2 mediante la API HTTP (debe retornar un JSON vacío inicialmente)
curl http://localhost:5000/v2/_catalog
# Retorno esperado: {"repositories":[]}
3. Configuración de Registro Inseguro (HTTP) en Clientes Docker
Dado que el registro opera sobre HTTP plano dentro de una subred privada confiable, el demonio de Docker en los hosts donde se construyen imágenes debe autorizar la excepción:
3.1 Edición de /etc/docker/daemon.json
{
"insecure-registries": ["192.168.1.187:5000"]
}
sudo systemctl restart docker
4. Integración Quirúrgica con Kubernetes y containerd
Por diseño de seguridad, containerd rechaza descargas desde registros que no implementen HTTPS con certificados firmados por una CA válida. Para conectar los nodos de Kubernetes (192.168.1.182 al .186) sin necesidad de certificados SSL autofirmados, se configura el subsistema CRI.
4.1 Corrección de Duplicidad en /etc/containerd/config.toml
Atención Técnica: containerd no permite rutas de certificados compuestas ni directivas heredadas de Docker que compitan con la interfaz CRI (
io.containerd.grpc.v1.cri).
Ejecutar este bloque de depuración en todos los nodos del clúster K8s (Control Plane y Workers):
# 1. Respaldo preventivo
sudo cp /etc/containerd/config.toml /etc/containerd/config.toml.bak
# 2. Eliminar referencias legadas o duplicadas de docker/certs.d
sudo sed -i '/config_path = .*\/etc\/docker\/certs.d/d' /etc/containerd/config.toml
# 3. Establecer la ruta estándar de containerd en la sección CRI
sudo sed -i 's|config_path = ""|config_path = "/etc/containerd/certs.d"|g' /etc/containerd/config.toml
4.2 Definición del Archivo de Anfitrión (hosts.toml)
Crear la configuración de bypass TLS para el endpoint 192.168.1.187:5000 en cada nodo del clúster:
sudo mkdir -p /etc/containerd/certs.d/192.168.1.187:5000
cat <<EOF | sudo tee /etc/containerd/certs.d/192.168.1.187:5000/hosts.toml
server = "http://192.168.1.187:5000"
[host."http://192.168.1.187:5000"]
capabilities = ["pull", "resolve", "push"]
skip_verify = true
EOF
4.3 Reinicio y Verificación del Runtime
sudo systemctl restart containerd
sudo systemctl status containerd --no-pager
5. Script de Ingesta Masiva de Imágenes Críticas de Infraestructura
Para entornos donde el clúster carece de salida a internet, este script automatiza la descarga, re-etiquetado y publicación masiva de las dependencias base de Kubernetes y la red Calico CNI hacia el registro local:
#!/usr/bin/env bash
set -eo pipefail
REGISTRY="192.168.1.187:5000"
IMAGES=(
# Calico CNI v3.29.1
"quay.io/calico/apiserver:v3.29.1"
"quay.io/calico/csi:v3.29.1"
"quay.io/calico/kube-controllers:v3.29.1"
"quay.io/calico/node-driver-registrar:v3.29.1"
"quay.io/calico/node:v3.29.1"
"quay.io/calico/typha:v3.29.1"
"quay.io/tigera/operator:v1.36.2"
# Kubernetes Core v1.31
"registry.k8s.io/coredns/coredns:v1.11.3"
"registry.k8s.io/etcd:3.5.24-0"
"registry.k8s.io/kube-apiserver:v1.31.14"
"registry.k8s.io/kube-controller-manager:v1.31.14"
"registry.k8s.io/kube-proxy:v1.31.14"
"registry.k8s.io/kube-scheduler:v1.31.14"
)
echo "=== Iniciando Ingesta hacia Docker Registry Local ==="
for IMG in "${IMAGES[@]}"; do
NAME=$(echo "$IMG" | awk -F'/' '{print $NF}')
if [[ "$IMG" == *"registry.k8s.io"* ]]; then
LOCAL_TAG="$REGISTRY/k8s-core/$NAME"
else
LOCAL_TAG="$REGISTRY/calico/$NAME"
fi
echo "Descargando capa upstream: $IMG"
docker pull "$IMG"
echo "Etiquetando para almacén local: $LOCAL_TAG"
docker tag "$IMG" "$LOCAL_TAG"
echo "Publicando en el registro privado..."
docker push "$LOCAL_TAG"
echo "----------------------------------------------------"
done
echo "=== Almacén local sincronizado y listo para consumo ==="
6. Procedimiento de Rescate de Imágenes desde containerd
Si una imagen crítica ya se encuentra en la caché interna de containerd de un nodo de Kubernetes y se desea respaldar en el registro privado:
# 1. En el nodo K8s (k8s-control): Exportar imagen desde el namespace k8s.io
sudo ctr -n k8s.io images export calico-node-backup.tar quay.io/calico/node:v3.29.1
# 2. Transferir el archivo tar hacia el host del Registro
scp calico-node-backup.tar [email protected]:/tmp/
# 3. En el Servidor del Registro (192.168.1.187): Cargar en Docker Engine
docker load -i /tmp/calico-node-backup.tar
# 4. Re-etiquetar y subir al registro local
docker tag quay.io/calico/node:v3.29.1 192.168.1.187:5000/calico/node:v3.29.1
docker push 192.168.1.187:5000/calico/node:v3.29.1
7. Pruebas de Despliegue y Validación en Kubernetes
7.1 Construcción y Publicación de Imagen de Prueba
Desde el host de registro (192.168.1.187):
docker pull alpine:latest
docker tag alpine:latest 192.168.1.187:5000/manuti-test:v1
docker push 192.168.1.187:5000/manuti-test:v1
7.2 Lanzamiento de Pod de Validación en el Clúster
Desde el nodo maestro k8s-control (192.168.1.182):
kubectl run test-registry \
--image=192.168.1.187:5000/manuti-test:v1 \
--restart=Never \
-- /bin/sh -c "echo 'Conexión a Docker Registry exitosa' && sleep 3600"
7.3 Verificación de Estado Operativo
kubectl get pods test-registry -o wide
Salida esperada:
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
test-registry 1/1 Running 0 12s 10.244.2.14 k8s-worker3 <none> <none>
Si el pod alcanza el estado Running, se certifica que:
- El contenedor
local-registrydespacha capas de forma persistente. - La configuración de
hosts.tomlen containerd ignora la ausencia de TLS sin rechazar la conexión. - El clúster de Kubernetes es 100% autónomo para desplegar cargas de trabajo de Mañuti sin depender de repositorios públicos en la nube.