Self-Host a Docker Registry with Docker Compose

What Is Docker Registry?

Docker Registry is the official open-source container image registry from the CNCF Distribution project. It lets you store, distribute, and manage Docker images on your own infrastructure instead of relying on Docker Hub. Self-hosting a registry gives you full control over image storage, eliminates pull rate limits, and keeps proprietary images off third-party servers. Official site.

Prerequisites

  • A Linux server (Ubuntu 22.04+ recommended)
  • Docker and Docker Compose installed (guide)
  • 10 GB+ of free disk space (depends on image count)
  • 512 MB+ RAM
  • A domain name (required for TLS in production)

Docker Compose Configuration

Create a docker-compose.yml file:

services:
  registry:
    image: registry:3.0.0
    container_name: registry
    restart: unless-stopped
    ports:
      - "5000:5000"
    environment:
      REGISTRY_HTTP_ADDR: "0.0.0.0:5000"
      REGISTRY_STORAGE_DELETE_ENABLED: "true"
      REGISTRY_HTTP_HEADERS_X-Content-Type-Options: "[nosniff]"
      OTEL_TRACES_EXPORTER: "none"
    volumes:
      - registry-data:/var/lib/registry
    healthcheck:
      test: ["CMD", "wget", "--spider", "-q", "http://localhost:5000/v2/"]
      interval: 30s
      timeout: 5s
      retries: 3

volumes:
  registry-data:

Production Setup with TLS and Authentication

For production use, add TLS certificates and basic auth:

services:
  registry:
    image: registry:3.0.0
    container_name: registry
    restart: unless-stopped
    ports:
      - "5000:5000"
    environment:
      REGISTRY_HTTP_ADDR: "0.0.0.0:5000"
      REGISTRY_HTTP_TLS_CERTIFICATE: "/certs/fullchain.pem"
      REGISTRY_HTTP_TLS_KEY: "/certs/privkey.pem"
      REGISTRY_AUTH: "htpasswd"
      REGISTRY_AUTH_HTPASSWD_REALM: "Registry Realm"
      REGISTRY_AUTH_HTPASSWD_PATH: "/auth/htpasswd"
      REGISTRY_STORAGE_DELETE_ENABLED: "true"
      REGISTRY_HTTP_HEADERS_X-Content-Type-Options: "[nosniff]"
      OTEL_TRACES_EXPORTER: "none"
    volumes:
      - registry-data:/var/lib/registry
      - ./certs:/certs:ro
      - ./auth:/auth:ro
    healthcheck:
      test: ["CMD", "wget", "--no-check-certificate", "--spider", "-q", "https://localhost:5000/v2/"]
      interval: 30s
      timeout: 5s
      retries: 3

volumes:
  registry-data:

Generate the htpasswd file before starting:

mkdir -p auth certs
# Install htpasswd utility
apt-get install -y apache2-utils
# Create credentials (replace username/password)
htpasswd -Bbn myuser MyStr0ngP@ssw0rd > auth/htpasswd

Start the stack:

docker compose up -d

Initial Setup

After starting the registry, verify it’s running:

curl http://localhost:5000/v2/

You should see {} as the response. If using authentication:

curl -u myuser:MyStr0ngP@ssw0rd http://localhost:5000/v2/

Push Your First Image

Tag and push an image to your registry:

# Tag an existing image
docker tag nginx:latest localhost:5000/my-nginx:1.0

# Push to your registry
docker push localhost:5000/my-nginx:1.0

# Pull it back
docker pull localhost:5000/my-nginx:1.0

List Repositories

curl http://localhost:5000/v2/_catalog

Configuration

The registry is configured entirely through environment variables. Every config key maps to an env var with the REGISTRY_ prefix, using underscores for nesting.

Key configuration options:

Environment VariableDefaultPurpose
REGISTRY_HTTP_ADDR0.0.0.0:5000Listen address and port
REGISTRY_STORAGE_DELETE_ENABLEDfalseAllow image deletion via API
REGISTRY_STORAGE_CACHE_BLOBDESCRIPTORinmemoryLayer cache backend
REGISTRY_HTTP_TLS_CERTIFICATEPath to TLS certificate
REGISTRY_HTTP_TLS_KEYPath to TLS private key
REGISTRY_AUTHAuthentication backend (htpasswd)
REGISTRY_PROXY_REMOTEURLEnable pull-through cache mode

Storage Backends

The default filesystem storage works for most self-hosted setups. For larger deployments, the registry also supports S3-compatible storage (MinIO, Garage), Azure Blob, and Google Cloud Storage — configured via environment variables.

Advanced Configuration (Optional)

Pull-Through Cache

Run the registry as a caching proxy for Docker Hub to reduce bandwidth and avoid rate limits:

