Saltar a contenido

11 - Image Registries

What this session is

About 30 minutes. How to share images: log in to a registry, tag for upload, push, pull. The two big ones - Docker Hub and GitHub Container Registry (GHCR).

What a registry is

A registry is just a server that stores images. docker pull alpine contacts Docker Hub. docker pull ghcr.io/owner/image contacts GitHub Container Registry. Any compliant server can host a registry; you can even run one yourself.

The biggest public registries:

Registry URL prefix Notes
Docker Hub docker.io/ (often omitted) The default; largest ecosystem
GitHub Container Registry ghcr.io/ Free for OSS; tightly integrated with GitHub
Google Artifact Registry <region>-docker.pkg.dev/ Cloud-native
AWS ECR <account>.dkr.ecr.<region>.amazonaws.com/ AWS-specific
Azure Container Registry <name>.azurecr.io/ Azure-specific

For your first contributions, Docker Hub and GHCR are the relevant ones.

Image references, full form

[REGISTRY]/[NAMESPACE]/[NAME]:[TAG]@[DIGEST]
  • REGISTRY - defaults to docker.io when omitted.
  • NAMESPACE - your username, org, or library (for official images).
  • NAME - the image name.
  • TAG - version label.
  • DIGEST - content hash (immutable).

Examples (same image, four ways):

nginx
nginx:latest
docker.io/library/nginx:latest
docker.io/library/nginx@sha256:abc...

When you docker pull, the short forms work; the daemon fills in defaults.

Log in

docker login                       # Docker Hub
docker login ghcr.io               # GitHub Container Registry
docker login <other-registry>      # other

For Docker Hub, use your dockerhub username + password (or a personal access token - recommended).

For GHCR, use your GitHub username + a Personal Access Token (PAT) with write:packages scope. Generate one at github.com/settings/tokens.

Your credentials are stored in ~/.docker/config.json. For security, modern Docker uses your OS keychain on macOS/Windows by default.

Tag an image for upload

To push to a registry, the image must be tagged with the registry's prefix:

docker tag myimage:1.0 myname/myimage:1.0           # Docker Hub
docker tag myimage:1.0 ghcr.io/myname/myimage:1.0   # GHCR

docker tag SOURCE TARGET doesn't copy - it adds a new label to the same image. After tagging, both names refer to the same image. Remove either via docker rmi; the underlying image stays as long as one tag points to it.

Push

docker push myname/myimage:1.0
docker push ghcr.io/myname/myimage:1.0

Watches each layer upload (only changed layers transfer - Docker compares hashes).

After a push, visit Docker Hub or GHCR in your browser. You should see the image. Add a README on Docker Hub's UI; verify visibility (public vs private - check the settings).

Pull on another machine

docker pull myname/myimage:1.0

If private, you need to docker login first.

Multi-arch images

Modern registries support multi-arch manifests: one tag points to several architecture-specific images. nginx:1.27 resolves to the right one for your host (amd64, arm64, etc.).

Build multi-arch yourself with docker buildx:

docker buildx create --name multiarch --use
docker buildx build --platform linux/amd64,linux/arm64 -t myname/myimage:1.0 --push .

That builds both architectures in one go and pushes a single multi-arch manifest. Useful when you publish for both x86 servers and ARM (Macs, Raspberry Pis).

Pull-through caches

Big setups run a local registry as a pull-through cache - pulls from your local one, which only contacts Docker Hub on cache misses. Lessens hit on Docker Hub rate limits (free Docker Hub limits per-IP pull rate); faster pulls in your network.

registry:2 is the official open-source image. Run it; configure your Docker daemon (daemon.json) to use it. Beyond beginner; recognize the pattern.

Self-hosted registries

docker run -d -p 5000:5000 registry:2 runs a private registry on localhost:5000. Push/pull:

docker tag myimage:1.0 localhost:5000/myimage:1.0
docker push localhost:5000/myimage:1.0
docker pull localhost:5000/myimage:1.0

For shared internal use, you'd also want TLS, auth, and storage backed by something durable (S3, GCS). The defaults are insecure-by-design for local-only testing.

Public vs private

Both Docker Hub and GHCR support both. By default:

  • Docker Hub: new repos are public unless you have a paid plan.
  • GHCR: inherits the parent repo's visibility (public if your repo is public, private if not). For org-owned images, configure in package settings.

Public images can be pulled by anyone, no auth. Private require docker login.

CI: building and pushing on every commit

A typical GitHub Actions workflow:

name: Build and push image
on: { push: { branches: [main] } }
jobs:
  build:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
    steps:
      - uses: actions/checkout@v4
      - uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
      - uses: docker/build-push-action@v6
        with:
          context: .
          push: true
          tags: ghcr.io/${{ github.repository }}:latest

Every push to main rebuilds and pushes to ghcr.io/owner/repo:latest. You'll see this exact pattern in many OSS Rust/Go/Python projects.

Exercise

You need a Docker Hub account (free at hub.docker.com) and/or a GitHub PAT for GHCR.

  1. Tag and push to Docker Hub:

    docker login                                           # log in
    docker tag pyapp:1.0 <your-dockerhub-user>/pyapp:1.0   # tag
    docker push <your-dockerhub-user>/pyapp:1.0
    
    Open Docker Hub in a browser. Find your image.

  2. Pull from another tag:

    docker rmi <your-dockerhub-user>/pyapp:1.0             # remove local
    docker pull <your-dockerhub-user>/pyapp:1.0
    docker run --rm <your-dockerhub-user>/pyapp:1.0
    

  3. Push to GHCR:

    docker login ghcr.io                                   # use a GitHub PAT
    docker tag pyapp:1.0 ghcr.io/<your-gh-user>/pyapp:1.0
    docker push ghcr.io/<your-gh-user>/pyapp:1.0
    
    Visit GitHub → your profile → Packages. Find the image. Optionally make it public from the package settings.

  4. Pull-through cache (advanced):

    docker run -d --restart=always --name cache -p 5000:5000 \
      -e REGISTRY_PROXY_REMOTEURL=https://registry-1.docker.io \
      registry:2
    docker pull localhost:5000/library/alpine        # pulls via your local cache
    

What you might wonder

"What's the difference between docker push and the image actually appearing?" After push, Docker Hub processes the upload; usually visible immediately. GHCR shows it under your packages quickly too.

"How do I delete images from a registry?" Docker Hub: via the web UI. GHCR: via GitHub Packages UI. Programmatically via each registry's API. There's no docker rm for remote images; pushing again with the same tag overwrites.

"What about image signing?" For real supply-chain integrity, sign images with cosign (sigstore). Verify at deploy. The "Container Internals" senior reference path covers this. Beyond beginner.

"Docker Hub rate limits?" Free anonymous pulls: ~100/6 hours per IP. Logged-in free: 200/6h. Paid plans: higher or unlimited. For CI on free tier, log in or use a registry mirror.

Done

  • Tag images for a registry.
  • Push to Docker Hub and GHCR.
  • Pull (private images need docker login first).
  • Recognize multi-arch images.
  • Know about pull-through caches and self-hosted registries.

Next: Reading other people's Dockerfiles →

Comments