Un registro Docker privado te permite guardar y distribuir tus propias imágenes de contenedor sin publicarlas en Docker Hub ni depender de sus límites de descarga. La imagen oficial registry (el proyecto CNCF Distribution) implementa la misma API que usan docker push y docker pull. En este tutorial desplegarás el registro con Docker Compose en Ubuntu 24.04, lo protegerás con usuario y contraseña, lo publicarás detrás de Nginx con un certificado de Let's Encrypt y aprenderás a liberar espacio borrando imágenes antiguas.
Requisitos previos
Para seguir esta guía necesitas:
- Un servidor con Ubuntu 24.04 LTS, por ejemplo un VPS de CubePath, con espacio en disco suficiente para tus imágenes (20 GB o más).
- Un usuario no root con privilegios
sudo. - Docker Engine y el plugin de Docker Compose instalados desde el repositorio oficial de Docker.
- Un dominio con un registro DNS A, por ejemplo
registry.your_domain, apuntando a la IP pública del servidor. - Los puertos 80 y 443 accesibles desde Internet.
Paso 1: Crear los directorios y el usuario del registro
Guarda todo lo relacionado con el registro en /opt/registry: la configuración, el fichero de usuarios y las capas de las imágenes:
sudo mkdir -p /opt/registry/auth /opt/registry/data
El registro admite autenticación básica con un fichero htpasswd cifrado con bcrypt. La herramienta htpasswd está en el paquete apache2-utils:
sudo apt update
sudo apt install apache2-utils
Crea el fichero con tu primer usuario. La opción -B usa bcrypt, que es el único formato que acepta el registro, y -c crea el fichero. Te pedirá la contraseña dos veces:
sudo htpasswd -Bc /opt/registry/auth/htpasswd your_user
New password:
Re-type new password:
Adding password for user your_user
Para añadir más usuarios más adelante, repite el comando sin -c, o sobrescribirás el fichero.
Paso 2: Configurar y arrancar el registro
Crea el fichero de configuración del registro:
sudo nano /opt/registry/config.yml
version: 0.1
log:
level: info
storage:
filesystem:
rootdirectory: /var/lib/registry
delete:
enabled: true
http:
addr: :5000
headers:
X-Content-Type-Options: [nosniff]
auth:
htpasswd:
realm: Registry
path: /auth/htpasswd
storage.delete.enabled permite borrar imágenes por la API (lo usarás en el paso 6) y el bloque auth exige usuario y contraseña en todas las peticiones.
Crea ahora el fichero de Compose:
sudo nano /opt/registry/compose.yaml
services:
registry:
image: registry:3
restart: unless-stopped
ports:
- "127.0.0.1:5000:5000"
volumes:
- ./config.yml:/etc/distribution/config.yml:ro
- ./auth:/auth:ro
- ./data:/var/lib/registry
El puerto se publica solo en 127.0.0.1, de modo que el registro no es accesible desde fuera sin pasar por Nginx y su HTTPS. La versión 3 de la imagen lee la configuración de /etc/distribution/config.yml.
Arranca el registro:
cd /opt/registry
docker compose up -d
Comprueba que responde y que exige autenticación. Sin credenciales debe devolver 401:
curl -i http://127.0.0.1:5000/v2/
HTTP/1.1 401 Unauthorized
Content-Type: application/json
Docker-Distribution-Api-Version: registry/2.0
Www-Authenticate: Basic realm="Registry"
...
Con tu usuario, la respuesta es 200 y un JSON vacío:
curl -u your_user http://127.0.0.1:5000/v2/
Enter host password for user 'your_user':
{}
Paso 3: Publicar el registro con Nginx y HTTPS
Docker exige HTTPS para cualquier registro que no esté en localhost. Instala Nginx y Certbot con su plugin para Nginx:
sudo apt install nginx certbot python3-certbot-nginx
Permite el tráfico HTTP y HTTPS en el cortafuegos:
sudo ufw allow OpenSSH
sudo ufw allow 'Nginx Full'
sudo ufw enable
Crea el sitio de Nginx para el registro:
sudo nano /etc/nginx/sites-available/registry
server {
listen 80;
listen [::]:80;
server_name registry.your_domain;
# Sin límite de tamaño: las capas de una imagen pueden ocupar varios GB
client_max_body_size 0;
chunked_transfer_encoding on;
location /v2/ {
proxy_pass http://127.0.0.1:5000;
proxy_set_header Host $http_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;
}
}
Las cabeceras Host y X-Forwarded-Proto son necesarias: el registro las usa para construir las URL de subida que devuelve al cliente, y si faltan, docker push intenta continuar por HTTP y falla.
Activa el sitio, comprueba la sintaxis y recarga Nginx:
sudo ln -s /etc/nginx/sites-available/registry /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
Pide el certificado. Certbot añade al sitio la configuración TLS y la redirección de HTTP a HTTPS, y deja programada la renovación automática:
sudo certbot --nginx -d registry.your_domain
Comprueba el acceso por HTTPS:
curl -u your_user https://registry.your_domain/v2/
{}
Paso 4: Subir y descargar imágenes
Desde cualquier equipo con Docker (tu portátil, un servidor de CI o el propio servidor), inicia sesión en el registro:
docker login registry.your_domain
Username: your_user
Password:
Login Succeeded
Para subir una imagen al registro, su nombre debe empezar por el dominio del registro. Descarga una imagen pequeña de prueba, etiquétala y súbela:
docker pull hello-world
docker tag hello-world registry.your_domain/hello-world:1.0
docker push registry.your_domain/hello-world:1.0
The push refers to repository [registry.your_domain/hello-world]
53d204b3dc5d: Pushed
1.0: digest: sha256:... size: 525
Comprueba que el registro la tiene listando los repositorios y sus etiquetas:
curl -u your_user https://registry.your_domain/v2/_catalog
curl -u your_user https://registry.your_domain/v2/hello-world/tags/list
{"repositories":["hello-world"]}
{"name":"hello-world","tags":["1.0"]}
Para confirmar la descarga, borra la copia local y vuelve a bajarla del registro:
docker image rm registry.your_domain/hello-world:1.0
docker pull registry.your_domain/hello-world:1.0
Nota
docker loginguarda las credenciales en~/.docker/config.jsoncodificadas en base64, no cifradas. En equipos compartidos o servidores de CI, configura un credential helper o usa un usuario del registro dedicado a cada sistema.
Paso 5: Hacer copia de seguridad del registro
Todo el contenido del registro está en /opt/registry/data, y los usuarios en /opt/registry/auth. Para una copia consistente, para el registro un momento mientras copias los datos:
cd /opt/registry
docker compose stop registry
sudo tar -czf /root/registry-backup-$(date +%F).tar.gz -C /opt registry
docker compose start registry
Guarda el archivo fuera del servidor. Para restaurar, basta con extraerlo en /opt y ejecutar docker compose up -d.
Paso 6: Borrar imágenes y liberar espacio
Borrar una etiqueta no libera disco por sí solo. El proceso tiene dos partes: borrar el manifiesto por la API y después ejecutar el recolector de basura, que elimina las capas que ya no usa ninguna imagen.
Obtén primero el digest del manifiesto de la etiqueta que quieres borrar. Las cabeceras Accept son necesarias para que el registro devuelva el digest correcto tanto de imágenes simples como multiarquitectura:
curl -sI -u your_user \
-H 'Accept: application/vnd.oci.image.index.v1+json' \
-H 'Accept: application/vnd.oci.image.manifest.v1+json' \
-H 'Accept: application/vnd.docker.distribution.manifest.list.v2+json' \
-H 'Accept: application/vnd.docker.distribution.manifest.v2+json' \
https://registry.your_domain/v2/hello-world/manifests/1.0 | grep -i docker-content-digest
Docker-Content-Digest: sha256:your_digest
Borra el manifiesto con ese digest:
curl -u your_user -X DELETE https://registry.your_domain/v2/hello-world/manifests/sha256:your_digest
La respuesta es un 202 Accepted sin cuerpo. Ahora ejecuta el recolector de basura. Debe correr con el registro parado para no borrar capas de una subida en curso:
cd /opt/registry
docker compose stop registry
docker compose run --rm --entrypoint registry registry garbage-collect --delete-untagged /etc/distribution/config.yml
docker compose start registry
La salida enumera los blobs marcados y eliminados. Comprueba el espacio que ocupa el registro:
sudo du -sh /opt/registry/data
Solución de problemas
413 Request Entity Too Largeal hacer push: Nginx está limitando el tamaño de la petición. Confirma queclient_max_body_size 0;está dentro del bloqueserverque atiende HTTPS (Certbot modifica ese bloque, no crea uno nuevo) y recarga Nginx.unauthorized: authentication required: el usuario o la contraseña no coinciden, o el ficherohtpasswdno se creó con-B. Vuelve a crear el usuario consudo htpasswd -B /opt/registry/auth/htpasswd your_usery reinicia el registro condocker compose restart.http: server gave HTTP response to HTTPS client: estás usandoyour_server_ip:5000en lugar del dominio. Usa siempreregistry.your_domain, que pasa por Nginx y HTTPS.blob upload unknowno push que falla a mitad: faltaX-Forwarded-ProtooHosten la configuración de Nginx. Revisa el bloquelocation /v2/del paso 3.
Conclusión
Tienes un registro Docker privado con autenticación, servido por HTTPS con un certificado de Let's Encrypt y con un procedimiento para liberar espacio. Como siguientes pasos, crea un usuario distinto para cada sistema de CI, programa la copia de seguridad y la recolección de basura en una ventana de mantenimiento semanal, y si el disco se queda corto, mueve el almacenamiento a un bucket compatible con S3 usando el driver s3 del registro.