services:
  registry:
    image: registry:3.0.0
    container_name: registry-cache
    restart: unless-stopped
    ports:
      - "5000:5000"
    environment:
      REGISTRY_HTTP_ADDR: "0.0.0.0:5000"
      REGISTRY_PROXY_REMOTEURL: "https://registry-1.docker.io"
      OTEL_TRACES_EXPORTER: "none"
    volumes:
      - cache-data:/var/lib/registry

volumes:
  cache-data:

Configure Docker clients to use the cache by adding to /etc/docker/daemon.json:

{
  "registry-mirrors": ["http://your-server:5000"]
}

Garbage Collection

Deleted images leave behind unreferenced blobs. Clean them up:

docker exec registry bin/registry garbage-collect /etc/distribution/config.yml

Run this periodically (via cron) to reclaim disk space. The registry must be stopped or set to read-only during garbage collection to avoid data corruption.

S3 Backend Storage

environment:
  REGISTRY_STORAGE: "s3"
  REGISTRY_STORAGE_S3_ACCESSKEY: "your-access-key"
  REGISTRY_STORAGE_S3_SECRETKEY: "your-secret-key"
  REGISTRY_STORAGE_S3_REGION: "us-east-1"
  REGISTRY_STORAGE_S3_BUCKET: "my-registry"
  REGISTRY_STORAGE_S3_REGIONENDPOINT: "http://minio:9000"

Reverse Proxy

Put the registry behind a reverse proxy for TLS termination and domain-based access. See Reverse Proxy Setup for details.

Nginx Proxy Manager config: set the forward hostname to the registry container and port 5000. Enable WebSocket support. Set the maximum upload size high enough for large images (e.g., 2 GB).

Key proxy requirement: the Host header must be passed through correctly. Docker clients use it for authentication and redirect handling.

Backup

Back up the registry data volume:

docker run --rm -v registry-data:/data -v $(pwd)/backup:/backup \
  alpine tar czf /backup/registry-backup-$(date +%Y%m%d).tar.gz /data

The /var/lib/registry volume contains all image layers and metadata. This is the only volume you need to back up. See Backup Strategy for a comprehensive approach.

Troubleshooting

Cannot push images — “server gave HTTP response to HTTPS client”

Symptom: docker push fails with an error about HTTP vs HTTPS. Fix: For local/development registries without TLS, add the registry to Docker’s insecure registries list in /etc/docker/daemon.json:

{
  "insecure-registries": ["your-server:5000"]
}

Then restart Docker: sudo systemctl restart docker. For production, always use TLS.

Push fails with “blob unknown to registry”

Symptom: Intermittent push failures mentioning unknown blobs. Fix: This usually indicates a storage driver issue. If using a network filesystem, switch to local storage or S3. Ensure the storage volume has sufficient disk space.

Authentication errors after restart

Symptom: 401 Unauthorized after restarting the registry. Fix: Verify the auth/htpasswd file still exists and is mounted correctly. Regenerate credentials if needed:

htpasswd -Bbn myuser newpassword > auth/htpasswd
docker compose restart registry

Disk space growing without bound

Symptom: The registry volume keeps growing even after deleting images. Fix: Run garbage collection. Image deletion via the API only marks layers for deletion — garbage collection actually frees the space:

docker exec registry bin/registry garbage-collect /etc/distribution/config.yml

Resource Requirements

  • RAM: 50-100 MB idle, 200-500 MB under heavy push/pull load
  • CPU: Low (most work is I/O bound)
  • Disk: Depends entirely on stored images — budget 2-10x the total size of images you plan to store (layers are shared but garbage collection is needed)

Verdict

A self-hosted Docker Registry is essential if you build custom images, run CI/CD pipelines, or want to avoid Docker Hub’s pull rate limits. The official registry is lightweight, battle-tested, and straightforward to deploy. For small-to-medium self-hosting setups, the basic filesystem storage is all you need. If you want a web UI for browsing images, pair it with a registry frontend or use Gitea, which includes a built-in container registry.

FAQ

Do I need a self-hosted registry?

If you only pull public images, a pull-through cache is more useful than a full registry. If you build and push custom images (for development, CI/CD, or private apps), a self-hosted registry eliminates Docker Hub rate limits and keeps your images under your control.

Can I use this with Podman?

Yes. Podman is fully compatible with OCI registries. Use podman push and podman pull with the same your-server:5000/image:tag format. See our Podman guide.

How does this compare to Harbor or Gitea’s registry?

Docker Registry is a bare-bones image store — no UI, no vulnerability scanning, no RBAC. Harbor adds all of those on top of the Distribution project. Gitea bundles a container registry with Git hosting. Use plain Registry for simplicity; Harbor for enterprise features; Gitea if you want Git + images in one tool.

Comments