What a registry actually does
A registry stores and distributes container images. It's organized by repository (a named collection of related images, usually for one application) and tag (a specific version within that repository). Pushing uploads a locally-built image to the registry. Pulling downloads an image from the registry to a local machine — this also happens automatically whenever you start a container from an image you don't already have locally.
docker pull nginx:1.25
docker tag myapp:1.0 myusername/myapp:1.0
docker push myusername/myapp:1.0
Docker Hub — the default public registry
Unless configured otherwise, docker pull/docker push, and an unqualified FROM in a Dockerfile, default to Docker Hub. It hosts a few categories of images:
- Official images —
node,postgres,nginx,python, and similar. Curated and actively maintained, vetted by Docker. Usually the recommended starting point for a base image. - Verified Publisher images — published directly by the software vendor. For example, a database company publishing its own official image. This carries an extra trust signal beyond a purely community-contributed image.
- Community/user images — published by anyone with a Docker Hub account, with no particular vetting. Treat these with the same caution you'd use for an unfamiliar package from a public code registry with no reputation signal.
Why unqualified image names default to Docker Hub
FROM node:20 # implicitly: docker.io/library/node:20 (Docker Hub, official image)
FROM myorg/myapp:1.0 # implicitly: docker.io/myorg/myapp:1.0 (Docker Hub, user/org namespace)
An image reference with no registry hostname resolves against Docker Hub by default. This is just a configured default in the Docker daemon, not a hardcoded rule. To use a different registry, just include its hostname in the image reference.
Rate limiting — a real, practical Docker Hub consideration
Docker Hub limits pulls for anonymous and free-tier accounts. This occasionally shows up as mysterious ImagePullBackOff-style failures in CI pipelines or clusters that pull from many nodes in a short window. Authenticating with a Docker Hub account, even a free one, raises these limits substantially, and is a simple fix for pipelines hitting the limit.
Why organizations often move to a private or alternative registry
Public registries are the right default for genuinely public, open-source images. But proprietary application images generally shouldn't go on a public registry at all — both for confidentiality (source or build details can sometimes be inferred from image layers) and to avoid depending on a third-party public service's availability or rate limits for critical internal deployments.
Related Resources
The full sequence
docker build -t myapp:1.0 .
docker tag myapp:1.0 myregistry.example.com/myteam/myapp:1.0
docker login myregistry.example.com
# Username: ...
# Password: ...
docker push myregistry.example.com/myteam/myapp:1.0
How Docker knows which registry to talk to
The hostname prefix of the image reference determines the target registry. No special flag is needed on docker push — the destination is fully encoded in the tag:
myregistry.example.com/myteam/myapp:1.0
└────────┬────────┘ └───┬───┘ └─┬─┘ └┬┘
registry host namespace repo tag
docker.io/library/nginx:1.25 (Docker Hub, implicit -- same shape, just defaulted)
If the reference's first segment doesn't look like a hostname (no dot or colon), Docker assumes Docker Hub. If it does look like a hostname — it has a . or :port, or is explicitly localhost — Docker treats it as a private registry address instead.
Authenticating with docker login
docker login myregistry.example.com
Credentials are cached locally in ~/.docker/config.json, in plaintext by default unless a credential helper is configured. Once logged in, later push/pull commands against the same registry don't need re-authentication in the same session.
Common private registry options
- Cloud-provider managed registries — AWS ECR, Google Artifact Registry/GCR, Azure Container Registry. Tightly integrated with each cloud's own IAM system, and often the natural choice if you're already on that cloud.
- Self-hosted registry software — the open-source Docker Registry (the
registry:2image, itself distributed via Docker Hub), or more full-featured options like Harbor, which adds vulnerability scanning, RBAC, and replication. - GitHub Container Registry (GHCR) and GitLab Container Registry — convenient when your source code and CI/CD already live on that platform, since auth can piggyback on the same platform identity.
Why organizations use private registries at all
- Confidentiality — proprietary images shouldn't be publicly pullable, the way an unauthenticated Docker Hub repository would be.
- Access control — a private registry can restrict which teams or services can push or pull which images, following the same least-privilege principle used elsewhere in container security.
- Reliability and control — you're not depending on a third-party public service's availability or rate limits for critical internal pulls.
- Compliance and scanning integration — many private registries integrate with vulnerability scanning and image signing, gating what's allowed to be pushed based on security policy.
Development and testing can reasonably pull common base images straight from Docker Hub. An organization's own proprietary images belong in a private registry, scoped to only the teams and systems that need access.
Related Resources
Why even a specific-looking version tag isn't a true guarantee
FROM node:20.11.0-slim
This looks precisely pinned — a specific patch version, not a broad 20 or latest. But it's still just a tag. Nothing stops the maintainers of the node image from re-pushing different content under that exact tag later — for example, a critical security patch to an already-released tag. This does happen. A tag, no matter how specific it looks, is a mutable pointer, not a guarantee of identical content.
Digest pinning — the actual guarantee
FROM node@sha256:a1b2c3d4e5f6789...
This reference can never silently change. The digest is a cryptographic hash of the image's actual content, so any change to that content produces a completely different hash. This reference either resolves to the exact same bytes every time, or fails outright if that content is no longer available — it never silently substitutes something different.
Where this matters most: supply-chain security and reproducible builds
# CI/CD pipeline, or a Kubernetes manifest, deploying a base or dependency image
image: node@sha256:a1b2c3d4e5f6789...
Digest pinning is the only mechanism that guarantees exactly what's being built or deployed. This matters for a security audit confirming precisely what code is running, a compliance requirement for reproducible builds, or just eliminating "it worked yesterday, broke today with no code changes" incidents caused by an upstream base image silently changing under an unchanged tag.
The common middle-ground practice: automated digest resolution
# A CI pipeline step that resolves a tag to its current digest at build time,
# then uses that resolved digest for the actual deployment reference --
# giving humans the readability of a tag during development, while the
# ACTUAL deployed/built reference is digest-pinned underneath
docker pull node:20.11.0-slim
docker inspect node:20.11.0-slim --format='{{index .RepoDigests 0}}'
Most teams don't hand-write digest references directly in Dockerfiles — that would be unreadable and hard to update. Instead, CI/CD automation resolves and locks in the actual digest at build time, often recording it in a lockfile-like artifact. This gives you both readable tags during development and true reproducibility for what actually ships.
The tradeoff: you lose automatic security patches
FROM node:20-slim # gets security patches automatically on rebuild, but is a moving target
FROM node@sha256:abc123... # never changes, but you must manually update this reference
# to actually receive newer base-image security patches
Digest pinning is a real tradeoff: you gain perfect reproducibility, but lose the automatic security patches you'd get from a broader, actively-maintained tag. Many teams pin digests only for final, deployed production artifacts, while still tracking broader version tags for base images used during development — with dependency-update tooling like Dependabot or Renovate opening pull requests as new patch versions become available.
Related Resources
Why architecture matters for container images
Unlike an interpreted script, a container image typically contains compiled, architecture-specific binaries. A binary compiled for amd64 (traditional Intel/AMD 64-bit) won't run on an arm64 machine — Apple Silicon Macs, AWS Graviton instances, many Raspberry Pi-class devices — and vice versa. Without multi-architecture support, an organization deploying to both x86-64 servers and ARM infrastructure would need to build, tag, and manage entirely separate images per architecture, and manually track which one to deploy where.
Building a multi-architecture image with buildx
docker buildx create --use --name multiarch-builder
docker buildx build \
--platform linux/amd64,linux/arm64 \
-t myregistry.example.com/myapp:1.0 \
--push \
.
This single command builds the image for both architectures and pushes one manifest list (sometimes called a "fat manifest") to the registry. One image reference, myapp:1.0, points at multiple architecture-specific variants underneath.
What happens when someone pulls this image
docker pull myregistry.example.com/myapp:1.0
Docker inspects the manifest list, detects the pulling machine's architecture, and fetches only the matching variant. An arm64 Mac and an amd64 cloud server both run docker pull myapp:1.0 identically, and each transparently gets the right binary. No explicit architecture selection needed.
How buildx builds for an architecture different from the build machine's own
Building an arm64 image on an amd64 machine, or vice versa, needs either cross-compilation (if the build tooling supports it) or QEMU-based emulation. buildx can set up QEMU automatically, running the build for the "foreign" architecture in an emulated environment. This is meaningfully slower than a native build, since emulation has real overhead.
For performance-sensitive multi-arch CI pipelines, some teams instead use separate native build machines per architecture — a real arm64 runner and a real amd64 runner, each building its own native variant. The results are combined into one manifest list afterward.
Why this has become increasingly important
Apple Silicon Macs (ARM-based) are now common among developers. ARM-based cloud instances, like AWS Graviton, are often cheaper and more power-efficient than equivalent x86-64 instances, and have become common in production too. An organization can no longer safely assume "everyone builds and runs on amd64." Multi-architecture support has gone from a niche concern to a practical requirement for many teams.
buildx's broader role beyond multi-arch
buildx is Docker's interface to BuildKit, its modern build engine. Multi-arch building is one of its most visible capabilities, but BuildKit/buildx also provides better build caching and build secrets, beyond what the older legacy build engine supported. For a single-architecture target, the extra complexity isn't strictly necessary — but many teams build multi-arch by default anyway, just to avoid revisiting the question later.
Related Resources
How scanning actually works
docker scout cves myapp:1.0
# or
trivy image myapp:1.0
A scanner inspects an image's layers to build an inventory of every installed package and its exact version — OS-level packages via the package manager's metadata, plus language-level dependencies like npm packages, Python packages, or Go modules, depending on the scanner. It then cross-references this inventory against vulnerability databases (like the National Vulnerability Database and vendor advisories) to report which packages have known, disclosed vulnerabilities (CVEs), each with a severity rating: critical, high, medium, or low.
myapp:1.0
Total: 12 vulnerabilities found
CRITICAL: 1
- CVE-2023-XXXXX in openssl 1.1.1k (fixed in 1.1.1t)
HIGH: 3
- CVE-2022-YYYYY in libcurl 7.68.0 (fixed in 7.74.0)
...
Why images need to be scanned repeatedly, not just once
A vulnerability report for an image can change without the image itself changing at all. A new CVE disclosed today might affect a package version that's been sitting unchanged inside an already-deployed image for months. Scanning shouldn't be a one-time gate at build time. Periodically re-scanning already-deployed images against the updated vulnerability database catches newly disclosed issues in software you already shipped and forgot about.
Integrating scanning into CI/CD
# Simplified CI pipeline step concept
- name: Scan image for vulnerabilities
run: trivy image --exit-code 1 --severity CRITICAL,HIGH myapp:${{ github.sha }}
A common practice is failing the CI build with --exit-code 1 if the scan finds vulnerabilities at or above a chosen severity. This stops a dangerous image from ever reaching a registry or production, catching the problem as early as possible — often called "shifting left."
What to actually do about a finding
Most findings resolve one of a few ways:
- Update the base image. Often the highest-leverage fix, since a newer base image tag frequently already includes patched versions of many packages.
- Update the specific affected dependency, if it's pinned to an outdated version in your own dependency manifest.
- Document and accept the risk, with a tracked exception, for the rare case where a fix isn't available yet and the vulnerable code path genuinely isn't reachable in your usage. Don't just silently ignore it.
Reducing the attack surface in the first place
Scanning detects problems — it doesn't prevent them. Pairing it with other practices reduces how much there is to find. Smaller base images, like Alpine or distroless, simply contain fewer packages. Multi-stage builds exclude build-time-only tooling from the final image. Both shrink the surface a scanner has to report on.
Registry-integrated scanning
Many registries — Docker Hub's paid tiers, GitHub Container Registry, AWS ECR, Harbor — offer built-in scanning that runs automatically on every push, with results in the registry's UI or API. This is convenient for centralizing scan results without a separate CI step. Standalone tools like Trivy stay popular because they run identically in any CI system or locally on a developer's machine, regardless of registry.