--- title: "Docker Registry con almacenamiento S3 en Ubuntu 24.04" description: "Despliega un Docker Registry privado en Ubuntu 24.04 que guarda las imágenes en un bucket S3 autoalojado con Garage, con HTTPS, usuarios y limpieza semanal." category: "Herramientas de Desarrollo" language: es source: https://cubepath.com/es/docs/herramientas-de-desarrollo/minio-como-backend-de-almacenamiento-docker-registry updated: 2026-09-25 --- # Docker Registry con almacenamiento S3 en Ubuntu 24.04 Docker Registry (el proyecto `distribution`) es el servidor que almacena y sirve imágenes de contenedores. Por defecto guarda las capas en el disco local, pero puede usar cualquier almacenamiento compatible con S3, lo que separa el servicio de los datos y facilita crecer o replicar. En este tutorial desplegarás en Ubuntu 24.04 un registro privado con Docker Compose que guarda las imágenes en un bucket S3 autoalojado, lo publicarás con HTTPS mediante Caddy, protegerás el acceso con usuario y contraseña y programarás la recolección de basura. Durante años la opción habitual para el backend S3 era MinIO, pero el repositorio de la edición comunitaria está archivado y ya no se publican binarios ni imágenes nuevas. Esta guía usa **Garage**, un servidor de objetos compatible con S3, ligero y mantenido activamente, que ofrece imagen oficial de contenedor. La configuración del registro es la misma para cualquier backend S3, así que si ya tienes otro servicio compatible solo tendrás que cambiar el endpoint y las credenciales del paso 4. ## Requisitos previos Para seguir esta guía necesitas: - Un servidor con Ubuntu 24.04 LTS, por ejemplo un VPS de CubePath, con al menos 2 GB de RAM y espacio en disco suficiente para tus imágenes. - Un usuario no root con privilegios `sudo`. - Docker Engine y el plugin Docker Compose instalados desde el repositorio oficial de Docker. - Un dominio, por ejemplo `registry.your_domain`, con un registro DNS A que apunte a la IP pública del servidor (`your_server_ip`). - Los puertos 80 y 443 abiertos hacia el servidor. A lo largo del tutorial sustituye `registry.your_domain` por tu dominio real. ## Paso 1: Preparar el directorio del proyecto Todo el despliegue vivirá en `/opt/registry`: la configuración de Garage, la del registro, el fichero de usuarios y los datos del bucket. Crea la estructura y asígnala a tu usuario para no necesitar `sudo` en cada edición: ```bash sudo mkdir -p /opt/registry/{auth,garage/meta,garage/data} sudo chown -R "$USER":"$USER" /opt/registry cd /opt/registry ``` Instala también `apache2-utils`, que incluye `htpasswd` para generar el fichero de usuarios del registro, y `openssl` para crear secretos aleatorios: ```bash sudo apt update sudo apt install -y apache2-utils openssl ``` ## Paso 2: Configurar Garage Garage necesita un secreto compartido para su protocolo interno (RPC), aunque solo tengas un nodo. Genéralo: ```bash openssl rand -hex 32 ``` Copia el valor y crea el fichero de configuración de Garage: ```bash nano /opt/registry/garage.toml ``` Pega el siguiente contenido y sustituye `your_rpc_secret` por el valor generado: ```toml metadata_dir = "/var/lib/garage/meta" data_dir = "/var/lib/garage/data" db_engine = "lmdb" replication_factor = 1 rpc_bind_addr = "0.0.0.0:3901" rpc_public_addr = "127.0.0.1:3901" rpc_secret = "your_rpc_secret" [s3_api] s3_region = "garage" api_bind_addr = "0.0.0.0:3900" ``` `replication_factor = 1` indica que hay una sola copia de cada objeto, lo correcto para un único servidor. La API S3 escucha en el puerto 3900 solo dentro de la red de Docker: no la publicarás al exterior porque el registro será el único cliente. Protege el fichero, ya que contiene el secreto: ```bash chmod 600 /opt/registry/garage.toml ``` ## Paso 3: Arrancar Garage y crear el bucket Crea el fichero de Docker Compose. De momento solo define Garage; el registro se añade en el paso 5. ```bash nano /opt/registry/compose.yaml ``` ```yaml services: garage: image: dxflrs/garage:v2.1.0 restart: unless-stopped volumes: - ./garage.toml:/etc/garage.toml:ro - ./garage/meta:/var/lib/garage/meta - ./garage/data:/var/lib/garage/data ``` > Nota: fija siempre una versión concreta de la imagen. Consulta la última versión estable en la página de publicaciones de Garage (garagehq.deuxfleurs.fr) antes de desplegar. Arranca el contenedor: ```bash docker compose up -d garage ``` La imagen de Garage no incluye shell; el binario está en `/garage`. Comprueba el estado del nodo: ```bash docker compose exec garage /garage status ``` ```output ==== HEALTHY NODES ==== ID Hostname Address Tags Zone Capacity DataAvail 3f2a9c81d7e4b605 8c1d2e3f4a5b 127.0.0.1:3901 NO ROLE ASSIGNED ``` El nodo funciona pero todavía no forma parte del clúster. Asígnale una zona y la capacidad que quieres dedicar al almacenamiento, usando el principio del ID que aparece en tu salida: ```bash docker compose exec garage /garage layout assign -z dc1 -c 50G 3f2a9c81d7e4b605 docker compose exec garage /garage layout apply --version 1 ``` Vuelve a ejecutar `status`: la columna `Zone` mostrará `dc1` y la capacidad asignada. Crea ahora el bucket y una clave de acceso exclusiva para el registro, y dale permisos de lectura y escritura sobre ese bucket: ```bash docker compose exec garage /garage bucket create docker-registry docker compose exec garage /garage key create registry-key ``` ```output ==== ACCESS KEY INFORMATION ==== Key ID: GK1a2b3c4d5e6f7a8b9c0d1e2f Key name: registry-key Secret key: 4f8e2d...c91a ``` Guarda el `Key ID` y el `Secret key`: los necesitarás en el siguiente paso. Después concede el acceso: ```bash docker compose exec garage /garage bucket allow --read --write --owner docker-registry --key registry-key ``` Comprueba que la clave aparece asociada al bucket: ```bash docker compose exec garage /garage bucket info docker-registry ``` La salida debe listar `registry-key` con permisos `RWO`. ## Paso 4: Configurar Docker Registry El registro se configura con un fichero YAML. Genera primero un secreto para firmar el estado de las subidas: ```bash openssl rand -hex 32 ``` Crea el fichero de configuración: ```bash nano /opt/registry/config.yml ``` Sustituye `your_key_id`, `your_secret_key` y `your_http_secret` por tus valores: ```yaml version: 0.1 log: level: info formatter: json storage: s3: accesskey: your_key_id secretkey: your_secret_key region: garage regionendpoint: http://garage:3900 forcepathstyle: true bucket: docker-registry rootdirectory: /registry delete: enabled: true redirect: disable: true maintenance: uploadpurging: enabled: true age: 168h interval: 24h dryrun: false http: addr: :5000 secret: your_http_secret headers: X-Content-Type-Options: [nosniff] auth: htpasswd: realm: Docker Registry path: /auth/htpasswd ``` Las claves importantes son: - `region` debe coincidir con `s3_region` de Garage y `regionendpoint` apunta al servicio `garage` por la red interna de Compose. - `forcepathstyle: true` usa URLs del tipo `http://garage:3900/docker-registry/...`, que Garage acepta sin configurar DNS por bucket. - `redirect.disable: true` es imprescindible aquí: sin ella, el registro redirige a los clientes a URLs firmadas del backend S3 para descargar capas, y `garage:3900` no es accesible desde fuera del servidor. - `delete.enabled: true` permite borrar manifiestos por API, requisito para liberar espacio con la recolección de basura. Protege el fichero, que contiene credenciales: ```bash chmod 600 /opt/registry/config.yml ``` Crea el primer usuario del registro. La opción `-B` usa bcrypt, el único formato que acepta el registro; `htpasswd` te pedirá la contraseña: ```bash htpasswd -Bc /opt/registry/auth/htpasswd your_user ``` Para añadir más usuarios después, repite el comando sin `-c`, que sobrescribiría el fichero. ## Paso 5: Arrancar el registro Añade el servicio `registry` al fichero Compose: ```bash nano /opt/registry/compose.yaml ``` El fichero completo queda así: ```yaml services: garage: image: dxflrs/garage:v2.1.0 restart: unless-stopped volumes: - ./garage.toml:/etc/garage.toml:ro - ./garage/meta:/var/lib/garage/meta - ./garage/data:/var/lib/garage/data registry: image: registry:3 restart: unless-stopped depends_on: - garage ports: - "127.0.0.1:5000:5000" volumes: - ./config.yml:/etc/distribution/config.yml:ro - ./auth:/auth:ro ``` El puerto 5000 se publica solo en `127.0.0.1`. Es importante porque Docker añade sus propias reglas de iptables y un puerto publicado en todas las interfaces quedaría expuesto aunque UFW lo bloquee. El acceso público llegará por HTTPS a través de Caddy. > Nota: la imagen `registry:3` lee la configuración de `/etc/distribution/config.yml`. En la versión 2 la ruta era `/etc/docker/registry/config.yml`; si montas el fichero en la ruta antigua, el registro arrancará con su configuración por defecto y guardará las capas en el disco del contenedor. Arranca el registro y revisa el log: ```bash docker compose up -d registry docker compose logs registry --tail 20 ``` Busca una línea con `listening on [::]:5000`. Comprueba que responde y exige autenticación: ```bash curl -i http://127.0.0.1:5000/v2/ ``` ```output HTTP/1.1 401 Unauthorized Docker-Distribution-Api-Version: registry/2.0 Www-Authenticate: Basic realm="Docker Registry" ``` ## Paso 6: Publicar el registro con HTTPS mediante Caddy El cliente de Docker exige HTTPS para cualquier registro que no sea local. Caddy obtiene y renueva automáticamente un certificado de Let's Encrypt, así que es la forma más sencilla de ponerlo delante. Instálalo desde su repositorio oficial: ```bash sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https curl curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list sudo apt update sudo apt install -y caddy ``` Sustituye el contenido del Caddyfile: ```bash sudo nano /etc/caddy/Caddyfile ``` ```text registry.your_domain { reverse_proxy 127.0.0.1:5000 } ``` Caddy envía la cabecera `X-Forwarded-Proto`, con la que el registro genera correctamente las URLs `https://` de las subidas. No hay límite de tamaño de petición por defecto, así que las capas grandes no dan problemas. Abre los puertos en el cortafuegos y recarga Caddy: ```bash sudo ufw allow OpenSSH sudo ufw allow 80/tcp sudo ufw allow 443/tcp sudo ufw enable sudo systemctl reload caddy ``` Comprueba que el certificado se ha emitido y el registro responde por HTTPS: ```bash curl -I https://registry.your_domain/v2/ ``` ```output HTTP/2 401 docker-distribution-api-version: registry/2.0 www-authenticate: Basic realm="Docker Registry" ``` Si la petición falla, revisa `sudo journalctl -u caddy --no-pager -n 50`: el error más habitual es que el registro DNS todavía no apunte al servidor. ## Paso 7: Subir y descargar una imagen Desde tu equipo o desde el propio servidor, inicia sesión con el usuario del paso 4: ```bash docker login registry.your_domain ``` ```output Login Succeeded ``` Descarga una imagen pequeña, etiquétala con el nombre de tu registro y súbela: ```bash docker pull alpine:3.20 docker tag alpine:3.20 registry.your_domain/demo/alpine:3.20 docker push registry.your_domain/demo/alpine:3.20 ``` Comprueba que el registro la conoce: ```bash curl -u your_user https://registry.your_domain/v2/_catalog ``` ```output {"repositories":["demo/alpine"]} ``` Y que las capas están realmente en el bucket de Garage y no en el disco del contenedor: ```bash cd /opt/registry docker compose exec garage /garage bucket info docker-registry ``` El campo de objetos y el tamaño deben ser mayores que cero. Para cerrar la prueba, borra la imagen local y descárgala desde el registro: ```bash docker rmi registry.your_domain/demo/alpine:3.20 docker pull registry.your_domain/demo/alpine:3.20 ``` ## Paso 8: Programar la recolección de basura Borrar una etiqueta solo elimina la referencia al manifiesto; las capas siguen ocupando espacio en el bucket hasta que ejecutas el recolector de basura. El recolector no debe correr mientras alguien sube imágenes, porque podría borrar capas de una subida en curso, así que el procedimiento seguro es parar el registro, recolectar y volver a arrancarlo. Para borrar una etiqueta usa `skopeo`, que resuelve el digest del manifiesto por ti: ```bash sudo apt install -y skopeo skopeo delete --creds your_user docker://registry.your_domain/demo/alpine:3.20 ``` Crea un script que haga la recolección: ```bash sudo nano /usr/local/sbin/registry-gc ``` ```bash #!/usr/bin/env bash set -euo pipefail cd /opt/registry docker compose stop registry trap 'docker compose start registry' EXIT docker compose run --rm registry garbage-collect --delete-untagged /etc/distribution/config.yml ``` El `trap` garantiza que el registro vuelve a arrancar aunque la recolección falle. Hazlo ejecutable y pruébalo una vez a mano: ```bash sudo chmod 755 /usr/local/sbin/registry-gc sudo /usr/local/sbin/registry-gc ``` La salida lista los manifiestos y blobs marcados y termina con líneas `Deleting blob` si había capas sin referencias. Para programarlo cada domingo de madrugada, crea un servicio y un temporizador de systemd: ```bash sudo nano /etc/systemd/system/registry-gc.service ``` ```ini [Unit] Description=Docker Registry garbage collection Requires=docker.service After=docker.service [Service] Type=oneshot ExecStart=/usr/local/sbin/registry-gc ``` ```bash sudo nano /etc/systemd/system/registry-gc.timer ``` ```ini [Unit] Description=Weekly Docker Registry garbage collection [Timer] OnCalendar=Sun *-*-* 04:00:00 Persistent=true [Install] WantedBy=timers.target ``` Activa el temporizador y comprueba la próxima ejecución: ```bash sudo systemctl daemon-reload sudo systemctl enable --now registry-gc.timer systemctl list-timers registry-gc.timer ``` Los resultados de cada ejecución quedan en `journalctl -u registry-gc`. ## Solución de problemas - **`no basic auth credentials` o `unauthorized` al hacer push**: ejecuta `docker login registry.your_domain` antes de subir y comprueba que el usuario se creó con `htpasswd -B`. Las contraseñas en MD5 o SHA no funcionan. - **Las subidas se quedan en `Retrying` o el log muestra `s3aws: ... connection refused`**: Garage no está listo o el nodo no tiene layout. Revisa `docker compose exec garage /garage status` y que `regionendpoint` sea `http://garage:3900`. - **`SignatureDoesNotMatch` o `AccessDenied`**: la `region` del registro no coincide con `s3_region` de Garage, o la clave no tiene permisos sobre el bucket (`garage bucket info docker-registry`). - **Los `docker pull` fallan con timeouts a `garage:3900`**: falta `redirect.disable: true` en `config.yml`. - **El espacio no baja tras la recolección**: solo se liberan capas de manifiestos borrados. Borra primero las etiquetas con `skopeo delete` y vuelve a ejecutar `registry-gc`. ## Conclusión Ya tienes un registro privado de imágenes con HTTPS y autenticación que guarda las capas en un bucket S3 autoalojado con Garage, y un temporizador que libera el espacio de las imágenes borradas cada semana. Como el registro solo depende de la API S3, puedes cambiar el backend editando el bloque `storage.s3`. Como siguientes pasos puedes: - Añadir más nodos de Garage en otros servidores con `replication_factor = 3` para tolerar la caída de uno. - Copiar el bucket a otra ubicación con `rclone` como copia de seguridad. - Configurar el registro como caché de Docker Hub con el bloque `proxy` en una instancia aparte, para acelerar las descargas de tus servidores.