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 officiallibrarynamespace" - 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.jsonafterdocker 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
# 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.0That 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 tolatestif omitted.
Examples:
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/ implicitThe 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:
# 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:
{
"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
# 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.0The error message is unhelpful; the cause is always a missing namespace.
Logging in with your account password instead of a PAT
$ docker login -u youruser
Password: <account password>
Login SucceededIt 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
# 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/myappIf 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@v3reads a username + PAT from secrets, thendocker/build-push-action@v5builds 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 pulland 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-1247get 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
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
# 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
# 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.0Same image bytes, two registries, two namespaces. Consumers pull from whichever they prefer.
Short Answer
Interview readyA concise answer to help you respond confidently on this topic during an interview.