Analytics cookies only (Google Analytics, PostHog). No ad networks, no cross-site tracking. See our privacy policy.
Self-host Distribution (Docker Registry)
updated Oct 2026deployed by usprices checked · Jul 2026Install verified on Ubuntu 26.04 · Oct 2026 · how we test
We earn commissions when you shop through the links below. Full disclosure →
The reference implementation of the OCI Distribution Specification and the registry engine inside Docker Hub, GitHub Container Registry, GitLab and Harbor, published on Docker Hub as the registry image: push and pull images and other OCI artifacts over HTTP, store them on local disk or S3-compatible, Azure and GCS backends, and gate access with htpasswd or a token server. Apache-2.0 licensed, a CNCF project written in Go; it runs as a single container with one data directory and no database or web UI.
Runs on a VPS from $4/mo (Kamatera 1GB, 1 GB RAM) · prices checked Sep 2026
Our recommendation
Pick Distribution when you need a place to push images that is yours and nothing more: it is the reference implementation of the OCI Distribution Specification and the engine Docker Hub, GitHub Container Registry, GitLab and Harbor are built on, shipped on Docker Hub as the `registry` image. Apache-2.0, a CNCF project in Go, one container, one data directory, no database and no web UI; storage is local disk or an S3-compatible, Azure or GCS backend, and access is htpasswd or a token server. Our snippet is the docs' native basic auth recipe: a bcrypt `htpasswd` made with the documented `httpd` one-liner from a password in `.env`, the three `REGISTRY_AUTH*` variables, a named volume for `/var/lib/registry`, and a closing login, push and pull against `localhost:5000` that proves the whole thing works. Upstream publishes no RAM minimum, so our 128 MB floor is an estimate; the binary is small. The catalog's Harbor wraps this same engine in projects, RBAC, replication, scanning and a UI. Distribution is for one person or one pipeline that wants the registry and nothing else.
What you need
Any VPS with at least 128 MB of RAM
A domain you control — most self-hosted setups need HTTPS in front of them
Paid link — we earn a commission if you shop through it.
Install
Run these commands on your server:
# Distribution — the OCI registry Docker Hub, GHCR and Harbor are built on, run as a private registry with htpasswd auth — based on the "Deploy a registry server" docs (native basic auth), pinned to registry:3.1.2echo "REGISTRY_PASSWORD=$(openssl rand -hex 16)" > .env. ./.envmkdir -p auth# bcrypt (-B) is the only htpasswd format the registry acceptsdocker run --rm --entrypoint htpasswd httpd:2.4.69-alpine -Bbn registry "$REGISTRY_PASSWORD" > auth/htpasswdcat > docker-compose.yml <<'EOF'services: registry: image: registry:3.1.2 restart: unless-stopped environment: REGISTRY_AUTH: htpasswd REGISTRY_AUTH_HTPASSWD_REALM: Registry Realm REGISTRY_AUTH_HTPASSWD_PATH: /auth/htpasswd ports: - 5000:5000 volumes: - registry_data:/var/lib/registry - ./auth:/auth:rovolumes: registry_data:EOFdocker compose up -dtimeout 120 bash -c 'until curl -fsS http://localhost:5000/ >/dev/null; do sleep 3; done'# prove it works: log in (Docker treats 127.0.0.0/8 as an insecure registry, so plain HTTP is allowed here), push a small image and pull it backecho "$REGISTRY_PASSWORD" | docker login -u registry --password-stdin localhost:5000docker pull alpine:3.22docker tag alpine:3.22 localhost:5000/alpine:3.22docker push localhost:5000/alpine:3.22docker pull localhost:5000/alpine:3.22# → http://SERVER_IP:5000 — an empty 200 (liveness only): there is no web UI; the registry API lives at /v2/ and answers 401 until you send the credentials in .env
The Deploy a registry server docs and the Docker daemon reference set the auth and TLS rules:
non-negotiableBasic auth works only with bcrypt entries; the docs warn that an htpasswd file written without -B makes every login fail, which is why the one-liner runs httpd's htpasswd -Bbn. The credentials are the single user registry with the password in .env; add users by appending more -Bbn lines to auth/htpasswd and restarting. Without the three REGISTRY_AUTH* variables the registry is world-writable, so do not drop them to make a client happy.
non-negotiableImages live in the registry_data volume at /var/lib/registry, and the image's default config.yml keeps storage.delete.enabled: true, so manifests can be deleted over the API. Upgrades are a tag bump and docker compose up -d. The same default config opens a debug listener with Prometheus metrics on port 5001; our compose does not publish it, and you should not without auth in front.
non-negotiableThe docs say to configure TLS first for authentication to work, and run their examples on the floating registry:3, which we pin to an exact tag. Our login over plain HTTP succeeds only because the Docker daemon treats 127.0.0.0/8 as an insecure registry automatically, which the daemon reference says not to rely on. Other machines refuse the HTTP registry unless it is in their insecure-registries, and the docs add that an insecure registry cannot be combined with basic auth: the fix is a certificate via REGISTRY_HTTP_TLS_CERTIFICATE/REGISTRY_HTTP_TLS_KEY or a TLS proxy in front.
non-negotiableGET / is a bare 200 that the docs describe as always returning OK (a liveness probe target), while /v2/ answers 401 with a WWW-Authenticate challenge until credentials are sent and every response under it carries Docker-Distribution-API-Version: registry/2.0. Point health checks at /, remember there is nothing to open in a browser, and once a reverse proxy terminates TLS publish 5000 only to it.