Private Registries
A private registry is an image registry that only approved users, machines, or teams can access. It matters because most real applications contain company code, internal base images, licensed dependencies, or deployment-specific images that should not be published publicly. A private registry gives you controlled distribution: build an image once, push it to a trusted place, then let CI, servers, and teammates pull the exact same image.
Overview: How Private Registries Work
A Docker registry stores image content, not running containers. An image is a read-only template made from a configuration object plus filesystem layers. When you push an image, Docker uploads any missing layer blobs, then uploads a manifest that says which layers and config belong to a tag such as registry.example.com/platform/inventory-api:1.4.2. When another machine pulls that reference, Docker downloads the manifest, checks which blobs it already has, fetches only the missing layers, and records the image locally.
The word private mainly changes access control. Docker Hub private repositories, GitHub Container Registry, GitLab Container Registry, Amazon ECR, Google Artifact Registry, Azure Container Registry, Harbor, and self-hosted Registry v2 all speak registry-style APIs, but they decide who may push or pull. The Docker CLI usually authenticates with docker login. After login, credentials or credential-helper references are stored in the client configuration under ~/.docker/config.json on Linux, macOS, and Windows. Docker Desktop commonly uses the operating system keychain through a credential helper instead of storing the password directly.
Image names are important. A registry-qualified image reference starts with a hostname, then a namespace or repository path, then an optional tag: registry.example.com/platform/inventory-api:1.4.2. If no registry hostname is present, Docker assumes Docker Hub. If no tag is present, Docker assumes latest, which is a tag name, not a guarantee that the image is newest or stable. For private registries, always use the full registry hostname and a meaningful immutable version tag for deployments.
Syntax
docker login [REGISTRY_HOST]
docker tag SOURCE_IMAGE[:TAG] REGISTRY_HOST/NAMESPACE/REPOSITORY:TAG
docker push REGISTRY_HOST/NAMESPACE/REPOSITORY:TAG
docker pull REGISTRY_HOST/NAMESPACE/REPOSITORY:TAG
docker logout [REGISTRY_HOST]
| Part | Meaning |
|---|---|
REGISTRY_HOST |
The registry hostname, such as registry.example.com, ghcr.io, or localhost:5000. |
NAMESPACE |
An account, organization, project, or team path used by the registry for grouping and permissions. |
REPOSITORY |
The image repository name, usually the application or base image name. |
TAG |
A human-readable reference to one image version, such as 1.4.2, 2026-08-03, or a Git SHA. |
docker login |
Authenticates this Docker client to the registry. Many registries require a token instead of your normal password. |
docker logout |
Removes the stored credentials for that registry from this client. |
Examples
Log in to a private registry
docker login registry.example.com
Output:
Username: your-user
Password:
Login Succeeded
This command authenticates your Docker client to registry.example.com. In a browser UI you may create a personal access token, robot account token, or CI token, then paste that token at the password prompt. Do not put real tokens in a Dockerfile or build argument; anything baked into an image layer can be recovered later.
Tag and push an application image
docker tag inventory-api:1.4.2 registry.example.com/platform/inventory-api:1.4.2
docker push registry.example.com/platform/inventory-api:1.4.2
Output:
The push refers to repository [registry.example.com/platform/inventory-api]
8b1a2f4d7c31: Pushed
6d92f8c3a156: Pushed
1.4.2: digest: sha256:3b9a6f5c8d7e2a1f0c4b9d6e1a2c3f4b5d6e7f8091a2b3c4d5e6f708192a0b1c size: 856
docker tag does not copy the image. It creates a new local reference that points to the same image ID. docker push contacts the registry, uploads missing layer blobs, and finally attaches the 1.4.2 tag to a manifest. If another tag already exists with that name and the registry allows overwrites, the tag can move, so production teams often protect release tags or deploy by digest.
Pull and run from the private registry
docker pull registry.example.com/platform/inventory-api:1.4.2
docker run --rm -p 8080:8080 registry.example.com/platform/inventory-api:1.4.2
Output:
1.4.2: Pulling from platform/inventory-api
Digest: sha256:3b9a6f5c8d7e2a1f0c4b9d6e1a2c3f4b5d6e7f8091a2b3c4d5e6f708192a0b1c
Status: Downloaded newer image for registry.example.com/platform/inventory-api:1.4.2
The pull succeeds only if this client is authenticated and has permission to read the repository. The -p 8080:8080 flag publishes container port 8080 to host port 8080. If the image Dockerfile contains EXPOSE 8080, that is only metadata and documentation; EXPOSE does not publish the port by itself.
Run a local private registry for practice
docker run -d -p 5000:5000 --restart=always --name local-registry registry:2
docker pull alpine:3.20
docker tag alpine:3.20 localhost:5000/demo/alpine:3.20
docker push localhost:5000/demo/alpine:3.20
Output:
Unable to find image 'registry:2' locally
2: Pulling from library/registry
Status: Downloaded newer image for registry:2
The push refers to repository [localhost:5000/demo/alpine]
3.20: digest: sha256:beef1234beef1234beef1234beef1234beef1234beef1234beef1234beef1234 size: 528
The official registry:2 image is useful for local experiments. Docker treats localhost:5000 as a local registry endpoint, so this works without TLS for local development. Do not expose this simple registry on a network as-is. Real private registries need TLS, authentication, authorization, backups, retention policy, and usually vulnerability scanning.
How it Works Step by Step
- You build or pull an image locally. Docker stores its config object and read-only layer blobs in the local image store.
- You tag the image with a registry-qualified name. This changes local metadata only; layers are not rebuilt.
- You log in. The Docker client obtains or stores credentials for that registry host.
- You push. The client asks the registry which blobs it already has, uploads missing blobs, uploads the image config, and writes a manifest for the tag.
- Another host pulls. Docker fetches the manifest first, verifies digests, downloads missing blobs, then assembles the image from the same read-only layers.
- A container starts from that pulled image. Docker adds a thin writable container layer and starts the configured process. The registry is no longer involved unless another pull is needed.
The digest is the content-addressed identity of the pushed manifest. Tags are convenient names; digests are cryptographic references to exact content. In production, a common pattern is to publish a friendly tag for humans and record the digest in deployment logs or manifests so you can prove exactly what ran.
Common Mistakes
Assuming latest is safe
docker pull registry.example.com/platform/inventory-api:latest
This is risky in production because latest is just a movable tag. Today it may point to one build; tomorrow it may point to another. Use a version tag, build number, or digest instead.
docker pull registry.example.com/platform/inventory-api:1.4.2
Forgetting the registry hostname
docker push platform/inventory-api:1.4.2
Without a registry hostname, Docker interprets the name as a Docker Hub repository path. For a private registry, include the host explicitly.
docker push registry.example.com/platform/inventory-api:1.4.2
Baking credentials into an image
FROM alpine:3.20
ENV REGISTRY_TOKEN="<YOUR_REGISTRY_TOKEN>"
This is wrong because environment variables in a Dockerfile become image metadata, and secrets in earlier layers remain recoverable even if a later layer removes a file. Use registry login on the client or CI runner, and use your orchestrator secret store for runtime application secrets.
Best Practices
- Use registry-qualified names for private images, such as
registry.example.com/team/app:1.2.3. - Prefer immutable deployment tags or digests over moving tags like
latest. - Use least-privilege tokens: CI that only pulls should not have push permission.
- Protect release repositories with tag immutability, approval rules, or restricted write access.
- Use TLS for any registry that is not strictly local. Avoid insecure registries except in isolated labs.
- Log out of shared machines with
docker logout registry.example.com. - Keep secrets out of Dockerfiles, image labels, and build arguments.
- Enable image scanning and retention cleanup in the registry so stale vulnerable images do not accumulate forever.
- Document repository ownership and naming conventions before many teams start publishing images.
Practice Exercises
- Your team has a local image named
billing-worker:2.0.0. Write the commands to tag and push it toregistry.example.com/payments/billing-worker:2.0.0. Hint: the tag command comes before the push command. - Start a local registry on port 5000, push
alpine:3.20intolocalhost:5000/lab/alpine:3.20, remove the local tagged copy, then pull it again from the registry. Expected end state:docker imagesshows the localhost reference. - Design a registry permission model for a small company with developers, CI, and production servers. Decide who can push, who can pull, and which tags should be immutable.
Summary
- A private registry stores protected image layers, configs, manifests, tags, and digests.
docker loginauthenticates the client;docker tagprepares the image name;docker pushpublishes it.- Private image references should include the registry hostname and a stable version tag.
- Tags can move, while digests identify exact content.
- Never bake registry credentials or application secrets into image layers.
- Production registries need access control, TLS, scanning, retention, and backup planning.
