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. Thelatesttag 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 mypasswordon 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.
| Public | Private | |
|---|---|---|
| Who can pull | Anyone | Only authenticated users with access |
| Login needed to pull | No | Yes |
| Good for | Open-source tools, base images | Company apps, anything proprietary |
| Cost | Usually free | Often 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
| Registry | Host | Best when |
|---|---|---|
| Docker Hub | docker.io | You want the default, widely-known public registry; sharing open-source images |
| GitHub Container Registry (GHCR) | ghcr.io | Your code is on GitHub; you want images next to your repo and tied to GitHub permissions |
| Amazon ECR | <id>.dkr.ecr.<region>.amazonaws.com | You deploy on AWS (ECS, EKS); you want IAM-based access and low pull latency inside AWS |
| GitLab / Google Artifact Registry / Azure ACR | varies | You 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
latestas a convenience pointer only — never deploylatestto 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 likeGITHUB_TOKENrather than long-lived secrets. - Run
docker logouton 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.