Skip to main content

How to push and pull images on Docker Hub?

Pushing and pulling images on Docker Hub is the basic workflow for sharing images: tag an image with your namespace, log in once, push it, then pull from anywhere.

Theory

TL;DR

  • Four commands cover 99 percent of cases: docker login, docker tag, docker push, docker pull.
  • Image name format: <namespace>/<repo>:<tag> where <namespace> is your Docker Hub username or organization. No prefix means "the official library namespace" - you cannot push there.
  • Authenticate with a personal access token (PAT), not your account password. PATs scope (read-only / read-write / read-write-delete) and revoke cleanly.
  • Credentials live in ~/.docker/config.json after docker login. On macOS / Windows, behind a credential helper.
  • Public repos: unlimited count, free. Private repos beyond the free tier need a paid plan. Anonymous pulls are rate-limited (100 per 6h per IP); authenticated pulls are higher.

Quick example

bash
# 1. Build a local image $ docker build -t myapp:1.0 . # 2. Tag it with your Docker Hub namespace $ docker tag myapp:1.0 youruser/myapp:1.0 # 3. Authenticate (one time, until you logout) $ docker login -u youruser Password: **** # paste your PAT here, not your account password Login Succeeded # 4. Push $ docker push youruser/myapp:1.0 The push refers to repository [docker.io/youruser/myapp] adc4d6e8f1c2: Pushed 8a3f2d1c9b8e: Pushed 1.0: digest: sha256:9f8e7d6c... size: 1247 # From another machine $ docker pull youruser/myapp:1.0

That is the complete loop. Build, tag, login, push - then pull from any host that has Docker installed.

Image naming and tagging

The full Docker Hub form: [docker.io/]<namespace>/<repo>:<tag>.

  • docker.io/ is the implicit default registry. You almost never type it.
  • <namespace> = your Docker Hub username or an organization you belong to.
  • <repo> = the repository name. Created automatically on first push.
  • <tag> = a label for this build (v1.0, 2026-04-30, pr-1247, latest). Defaults to latest if omitted.

Examples:

bash
youruser/myapp:1.0 # personal namespace, version tag mycompany/api:2026-04-30 # org namespace, date tag mycompany/api # implies :latest - avoid for production nginx:1.27 # official library/nginx, library/ implicit

The docker tag command does not move bytes; it only adds a name pointing at the same image ID. One image can have many tags.

Authentication: use a PAT, not a password

Docker Hub supports two ways to log in:

bash
# Old way - account password (works, but bad practice) $ docker login -u youruser Password: <account password> # Modern way - personal access token $ docker login -u youruser Password: <paste PAT from https://hub.docker.com/settings/security>

PATs let you:

  • Scope the token (read-only / read-write / read-write-delete-public-repo).
  • Revoke without changing your account password.
  • Use multiple tokens per machine (CI runner, laptop, server) and rotate independently.

In CI, tokens are stored as repository secrets. Never commit a token to git, never paste one into a Dockerfile.

Where credentials live

After docker login, credentials land in ~/.docker/config.json:

json
{ "auths": { "https://index.docker.io/v1/": { "auth": "base64(username:PAT)" } } }

On macOS and Windows, Docker uses the OS keychain (credsStore: osxkeychain, desktop) - the file just points at the keychain, not the raw token. Linux without a credential helper stores the base64-encoded token directly in the file. chmod 600 ~/.docker/config.json if you do that.

Common mistakes

Pushing without your namespace prefix

bash
# WRONG: tries to push into the official library/ namespace $ docker push myapp:1.0 requested access to the resource is denied # RIGHT: include your namespace $ docker tag myapp:1.0 youruser/myapp:1.0 $ docker push youruser/myapp:1.0

The error message is unhelpful; the cause is always a missing namespace.

Logging in with your account password instead of a PAT

bash
$ docker login -u youruser Password: <account password> Login Succeeded

It works, but if your password leaks, the attacker has full account access. PATs limit blast radius and revoke independently.

Pushing :latest and forgetting to push a versioned tag

