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.
Comments
No comments yet