Minio as Docker Registry Storage Backend

MinIO provides S3-compatible object storage that works as a scalable, high-availability backend for Docker Registry, replacing local filesystem storage. This guide covers configuring Docker Registry with MinIO as the S3 storage backend, including TLS, garbage collection, and a high-availability deployment.

Prerequisites

  • Ubuntu 20.04+/Debian 11+ or CentOS 8+/Rocky Linux 8+
  • Docker and Docker Compose installed
  • 4+ GB available disk space for MinIO
  • Root or sudo access

Installing MinIO

Deploy MinIO via Docker (quickest method):

mkdir -p /opt/minio/{data,config}

docker run -d \
  --name minio \
  -p 9000:9000 \
  -p 9001:9001 \
  -v /opt/minio/data:/data \
  -e MINIO_ROOT_USER=minioadmin \
  -e MINIO_ROOT_PASSWORD=minioadmin_secret \
  --restart unless-stopped \
  quay.io/minio/minio server /data --console-address ":9001"

Or install the binary directly:

# Download MinIO
wget https://dl.min.io/server/minio/release/linux-amd64/minio -O /usr/local/bin/minio
chmod +x /usr/local/bin/minio

# Create a dedicated user
useradd -r -s /sbin/nologin minio-user
mkdir -p /var/lib/minio
chown minio-user:minio-user /var/lib/minio

# Create environment file
cat > /etc/minio.env << 'EOF'
MINIO_VOLUMES="/var/lib/minio"
MINIO_OPTS="--console-address :9001"
MINIO_ROOT_USER=minioadmin
MINIO_ROOT_PASSWORD=minioadmin_secret
EOF

# Create systemd service
tee /etc/systemd/system/minio.service << 'EOF'
[Unit]
Description=MinIO Object Storage
After=network.target

[Service]
Type=simple
EnvironmentFile=/etc/minio.env
ExecStart=/usr/local/bin/minio server $MINIO_VOLUMES $MINIO_OPTS
User=minio-user
Restart=on-failure

[Install]
WantedBy=multi-user.target
EOF

systemctl daemon-reload
systemctl enable --now minio

Creating a MinIO Bucket for Registry

Use the MinIO console at http://your-server:9001 or the mc CLI:

# Install MinIO Client
wget https://dl.min.io/client/mc/release/linux-amd64/mc -O /usr/local/bin/mc
chmod +x /usr/local/bin/mc

# Configure mc alias
mc alias set local http://localhost:9000 minioadmin minioadmin_secret

# Create the registry bucket
mc mb local/docker-registry

# Set the bucket policy (private by default - correct for a registry)
mc anonymous set none local/docker-registry

# Create a dedicated service account for the registry
mc admin user add local registry-user registry_secret_123

# Create policy for registry access
cat > /tmp/registry-policy.json << 'EOF'
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "s3:GetBucketLocation",
        "s3:ListBucket",
        "s3:ListBucketMultipartUploads"
      ],
      "Resource": ["arn:aws:s3:::docker-registry"]
    },
    {
      "Effect": "Allow",
      "Action": [
        "s3:AbortMultipartUpload",
        "s3:DeleteObject",
        "s3:GetObject",
        "s3:ListMultipartUploadParts",
        "s3:PutObject"
      ],
      "Resource": ["arn:aws:s3:::docker-registry/*"]
    }
  ]
}
EOF

mc admin policy create local registry-policy /tmp/registry-policy.json
mc admin policy attach local registry-policy --user registry-user

Configuring Docker Registry with MinIO

Create the Docker Registry configuration:

mkdir -p /opt/registry

cat > /opt/registry/config.yml << 'EOF'
version: 0.1
log:
  level: info
  formatter: text

storage:
  s3:
    accesskey: registry-user
    secretkey: registry_secret_123
    regionendpoint: http://localhost:9000   # MinIO endpoint
    region: us-east-1                       # Any value works with MinIO
    bucket: docker-registry
    encrypt: false
    secure: false                           # Set true if MinIO has TLS
    v4auth: true
    chunksize: 5242880                      # 5MB chunks
    rootdirectory: /registry               # Prefix within the bucket
  cache:
    blobdescriptor: inmemory
  delete:
    enabled: true                           # Required for garbage collection

http:
  addr: :5000
  secret: generate_a_strong_secret_here

auth:
  htpasswd:
    realm: Registry
    path: /auth/htpasswd
EOF

Create registry authentication:

mkdir -p /opt/registry/auth
sudo apt install -y apache2-utils
htpasswd -Bbn registry_user registry_password > /opt/registry/auth/htpasswd

