Building an image is only half the story. You need to name it, store it, and distribute it to the machines that will run it. This lesson covers the full naming convention, how registries work, and the mechanics of push and pull.

1. Image Naming Convention

Every Docker image reference follows this pattern:

registry/repository:tag

For example: docker.io/library/nginx:1.25-alpine

docker.io / library/nginx : 1.25-alpine Registry (default: docker.io) Repository (namespace/image) Tag (default: latest) When you type "nginx", Docker interprets it as docker.io/library/nginx:latest
PartExampleDefault
Registrydocker.io, ghcr.io, 123456.dkr.ecr.us-east-1.amazonaws.comdocker.io
Repositorylibrary/nginx, myuser/myapplibrary/ (for official images)
Tag1.25-alpine, v2.3.1, latestlatest

2. Tags Are Mutable Pointers

A tag is just a label that points to a manifest digest. It can be moved at any time. There is nothing special about :latest — it is simply the default tag applied when you don't specify one.

# Build and tag
$ docker build -t myapp:v1.0 .

# Push — the tag "v1.0" now points to digest sha256:abc123...
$ docker push myapp:v1.0

# Rebuild with a fix, re-tag, re-push
$ docker build -t myapp:v1.0 .
$ docker push myapp:v1.0
# ⚠️ Now "v1.0" points to sha256:def456... — the OLD image is orphaned!
⚠️ Why This Is Dangerous for Production

If you deploy myapp:v1.0 and someone pushes a new image under the same tag, your next pod restart will pull a different image. You've lost reproducibility. For production, pin by digest.

3. Digests — The Immutable Truth

Every image manifest has a content-addressable SHA-256 digest. Unlike tags, digests are immutable — they always refer to the exact same bytes.

# Pull by digest — guaranteed to be the same image forever
$ docker pull nginx@sha256:6a5bacc77c1a5c25c5e6a1a2b3e0c4f5a6b7c8d9e0f1234567890abcdef12345

# Get the digest of a local image
$ docker inspect --format='{{index .RepoDigests 0}}' nginx:1.25-alpine
nginx@sha256:6a5bacc77c1a...
Use CaseUse Tag?Use Digest?
Local development✅ ConvenientUsually overkill
CI/CD pipelines✅ For human readability✅ Pin base images
Production KubernetesFor reference only✅ Always pin
Shared base imagesMajor version tags✅ Exact reproducibility

4. Registries

A registry is an HTTP service that stores image manifests and layer blobs. Here's how the major options compare:

RegistryOperatorFree TierPrivate ReposNotable Features
Docker HubDocker Inc.1 private repoOfficial images, rate limits (100 pulls/6h anonymous)
GHCRGitHub500 MB freeTight GitHub Actions integration, GITHUB_TOKEN auth
Amazon ECRAWS500 MB/monthIAM auth, image scanning, lifecycle policies
Google Artifact RegistryGCP500 MB freeMulti-format (Docker, npm, Maven), vulnerability scanning
Azure ACRMicrosoftBasic tierGeo-replication, ACR Tasks (in-cloud builds)
Quay.ioRed HatUnlimited publicSecurity scanning, robot accounts
HarborSelf-hosted (CNCF)Free (you host)RBAC, replication, vulnerability scanning, signing

5. Push & Pull Mechanics

Understanding what happens on the wire helps you optimise build times and storage costs.

Docker Client Registry ── PUSH ── Layer A Layer B Layer C Already exists! (skipped) Manifest uploaded LAST ── PULL ── Manifest first Then only missing layers (parallel)

Key insight: Layer deduplication means if 10 images share the same base layers, those layers are stored and transferred only once. This is why choosing common base images saves enormous bandwidth and storage.

6. Multi-Architecture Images

A single tag can serve different platforms. When you run docker pull nginx on an ARM Mac vs an x86 Linux server, you get different binaries — same tag.

This works via a manifest list (Docker) or OCI index — a meta-manifest that maps platforms to their specific image manifests:

# Inspect the manifest list
$ docker manifest inspect nginx:1.25-alpine
{
  "manifests": [
    { "platform": { "architecture": "amd64", "os": "linux" }, "digest": "sha256:aaa..." },
    { "platform": { "architecture": "arm64", "os": "linux" }, "digest": "sha256:bbb..." },
    { "platform": { "architecture": "arm",   "os": "linux" }, "digest": "sha256:ccc..." }
  ]
}
# Build multi-arch with buildx
$ docker buildx create --name multibuilder --use
$ docker buildx build --platform linux/amd64,linux/arm64 \
    -t myuser/myapp:v2.0 --push .

