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

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 Large al hacer push: Nginx está limitando el tamaño de la petición. Confirma que client_max_body_size 0; está dentro del bloque server que 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 fichero htpasswd no se creó con -B. Vuelve a crear el usuario con sudo htpasswd -B /opt/registry/auth/htpasswd your_user y reinicia el registro con docker compose restart.
  • http: server gave HTTP response to HTTPS client: estás usando your_server_ip:5000 en lugar del dominio. Usa siempre registry.your_domain, que pasa por Nginx y HTTPS.
  • blob upload unknown o push que falla a mitad: falta X-Forwarded-Proto o Host en la configuración de Nginx. Revisa el bloque location /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.