Skip to content
DevOps devops containers 5 min read

Container Registries

When you build a Docker image on your laptop, it only lives on your laptop. To run that image on a server, in a teammate’s environment, or inside a CI pipeline (CI means “continuous integration”, an automated system that builds and tests your code), you need somewhere central to store it and pull it from. That central store is called a container registry (a server that holds Docker images so anyone with access can download them). This page shows you how to log in, tag, push, and pull images, and how to choose between Docker Hub, GitHub Container Registry (GHCR), and cloud registries like Amazon ECR.

What a registry actually stores

A registry stores images, organized into repositories. A repository is a named collection of versions of one image (for example myapp), and each version is identified by a tag (a label like v1.0 or latest). The full address of an image looks like this:

registry-host/namespace/repository:tag

For example ghcr.io/acme/myapp:1.4.0 means host ghcr.io, namespace (your user or org) acme, repository myapp, and tag 1.4.0. When you push, you upload an image to this address. When you pull, you download it.

If you leave the tag off, Docker assumes :latest. The latest tag is just a convention, not a real “newest” pointer. Always push explicit version tags for anything you deploy, so you know exactly what is running.

Logging in

Before you can push to a private repository, you must authenticate. The docker login command stores a token so future pushes and pulls work without asking again.

docker login

It prompts for your username and password (or token), then saves credentials to ~/.docker/config.json.

Output:

Username: jsingh
Password:
Login Succeeded

For GHCR and other registries you pass the host explicitly:

docker login ghcr.io -u jsingh

When to use a password vs a token: Never type your real account password into automated scripts. Generate a personal access token (a revocable credential scoped to specific permissions) instead. For Docker Hub, create one under Account Settings > Security. For GHCR, create a GitHub token with the write:packages scope. You can pipe the token in non-interactively, which is what CI does:

echo "$GHCR_TOKEN" | docker login ghcr.io -u jsingh --password-stdin

Avoid docker login -p mypassword on the command line — the password is saved in your shell history (~/.bash_history) and visible to other users in the process list. Always use --password-stdin.

Tagging an image

An image must carry the registry address in its name before you can push it. You apply that name with docker tag. Suppose you built an image locally called myapp:1.0:

docker images

Output:

REPOSITORY   TAG   IMAGE ID       CREATED         SIZE
myapp        1.0   8f2a9c1d3e4b   2 minutes ago   142MB

Tag it for Docker Hub (replace jsingh with your username):

docker tag myapp:1.0 docker.io/jsingh/myapp:1.0

Or for GHCR:

docker tag myapp:1.0 ghcr.io/jsingh/myapp:1.0

Tagging does not copy the image — it just adds another name pointing to the same image ID.

Pushing and pulling

Once tagged and logged in, push uploads the image:

docker push ghcr.io/jsingh/myapp:1.0

Output:

The push refers to repository [ghcr.io/jsingh/myapp]
5f70bf18a086: Pushed
a1b2c3d4e5f6: Pushed
1.0: digest: sha256:9c4e... size: 1788

On another machine or server, pull it back down:

docker pull ghcr.io/jsingh/myapp:1.0

Then run it like any other image:

docker run -d -p 8080:8080 ghcr.io/jsingh/myapp:1.0

Public vs private repositories

A public repository can be pulled by anyone without logging in — good for open-source images and base images. A private repository requires authentication to pull, which is what you want for proprietary application code.

PublicPrivate
Who can pullAnyoneOnly authenticated users with access
Login needed to pullNoYes
Good forOpen-source tools, base imagesCompany apps, anything proprietary
CostUsually freeOften metered or seat-based

On Docker Hub and GHCR a new repository is created automatically the first time you push to it; you set it public or private in the web UI afterward (GHCR defaults new packages to private).

Choosing a registry

RegistryHostBest when
Docker Hubdocker.ioYou want the default, widely-known public registry; sharing open-source images
GitHub Container Registry (GHCR)ghcr.ioYour code is on GitHub; you want images next to your repo and tied to GitHub permissions
Amazon ECR<id>.dkr.ecr.<region>.amazonaws.comYou deploy on AWS (ECS, EKS); you want IAM-based access and low pull latency inside AWS
GitLab / Google Artifact Registry / Azure ACRvariesYou are already on that platform’s CI and cloud

Amazon ECR (Elastic Container Registry) is AWS’s private registry. Login uses the AWS CLI to fetch a temporary token rather than a static password:

aws ecr get-login-password --region us-east-1 \
  | docker login --username AWS --password-stdin 123456789012.dkr.ecr.us-east-1.amazonaws.com

Then tag and push to the ECR address. Unlike Docker Hub, ECR does not auto-create repositories — create one first with aws ecr create-repository --repository-name myapp.

Pushing from CI

The real payoff of a registry is automation: every time you merge code, CI builds a fresh image and pushes it, so your servers can pull a known-good version. Here is a GitHub Actions workflow that logs into GHCR and pushes. ${{ }} are GitHub’s variables; secrets.GITHUB_TOKEN is provided automatically and has package-write access.

name: build-and-push
on:
  push:
    branches: [main]
jobs:
  build:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
    steps:
      - uses: actions/checkout@v4
      - name: Log in to GHCR
        run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin
      - name: Build image
        run: docker build -t ghcr.io/${{ github.repository }}:${{ github.sha }} .
      - name: Push image
        run: docker push ghcr.io/${{ github.repository }}:${{ github.sha }}

Tagging with ${{ github.sha }} (the commit’s unique ID) gives every build a unique, traceable tag, so you can always trace a running container back to the exact commit that produced it.

Best Practices

  • Use revocable access tokens with the narrowest scope, never your account password, and always feed them via --password-stdin.
  • Push immutable, explicit tags (a version or commit SHA), and treat latest as a convenience pointer only — never deploy latest to production.
  • Keep proprietary code in private repositories; double-check the visibility setting right after the first push.
  • In CI, give the job the minimum permissions it needs (packages: write) and rely on platform-provided tokens like GITHUB_TOKEN rather than long-lived secrets.
  • Run docker logout on shared or temporary machines so credentials don’t linger in ~/.docker/config.json.
  • Periodically prune old tags to control storage costs, and enable vulnerability scanning if your registry offers it.
Last updated June 15, 2026
Was this helpful?