7. Private Registries & Auth

Authentication follows the Docker Registry v2 token-based flow:

# Login (stores credential in ~/.docker/config.json)
$ docker login ghcr.io
Username: myuser
Password: ***

# Credential helpers store tokens in OS keychain
# ~/.docker/config.json:
{
  "credHelpers": {
    "gcr.io": "gcloud",
    "123456.dkr.ecr.us-east-1.amazonaws.com": "ecr-login"
  }
}
# Kubernetes: create an image pull secret
$ kubectl create secret docker-registry regcred \
    --docker-server=ghcr.io \
    --docker-username=myuser \
    --docker-password=$GITHUB_TOKEN

# Reference in a pod spec:
# spec.imagePullSecrets:
#   - name: regcred
🔧 Not Just Docker: Skopeo

Skopeo can copy images between registries without pulling them locally. This is invaluable for mirroring, air-gapped environments, and CI pipelines that move images between staging and production registries.

# Copy from Docker Hub to GHCR — no local docker daemon needed
$ skopeo copy \
    docker://docker.io/library/nginx:1.25-alpine \
    docker://ghcr.io/myorg/nginx:1.25-alpine

# Inspect a remote image without pulling
$ skopeo inspect docker://docker.io/library/nginx:latest
🏢 Industry Practice: Private Registries at Scale

Most companies run private registries (Harbor, JFrog Artifactory) for security and compliance reasons:

  • Vulnerability scanning — block images with critical CVEs from being deployed
  • Signing & trust — only deploy images signed by authorised CI pipelines (cosign/Notary)
  • Replication — mirror images across regions for low-latency pulls
  • Audit trails — track who pushed what, when, for compliance (SOC2, HIPAA)
  • Promotion workflows — images move dev → staging → prod registries after passing gates

Hands-On Tasks

🛠️ Task 1: Tag & Push Multiple Tags
# Create a simple image
$ echo "FROM alpine:3.18" | docker build -t myapp:v1.0.0 -

# Add additional tags
$ docker tag myapp:v1.0.0 myapp:v1.0
$ docker tag myapp:v1.0.0 myapp:v1
$ docker tag myapp:v1.0.0 myapp:latest

# Verify all tags point to the same image ID
$ docker images myapp
REPOSITORY   TAG       IMAGE ID       CREATED        SIZE
myapp        latest    a1b2c3d4e5f6   2 seconds ago  7.34MB
myapp        v1        a1b2c3d4e5f6   2 seconds ago  7.34MB
myapp        v1.0      a1b2c3d4e5f6   2 seconds ago  7.34MB
myapp        v1.0.0    a1b2c3d4e5f6   2 seconds ago  7.34MB

# Push to a local registry
$ docker run -d -p 5000:5000 --name registry registry:2
$ docker tag myapp:v1.0.0 localhost:5000/myapp:v1.0.0
$ docker push localhost:5000/myapp:v1.0.0
🛠️ Task 2: Pull by Digest vs Tag
# Get the digest of an image
$ docker inspect --format='{{index .RepoDigests 0}}' alpine:3.18
alpine@sha256:abc123def456...

# Pull by tag (mutable — could change tomorrow)
$ docker pull alpine:3.18

# Pull by digest (immutable — always the same image)
$ docker pull alpine@sha256:abc123def456...

# Compare: both should have the same image ID right now
$ docker images --digests alpine

Knowledge Check

Key Takeaways

  • Image references follow registry/repository:tag — know what each part means and what the defaults are.
  • Tags are mutable — they can be overwritten. Never trust a tag alone for production reproducibility.
  • Digests are immutable — use them to pin exact image versions in production.
  • Registries are just HTTP services storing manifests and blobs — you can self-host one with a single docker run.
  • Push uploads layers individually (with dedup), then the manifest last. Pull fetches the manifest first, then only missing layers.
  • Multi-architecture images use manifest lists so one tag serves amd64, arm64, etc.
  • Use credential helpers and image pull secrets — never put registry passwords in plain text.