bash
# WRONG: only :latest is up there $ docker tag myapp:1.0 youruser/myapp:latest $ docker push youruser/myapp:latest # RIGHT: push both, so people can pin to a specific version $ docker tag myapp:1.0 youruser/myapp:1.0 $ docker tag myapp:1.0 youruser/myapp:latest $ docker push youruser/myapp:1.0 $ docker push youruser/myapp:latest # Or in one go: docker push --all-tags youruser/myapp

If consumers can only pull :latest, they cannot pin a specific version - and one day your :latest will surprise them.

Hitting anonymous pull rate limits in CI

ERROR: toomanyrequests: You have reached your pull rate limit.

Docker Hub rate-limits anonymous IPs to 100 pulls per 6 hours, and a shared CI IP can burn through that fast. Authenticate the CI runner (docker login with a token) or pull through a registry mirror (Google Artifact Registry, AWS ECR Pull-through cache).

Real-world usage

  • GitHub Actions: docker/login-action@v3 reads a username + PAT from secrets, then docker/build-push-action@v5 builds and pushes in one step. Standard pattern across thousands of repos.
  • Local laptop -> shared dev environment: push your branch image to a private Docker Hub repo, then have your colleague docker pull and run it without rebuilding.
  • Open-source distribution: tools like tini, dive, and many CLIs publish their official images to Docker Hub under a verified-publisher namespace.
  • CI/CD pipelines on smaller teams: PR builds tagged myapp:pr-1247 get pushed to a Docker Hub private repo, the staging environment pulls them, integration tests run.

Follow-up questions

Q: What is the difference between docker push and docker push --all-tags?


A: Plain docker push youruser/myapp:1.0 pushes one tag. docker push --all-tags youruser/myapp pushes every local tag of that repository. Useful when you have tagged the same image with both a version and latest.

Q: How do I push a private image?


A: Same commands. The repo is created on first push; you set its visibility to private from the Docker Hub web UI (or via the API). Free Docker Hub accounts include one private repo; more requires a paid plan.

Q: Can I push images for multiple architectures (amd64 + arm64)?


A: Yes, with docker buildx. docker buildx build --platform linux/amd64,linux/arm64 -t youruser/myapp:1.0 --push . builds both architectures and uploads them as a manifest list under one tag. Consumers automatically pull the right one for their CPU.

Q: How do I delete an image from Docker Hub?


A: Tag deletion is from the web UI or via curl against the Docker Hub API with a PAT that has the delete scope. There is no docker delete-tag CLI. Repository deletion is also web-UI-only.

Q: (Senior) How do you avoid embedding push credentials in CI logs and history?


A: Use the official login action (docker/login-action) which reads from masked secrets and never echoes them. For self-hosted CI, pass the token via --password-stdin: echo $DOCKER_TOKEN | docker login -u $DOCKER_USER --password-stdin. Never use docker login -u user -p $TOKEN - that puts the token in shell history and process listings (ps aux would show it during the call).

Examples

Push from a GitHub Actions workflow

yaml
name: build-and-push on: push: branches: [main] jobs: publish: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - uses: docker/build-push-action@v5 with: push: true tags: | youruser/myapp:latest youruser/myapp:${{ github.sha }}

Every push to main produces an image tagged with latest and the commit SHA. Pin production deploys to the SHA tag for reproducibility.

Pull rate limits in CI - the fix

bash
# Symptom: build job fails ERROR: toomanyrequests: You have reached your pull rate limit. # Fix in your CI: log in before any pull - name: Docker login uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Build run: docker build -t myapp .

Authenticated pulls have a much higher limit. The login step itself is free.

Push the same image to two registries

bash
# Tag once, push twice - useful for redundancy or migration $ docker build -t myapp:1.0 . $ docker tag myapp:1.0 youruser/myapp:1.0 $ docker tag myapp:1.0 ghcr.io/youruser/myapp:1.0 $ docker login docker.io $ docker push youruser/myapp:1.0 $ docker login ghcr.io $ docker push ghcr.io/youruser/myapp:1.0

Same image bytes, two registries, two namespaces. Consumers pull from whichever they prefer.

Short Answer

Interview ready
Premium

A concise answer to help you respond confidently on this topic during an interview.

Comments

No comments yet