Skip to content
shellhacks.dev
Docker

How to Run a Private Docker Registry With Examples

Run a private Docker registry with the registry image, add TLS and basic auth, push and pull images, free space with garbage collection, and weigh options.

6 min read
Summarize in AI
Illustration for "How to Run a Private Docker Registry With Examples": a terminal session, a request travelling to a server, a scrolling log.

A private registry gives a team its own place to push images without Docker Hub's limits or public visibility. The reference implementation is a single small container. The risk is leaving it open: an unauthenticated, plain HTTP registry on the network is a way to run code on every host that pulls from it. The examples use registry:2 and Docker Engine 27; pushing to hosted registries is in pushing to Docker Hub and GitHub.

Start one on localhost

Shell
docker run -d --restart=always --name registry \
  -p 127.0.0.1:5000:5000 \
  -v registry-data:/var/lib/registry \
  registry:2

docker tag myapp:1.0 localhost:5000/myapp:1.0
docker push localhost:5000/myapp:1.0
curl -s http://localhost:5000/v2/_catalog
Text
{"repositories":["myapp"]}

Docker treats localhost registries as safe, so no TLS is needed for this test. The volume keeps the images when the container is replaced. The image name carries the registry address, host:port/name:tag, which is how Docker decides where to push. Tags are covered in tagging and versioning images.

TLS and authentication for other hosts

Shell
mkdir -p auth certs
docker run --rm --entrypoint htpasswd httpd:2 -Bbn alice 's3cret-pass' > auth/htpasswd

docker run -d --restart=always --name registry -p 443:5000 \
  -v registry-data:/var/lib/registry \
  -v "$PWD/certs:/certs:ro" -v "$PWD/auth:/auth:ro" \
  -e REGISTRY_HTTP_TLS_CERTIFICATE=/certs/fullchain.pem \
  -e REGISTRY_HTTP_TLS_KEY=/certs/privkey.pem \
  -e REGISTRY_AUTH=htpasswd \
  -e REGISTRY_AUTH_HTPASSWD_REALM="Registry" \
  -e REGISTRY_AUTH_HTPASSWD_PATH=/auth/htpasswd \
  registry:2

docker login registry.example.com

Use a certificate from a real CA, such as Let's Encrypt, since a self-signed one means configuring every client daemon to trust it. -B makes bcrypt hashes, the only htpasswd format the registry accepts besides plain MD5-style ones it discourages. Putting nginx or Caddy in front for TLS is common and lets you share port 443, see reverse proxy with nginx or Caddy. Typing the password into a command line, as in the example, leaves it in history; use a file, as covered in shell script secrets.

Cleaning up

Shell
docker exec registry sh -c 'REGISTRY_STORAGE_DELETE_ENABLED=true registry garbage-collect --dry-run /etc/docker/registry/config.yml'

Deleting a manifest through the API removes the tag but not the layers, and the registry keeps growing until garbage collection runs. Enable deletion with REGISTRY_STORAGE_DELETE_ENABLED=true, delete the manifest by digest, then run garbage-collect with the registry stopped or in read-only mode, since a push during collection can lose layers. Start with --dry-run.

What a registry actually serves

The registry is an HTTP API with two kinds of objects. Blobs are layers and config files addressed by their SHA-256 digest, and manifests describe an image as a list of blob digests plus metadata. A tag is a name that points at a manifest. Pushing uploads any blobs the server does not have yet, then writes the manifest, and pulling reads the manifest and fetches its blobs. Because everything is addressed by digest, two images that share a base layer store it once, and a pull of an image whose layers are already on the host transfers nothing.

Seeing that in practice explains operations questions. The catalogue endpoint at /v2/_catalog lists repositories, /v2/NAME/tags/list lists tags, and curl with the right Accept header returns a manifest. Storage under /var/lib/registry holds the blobs as files in a content-addressed tree, which means a backup is a copy of that directory while the registry is idle, or a snapshot of the volume.

Choosing a storage backend

The default stores blobs on the local filesystem, which suits one host with a reliable disk. For anything that has to survive the host, the registry has storage drivers for S3 and compatible services, Google Cloud Storage and Azure Blob Storage, set in the configuration file under storage. With an object store behind it, the registry container is stateless, and replacing it is a routine deploy. A common setup also enables the cache section with Redis so that repeated blob lookups do not hit the object store each time.

Disk sizing needs thought because images accumulate. Each CI build that pushes a new tag adds layers, and nothing removes them until garbage collection runs. A team that pushes a tag per commit can add many gigabytes a week. Decide a retention rule up front, such as keeping the last twenty tags per repository and every release tag, and write the script that deletes the rest before the volume fills.

Pull-through cache for a public registry

The same image can act as a mirror of Docker Hub. Setting proxy.remoteurl to the Hub address makes the registry fetch and store images on first request, and later pulls from the team come from the cache. That removes most of the exposure to Hub rate limits for build fleets and speeds up cold builds. The daemons are then pointed at it through registry-mirrors in daemon.json. A mirror configured as a proxy cannot also accept pushes, so a team that needs both runs two instances.

Operating it day to day

Treat the registry like any service in the deploy path. Monitor the HTTP status codes and the disk, set up an alert on free space, and keep the TLS certificate renewal under automation, because an expired certificate stops every deploy at once with a trust error that is easy to misread. Pin the image to a specific tag such as registry:2.8 instead of a floating one, and read the release notes before upgrading, since the storage layout is stable but configuration keys have changed between versions.

Access control deserves a separate decision. Basic authentication with htpasswd gives every user the same rights: anyone who can log in can push and delete. If different teams need different rights, put the registry behind an authentication proxy, use the token authentication mode with an external service, or choose a product that has roles built in. Rotate the CI credentials like any other secret, and give the build system its own account so that its access can be revoked without touching people.

Checking that it works from another host

After the registry is reachable, test it from a client machine and not from the server. Run docker login registry.example.com, push a small image, delete it locally with docker rmi, and pull it back. The sequence proves DNS, the certificate chain, authentication and storage in one pass. If the login fails with an x509 error, the certificate does not cover the name the client used or the intermediate certificate is missing from the file. Using the full chain in the certificate file fixes the second case, and requesting a certificate for the exact host name fixes the first. If a push stalls on large layers behind a proxy, raise the proxy's body size limit, because many proxies refuse bodies over one megabyte by default.

Where this does not apply

The reference registry has no web interface, user management, vulnerability scanning or replication. For a team those usually matter, and Harbor or a hosted registry (GitHub Container Registry, GitLab Container Registry, a cloud provider's) provides them with less upkeep. A registry is also a critical piece of the deploy path, so it needs backups of its volume, as in backing up a Docker volume. For a handful of hosts and rare updates, docker save and load may be simpler. Configuration is described in the registry configuration reference and the deployment guide.

Also inDevOps
Taggeddocker
Share

Keep reading

Get new posts by email

One email when a new post is published. No spam.

Unsubscribe in one click. No spam, ever.