Share your image on Docker Hub
Name your image for a registry, sign in, and push it to Docker Hub with a version tag and latest, then pull it back and run it the way anyone else would.
What you'll learn
- Read an image name as registry, namespace, repository, and tag
- Sign in to Docker Hub with
docker login - Name an image for your namespace with
docker tag - Push a version tag and
latestwithdocker push - Pull and run your published image like any other
- Use the same commands with another registry, such as GitHub Container Registry
Your image works on your laptop. That’s also the only place it exists. A teammate who wants to try it, a CI job that needs to test it, and the server that will run it in production can’t reach into your machine’s image store. You could export it to a file and copy that around, but then every update means another copy, and nobody can tell which file is current.
A registry solves this. It’s a server that stores images and hands them out by name, so anyone with access runs your app with one command, and an update is just a new push.
In this guide, you’ll push a to-do app’s image to Docker Hub, under your own
account, as version 1.0 and as latest. Then you’ll pull it back and run it
the way someone else would, and see how the same commands work with other
registries.
Before you start
You can follow this whole guide in your browser. The terminals on this page are simulations: click through them to see what each command does.
To follow along on your own machine, you need:
-
Docker. Install Docker Desktop or Docker Engine.
-
A Docker account. A free Personal account is enough. Sign up if you don’t have one. Your username is your Docker Hub namespace, and you’ll type it in place of
<YOUR_DOCKER_USERNAME>throughout. The simulations use the example usernamemobywhale. -
An image called
todo-app. If you’ve built the sample app before, you already have one. If not, build it straight from its Git repository. It takes about a minute:$ docker build -t todo-app https://github.com/docker/getting-started-todo-app.gitAny image works the same way. To push your own instead, substitute its name for
todo-app.
Step 1: Read an image name
Prefer to watch? This short video explains registries and repositories. It predates this guide, so it signs in through Docker Desktop and uses a different sample app.
When you run docker push, you don’t tell Docker where to send the image. The
image’s name does. A full image name has four parts:
- Registry. The server that stores the image. Leave it out and Docker
assumes Docker Hub,
docker.io. - Namespace. Who owns the repository: on Docker Hub, your username or an organization you belong to. You can only push to a namespace you have access to.
- Repository. One image and all its versions, such as
todo-app. - Tag. One version within the repository. Leave it out and Docker assumes
latest.
So todo-app on its own is short for docker.io/library/todo-app:latest.
library is the namespace Docker Hub reserves for
Docker Official Images,
and you can’t push to it. That’s why the image needs a new name, with your
namespace in it, before it can go anywhere.
- In
mobywhale/todo-app:1.0,mobywhaleis the namespace. No registry is named, so Docker assumesdocker.io. - In
ghcr.io/mobywhale/todo-app:1.0,ghcr.iois the registry. A first part with a dot or a port in it, orlocalhost, is a registry hostname. - In
todo-app,latestis the tag. It isn’t written, but a name with no tag meanslatest. docker push todo-appis refused. It meansdocker.io/library/todo-app:latest, andlibraryis reserved for Docker Official Images.
Step 2: Decide who can pull it
On Docker Hub, every repository is either public or private:
- Public repositories appear in Docker Hub search, and anyone can pull them without signing in.
- Private repositories are hidden from search. Only you, your collaborators, and members of your organization with the right role can pull them.
You don’t have to create the repository before you push. When you push to a
repository that doesn’t exist yet, Docker Hub creates it with your
namespace’s default privacy setting. To choose the visibility and add a
description up front, create it first: sign in to
Docker Hub, select My Hub > Repositories >
Create repository, and name it todo-app. See
Create a repository for
every option.
Step 3: Sign in
Pulling a public image needs no account, but pushing does: Docker Hub has to know you own the namespace. Sign in from your terminal:
$ docker loginBy default, docker login signs you in through your browser. It prints a
one-time code and waits while you confirm it:
USING WEB-BASED LOGIN
i Info → To sign in with credentials on the command line, use 'docker login -u <username>'
Your one-time device confirmation code is: WDHB-QCXP
Press ENTER to open your browser or submit your device code here: https://login.docker.com/activate
Waiting for authentication in the browser…Press Enter, sign in, and confirm the code. Back in the terminal, Docker
prints Login Succeeded and stores the credential, so you only sign in once
per machine. If you use Docker Desktop and are already signed in there, your
terminal is signed in too.
Step 4: Name the image for your namespace
Prefer to watch? This video covers tagging and pushing, plus the build that comes before it. It predates this guide and uses a different image name.
Give the image a second name that includes your namespace and a version:
Host machine
docker tag todo-app <YOUR_DOCKER_USERNAME>/todo-app:1.0
docker image ls
IMAGE ID DISK USAGE CONTENT SIZE EXTRA mobywhale/todo-app:1.0 b3bd5c5db5a7 1.67GB 413MB U todo-app:latest b3bd5c5db5a7 1.67GB 413MB U
Look at the ID column: both names point at the same image. docker tag
doesn’t copy anything, which is why it prints nothing and finishes instantly.
A tag is a label, and an image can carry as many as you like.
Your sizes will differ, depending on how the image was built.
Step 5: Push it
Host machine
docker push <YOUR_DOCKER_USERNAME>/todo-app:1.0
The push refers to repository [docker.io/mobywhale/todo-app] 4cc914071dc3: Pushed 03b5c7f03493: Pushed 44136fa355b3: Pushed aaeb1b620bf2: Pushed a97cb698362f: Pushed 972bf98a819b: Pushed 427bfb6ff897: Pushed 76a3527c6bae: Pushed 62800baee39b: Pushed a28a9732d8cd: Pushed 24a65d53d507: Pushed 6a3f500343e7: Pushed 580f3b942ac0: Pushed 8ef3336e0622: Pushed 20528b52e727: Pushed 2a28edfb803d: Pushed 87ebb0f22f55: Pushed 1.0: digest: sha256:b3bd5c5db5a7dc1706e2c21f2566890ceb38b91072075ae2fae2e755c5d05598 size: 856
Docker uploads the image one layer at a time. Each line is a layer, named by
the start of its content hash. On Docker Hub, some layers may say
Mounted from library/node instead of Pushed: Docker Hub already has those
layers from the node base image, so it links them instead of storing a
second copy.
The last line is the digest, a hash of the image’s contents. The tag
1.0 now points at that digest in your repository. Open
your repository on Docker Hub and select the
Tags tab to see it.
Step 6: Push latest too
People who don’t care about versions will run <YOUR_DOCKER_USERNAME>/todo-app
with no tag, which means latest. Right now, there’s no latest in your
repository, so that command would fail. Tag the same image again and push it:
Host machine
docker tag <YOUR_DOCKER_USERNAME>/todo-app:1.0 <YOUR_DOCKER_USERNAME>/todo-app:latest
docker image ls <YOUR_DOCKER_USERNAME>/todo-app
IMAGE ID DISK USAGE CONTENT SIZE EXTRA mobywhale/todo-app:1.0 b3bd5c5db5a7 1.67GB 413MB U mobywhale/todo-app:latest b3bd5c5db5a7 1.67GB 413MB U
docker push <YOUR_DOCKER_USERNAME>/todo-app:latest
The push refers to repository [docker.io/mobywhale/todo-app] 4cc914071dc3: Already exists a97cb698362f: Layer already exists 8ef3336e0622: Layer already exists 972bf98a819b: Layer already exists 2a28edfb803d: Layer already exists 44136fa355b3: Already exists 76a3527c6bae: Layer already exists 62800baee39b: Layer already exists 24a65d53d507: Layer already exists 580f3b942ac0: Layer already exists aaeb1b620bf2: Layer already exists a28a9732d8cd: Layer already exists 20528b52e727: Layer already exists 6a3f500343e7: Layer already exists 87ebb0f22f55: Layer already exists 03b5c7f03493: Layer already exists 427bfb6ff897: Layer already exists latest: digest: sha256:b3bd5c5db5a7dc1706e2c21f2566890ceb38b91072075ae2fae2e755c5d05598 size: 856
This push uploads nothing. Every layer already exists in the repository, so Docker only adds the new tag. Both tags end with the same digest, because they name the same image.
That’s the usual release habit: push each release under its own version tag,
and move latest to the newest one. When you ship 1.1, 1.0 still points
at today’s image, so anyone who pinned it keeps getting exactly what they
tested.
Step 7: Run it from the registry
Now run the image the way a teammate would, by name from Docker Hub. First, remove the two local tags, so Docker has to ask the registry:
Host machine
docker image rm <YOUR_DOCKER_USERNAME>/todo-app:1.0 <YOUR_DOCKER_USERNAME>/todo-app:latest
Untagged: mobywhale/todo-app:1.0 Untagged: mobywhale/todo-app:latest
docker pull <YOUR_DOCKER_USERNAME>/todo-app:1.0
1.0: Pulling from mobywhale/todo-app Digest: sha256:b3bd5c5db5a7dc1706e2c21f2566890ceb38b91072075ae2fae2e755c5d05598 Status: Downloaded newer image for mobywhale/todo-app:1.0 docker.io/mobywhale/todo-app:1.0
The digest matches the one from your push, so this is the exact image you
uploaded. The pull is quick here because your machine still has the layers
under the todo-app name. On a machine that’s never seen the image, you’ll
see each layer download first.
Run it and check that it answers:
Host machine
docker run -d -p 3000:3000 --name todo <YOUR_DOCKER_USERNAME>/todo-app:1.0
8113c18f8b39b33a0bc9466f95d51a8fdb87b58f2889237eb3b8ec32ee120993
curl http://localhost:3000/api/greeting
{"greeting":"Hello world!"}Open http://localhost to use the app. On any
other machine with Docker, that same docker run command is all it takes. If
the repository is private, that machine has to docker login first, with an
account that has access.
When you’re done, remove the container and your local tags:
$ docker rm -f todo
$ docker image rm <YOUR_DOCKER_USERNAME>/todo-app:1.0To delete the repository itself, see Delete a repository.
Push to other registries
Docker Hub is the default, not the only choice. GitHub, every major cloud, and self-hosted products such as Harbor and JFrog Artifactory all run registries that speak the same protocol. The commands don’t change. Only the name does, because it now starts with the registry’s hostname:
Docker Hub <username>/todo-app:1.0
GitHub Container Registry ghcr.io/<owner>/todo-app:1.0
Amazon ECR <account-id>.dkr.ecr.<region>.amazonaws.com/todo-app:1.0
Azure Container Registry <registry-name>.azurecr.io/todo-app:1.0
Google Artifact Registry <region>-docker.pkg.dev/<project>/<repository>/todo-app:1.0Each registry has its own way to get credentials. You pass its hostname to
docker login, then tag and push as before. For GitHub Container Registry:
$ docker login ghcr.io -u <YOUR_GITHUB_USERNAME>
$ docker tag todo-app ghcr.io/<YOUR_GITHUB_USERNAME>/todo-app:1.0
$ docker push ghcr.io/<YOUR_GITHUB_USERNAME>/todo-app:1.0Choose what you build on
Anyone who pulls your image also gets everything in it, including its base
image: the FROM line in your Dockerfile. When you publish, that choice
becomes your users’ problem too, so it’s worth making deliberately.
- Docker Official Images, such as
node,python, andnginx, are curated, documented, and regularly updated. They’re the usual starting point, andnode:22is what the sample app builds on. See Docker Official Images. - Docker Hardened Images are minimal, production-focused images that
Docker maintains. They leave out shells and package managers the app
doesn’t need, which means fewer packages that can have vulnerabilities, and
they ship with signed SBOMs and provenance. They’re pulled from
dhi.ioafter adocker login dhi.io. See Docker Hardened Images.
For a hands-on comparison, see Build your application on a trusted foundation, which moves a Node.js image to a hardened base and measures what changed.
What happened
- The name is the address.
docker pushreads the registry and namespace from the image’s name, so publishing starts withdocker tag, not with an upload setting. - A registry stores layers and tags. Layers are stored by their content hash, so a layer the registry already has is never uploaded twice. A tag is a pointer to an image’s digest.
- Tags can move.
latestmoved the moment you pushed it, and anyone with push access can point1.0at a different image too. A tag names an image, but it doesn’t prove which one you’re getting. To pin exactly one, refer to it by digest, as in<YOUR_DOCKER_USERNAME>/todo-app@sha256:b3bd5c….
Next
Publishing makes your image someone else’s dependency. To give them evidence of what they’re running, see Prove where your container image came from, which signs an image by digest and catches a tag pushed over a trusted one. For everything Docker Hub can do with your repositories, see the Docker Hub documentation.
Part of a learning path
This works on its own and is also included in the paths below. Open a path to see the complete route.
Table of contents

