Registries: Docker Hub & GitHub Container Registry
An image on your laptop is useful only to you. To run it on a server, share it with your team or deploy it from a CI pipeline, you put it in a registry: a server that stores images and lets authorised people push and pull them. You have been pulling from Docker Hub since your first docker run. Now you will push your own images.
This lesson covers the two registries most developers start with, Docker Hub and GitHub Container Registry (GHCR), plus a tagging plan that keeps deployments safe.
How registries are organised
A registry holds repositories, and each repository holds many tags of one image. The image's full name tells Docker which registry to talk to:
| Image name | Registry | Repository | Tag |
|---|---|---|---|
mysql:8.4 | Docker Hub | library/mysql | 8.4 |
priyasharma/medical-shop:2.1.0 | Docker Hub | priyasharma/medical-shop | 2.1.0 |
ghcr.io/priya-sharma/medical-shop:2.1.0 | GHCR | priya-sharma/medical-shop | 2.1.0 |
To push an image, its name must start with a registry and namespace you are allowed to write to. You do not rebuild the image for this; you add another tag (another name) to the same image with docker tag.
Pushing to Docker Hub
Create a free account at hub.docker.com. Then, instead of your account password, create a personal access token in your account settings. A token can be limited (for example read and write, but not delete), shown once, and revoked at any time without changing your password.
echo "$DOCKERHUB_TOKEN" | docker login -u priyasharma --password-stdin
docker tag medical-shop:2.1.0 priyasharma/medical-shop:2.1.0
docker push priyasharma/medical-shop:2.1.0Login Succeeded The push refers to repository [docker.io/priyasharma/medical-shop] 6a8d3f1c9e02: Pushed b4e2c7a1d953: Pushed 2d35ebdb57d9: Mounted from library/node 2.1.0: digest: sha256:e3b7c9a1f0d24c6b8a5e7f3d1c9b0a2e4f6d8c1a3b5e7f9d0c2a4b6e8f1a3c5d size: 856
--password-stdin reads the token from standard input, so it does not appear in your shell history or in the list of running processes. Look at the push output: only your own layers were uploaded. The Node.js base layer says Mounted from library/node, because Docker Hub already had it and simply linked it. Pushing a new version of your app later uploads only the layers that changed.
Running docker login with no options opens a web-based login instead: it shows a one-time code and asks you to confirm it in the browser. That is convenient on your laptop; tokens with --password-stdin are the way to log in from scripts and servers.
On the server, anyone you allow can now run:
docker pull priyasharma/medical-shop:2.1.0New Docker Hub repositories are public unless you choose private. A public image can be pulled by anyone in the world, so never put secrets, private code you cannot share or customer data in it.
Pull rate limits
Docker Hub limits how many images can be pulled. Today, anonymous users get 100 pulls per 6 hours per IPv4 address (or per IPv6 /64 subnet), and logged-in users on a free personal account get 200 per 6 hours. A busy CI system or a classroom of students behind one college IP address can hit the anonymous limit quickly, and pulls then fail with a "toomanyrequests" error. Logging in, even with a free account, raises the limit. Docker can change these numbers, so check the Docker Hub documentation for current values.
Pushing to GitHub Container Registry
GHCR stores images next to your code on GitHub, at ghcr.io/OWNER/IMAGE. Its big advantage is in CI: a GitHub Actions workflow can push to it with its built-in token, with no extra account. From your own computer, log in with a GitHub personal access token (classic) that has the write:packages scope:
echo "$GHCR_TOKEN" | docker login ghcr.io -u priya-sharma --password-stdin
docker tag medical-shop:2.1.0 ghcr.io/priya-sharma/medical-shop:2.1.0
docker push ghcr.io/priya-sharma/medical-shop:2.1.0Things to know about GHCR:
- The owner and image names must be lowercase. A GitHub user
Priya-Sharmamust still writeghcr.io/priya-sharma/.... - A new package is private by default. Change its visibility in the package settings on GitHub if you want a public image.
- Add the label
org.opencontainers.image.sourcewith your repository URL in the Dockerfile, and GitHub links the package to that repository automatically:
LABEL org.opencontainers.image.source=https://github.com/priya-sharma/medical-shopA tagging plan for deployments
Tags are how you choose which version runs where. A simple, safe plan:
- A version tag for every release, such as
2.1.0. Treat it as permanent: never push different content to an existing version tag. - A Git commit tag, such as
sha-4f2a9c1, so you can trace any running image back to the exact code. - Moving tags such as
2.1,2orlatestfor convenience, but deploy servers with exact version tags (or digests), never withlatest.
Pushing several tags of one image is cheap: the layers are uploaded once.
docker tag medical-shop:2.1.0 priyasharma/medical-shop:2.1
docker tag medical-shop:2.1.0 priyasharma/medical-shop:latest
docker push --all-tags priyasharma/medical-shopOther registries
Every cloud has its own registry (Amazon ECR, Google Artifact Registry, Azure Container Registry), and GitLab has one built in. They all work the same way: log in, tag with the registry's host name, push. You can even run a private registry yourself with the official registry image, for example docker run -d -p 5000:5000 --name registry registry:3, and push to localhost:5000/medical-shop:2.1.0.
Common mistakes
- "denied: requested access to the resource is denied". The image name does not start with your namespace (
priyasharma/...), or you are not logged in to that registry. - Uppercase letters in GHCR names. Repository names must be lowercase.
- Logging in with your account password. Use an access token that you can limit and revoke.
- Overwriting a version tag. If
2.1.0changes after release, servers that pulled it earlier run different code. Publish2.1.1instead. - Pushing an image with secrets in it. Check
.dockerignoreand use build secrets before your first push.
Key points
- A registry stores repositories of tagged images; the image name decides which registry is used.
docker taggives an image a registry-qualified name;docker pushuploads only layers the registry lacks.- Log in with access tokens and
--password-stdin; Docker Hub needs a namespace likepriyasharma/, GHCR usesghcr.io/owner/. - Docker Hub limits pulls, and logged-in users get a higher limit than anonymous ones.
- Release with permanent version tags plus commit tags; deploy exact versions, not
latest.
Exercise
Kabir has built an image fees-api:1.4.0 on his laptop. His GitHub username is KabirMehta and his repository is KabirMehta/fees-api. Write the commands to log in to GHCR with a token stored in the variable GHCR_TOKEN, tag the image as version 1.4.0 and latest, and push both tags. Also write the Dockerfile line that links the package to his repository.
Show solution
echo "$GHCR_TOKEN" | docker login ghcr.io -u KabirMehta --password-stdin
docker tag fees-api:1.4.0 ghcr.io/kabirmehta/fees-api:1.4.0
docker tag fees-api:1.4.0 ghcr.io/kabirmehta/fees-api:latest
docker push --all-tags ghcr.io/kabirmehta/fees-apiLABEL org.opencontainers.image.source=https://github.com/KabirMehta/fees-apiThe login user name may keep its capitals, but the image path must be lowercase. --all-tags pushes both tags, and the shared layers are uploaded only once.
Quick Quiz
1. Why does a push show Mounted from library/node for some layers?
Layers are shared, so only new layers are sent.
2. Why use --password-stdin with docker login?
Passing secrets as command-line arguments exposes them.
3. Which GHCR image name is valid?
GHCR uses the host ghcr.io and requires lowercase names.
4. Which tag should a production server deploy?
Exact versions make deployments repeatable and easy to roll back.
Learn Docker & DevOps with a mentor
Free lessons get you started. Our Full Stack Web Development training adds real projects and code reviews from working developers.
- Project-based learning
- Mentor code reviews
- Free counselling call