Running Docker Registry

docker run -d \
  --name registry \
  --restart unless-stopped \
  -p 5000:5000 \
  -v /opt/registry/config.yml:/etc/docker/registry/config.yml \
  -v /opt/registry/auth:/auth \
  registry:2

Or with Docker Compose:

# /opt/registry/docker-compose.yml
version: '3.8'
services:
  registry:
    image: registry:2
    container_name: registry
    ports:
      - "127.0.0.1:5000:5000"
    volumes:
      - ./config.yml:/etc/docker/registry/config.yml
      - ./auth:/auth
    restart: unless-stopped
cd /opt/registry
docker compose up -d
docker compose logs -f registry

Test the registry:

# Login
docker login localhost:5000 -u registry_user -p registry_password

# Push a test image
docker pull alpine
docker tag alpine localhost:5000/alpine:test
docker push localhost:5000/alpine:test

# Verify it's in MinIO
mc ls local/docker-registry/registry/

TLS Configuration

For production, configure TLS on MinIO and update the registry config:

# Copy certificates to MinIO's config directory
mkdir -p /root/.minio/certs
cp /etc/letsencrypt/live/storage.yourdomain.com/fullchain.pem /root/.minio/certs/public.crt
cp /etc/letsencrypt/live/storage.yourdomain.com/privkey.pem /root/.minio/certs/private.key

# Restart MinIO to pick up TLS
systemctl restart minio

Update the registry config to use HTTPS:

storage:
  s3:
    regionendpoint: https://storage.yourdomain.com
    secure: true

For the Docker Registry itself, configure TLS via Nginx:

server {
    listen 443 ssl;
    server_name registry.yourdomain.com;

    ssl_certificate /etc/letsencrypt/live/registry.yourdomain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/registry.yourdomain.com/privkey.pem;

    client_max_body_size 0;  # Disable size limit for image pushes

    location / {
        proxy_pass http://127.0.0.1:5000;
        proxy_set_header Host $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;
    }
}

Garbage Collection

Docker Registry marks deleted layers but doesn't immediately free space. Run garbage collection to reclaim storage in MinIO:

# Run garbage collection (registry must be in read-only mode or stopped)
# First put the registry in maintenance mode
docker exec registry registry garbage-collect \
  /etc/docker/registry/config.yml --delete-untagged=true

# Check MinIO storage before and after
mc du local/docker-registry

Automate garbage collection weekly:

# /etc/cron.weekly/registry-gc
#!/bin/bash
docker exec registry registry garbage-collect \
  /etc/docker/registry/config.yml --delete-untagged=true

High-Availability Deployment

For HA, run multiple registry containers pointing to the same MinIO backend:

version: '3.8'
services:
  registry-1:
    image: registry:2
    volumes:
      - ./config.yml:/etc/docker/registry/config.yml
      - ./auth:/auth
    ports:
      - "127.0.0.1:5001:5000"
    restart: unless-stopped

  registry-2:
    image: registry:2
    volumes:
      - ./config.yml:/etc/docker/registry/config.yml
      - ./auth:/auth
    ports:
      - "127.0.0.1:5002:5000"
    restart: unless-stopped

Load balance with Nginx upstream:

upstream registry {
    server 127.0.0.1:5001;
    server 127.0.0.1:5002;
}

For MinIO HA, deploy a distributed cluster (4+ nodes recommended).

Troubleshooting

Registry can't connect to MinIO:

# Verify MinIO is accessible from the registry container
docker exec registry curl -v http://host.docker.internal:9000/docker-registry

# Check credentials
mc alias set test http://localhost:9000 registry-user registry_secret_123
mc ls test/docker-registry

Image push fails with "access denied":

# Check the MinIO policy attached to registry-user
mc admin user info local registry-user
mc admin policy list local

"unauthorized: authentication required" on pull:

# Verify htpasswd file
cat /opt/registry/auth/htpasswd

# Test authentication directly
curl -u registry_user:registry_password https://registry.yourdomain.com/v2/

Slow push performance:

Increase MinIO's erasure coding chunk size or deploy MinIO closer to the registry. Check network latency between registry and MinIO with ping.

Conclusion

Using MinIO as a Docker Registry storage backend decouples image storage from the registry instances, enabling horizontal scaling and centralized object storage management. This architecture supports high-availability deployments, easy backup via MinIO's mirroring features, and integration with existing S3-compatible workflows. For production, always enable TLS on both MinIO and the registry, and schedule regular garbage collection runs.