Guide

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.

Level
Beginner
Estimated time
15 minutes
Published
Last verified

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 latest with docker push
  • Pull and run your published image like any other
  • Use the same commands with another registry, such as GitHub Container Registry

The terminals below are simulated. Nothing runs on your machine, and each one starts from the state the guide has reached at that point.

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 username mobywhale.

  • 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.git

    Any 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:

The four parts of an image name The name docker.io/mobywhale/todo-app:1.0 split into four parts. docker.io is the registry, and is assumed when you leave it out. mobywhale is the namespace, your username or an organization. todo-app is the repository, which holds every version of one image. 1.0 is the tag, one version, and latest is assumed when you leave it out. Slashes separate the first three parts and a colon comes before the tag. REGISTRY NAMESPACE REPOSITORY TAG docker.io mobywhale todo-app 1.0 / / : which server omitted: docker.io your username or an organization one image, every version of it one version omitted: latest
  • 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.

  1. In mobywhale/todo-app:1.0, mobywhale is the namespace. No registry is named, so Docker assumes docker.io.
  2. In ghcr.io/mobywhale/todo-app:1.0, ghcr.io is the registry. A first part with a dot or a port in it, or localhost, is a registry hostname.
  3. In todo-app, latest is the tag. It isn’t written, but a name with no tag means latest.
  4. docker push todo-app is refused. It means docker.io/library/todo-app:latest, and library is 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 login

By 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.0

To 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.0

Each 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.0

Choose 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, and nginx, are curated, documented, and regularly updated. They’re the usual starting point, and node:22 is 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.io after a docker 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 push reads the registry and namespace from the image’s name, so publishing starts with docker 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. latest moved the moment you pushed it, and anyone with push access can point 1.0 at 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.

Learning paths

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.