Home DevOps Docker Registry and Docker Hub Explained — Push, Pull and Publish Images

Docker Registry and Docker Hub Explained — Push, Pull and Publish Images

In Plain English 🔥
Imagine you've baked the perfect chocolate cake and want to share the recipe with friends around the world. Docker Hub is like a giant, public recipe website where anyone can upload their recipe and anyone else can download it. A Docker Registry is the actual shelf system behind that website — the organised storage that keeps every recipe (image) safe and findable. Your Docker image is the recipe, and pushing or pulling it is just uploading or downloading from that shelf.
⚡ Quick Answer
Imagine you've baked the perfect chocolate cake and want to share the recipe with friends around the world. Docker Hub is like a giant, public recipe website where anyone can upload their recipe and anyone else can download it. A Docker Registry is the actual shelf system behind that website — the organised storage that keeps every recipe (image) safe and findable. Your Docker image is the recipe, and pushing or pulling it is just uploading or downloading from that shelf.

Every time a company deploys software, they face the same headache: 'It works on my machine' — but crashes everywhere else. Docker solves that by packaging your app and everything it needs into a single, portable image. But once you've built that image, where does it live? How does your production server in Frankfurt get the same image your laptop built in Toronto? That's where Docker registries and Docker Hub step in, and they are the invisible glue holding modern DevOps pipelines together.

Without a central place to store and distribute images, every developer would be manually copying files across machines, hoping nothing gets corrupted or out of date. A Docker registry acts as the single source of truth for your images — versioned, tagged and always available to any machine with network access. It eliminates the 'which version are we running?' chaos and makes automated deployments possible.

By the end of this article you'll know exactly what a Docker registry is, how Docker Hub works, how to create a free account and push your first real image, how to pull images from public and private repositories, and the difference between Docker Hub, private registries and self-hosted options. You'll be able to wire images into a real workflow — not just paste commands you don't understand.

What Is a Docker Registry and Why Does It Exist?

A Docker registry is a server-side application that stores and distributes Docker images. Think of it as a Git repository, but instead of source code, it holds container images. Just like Git has GitHub as its popular hosting platform, Docker has Docker Hub as its flagship registry.

Every Docker image you build lives only on your local machine until you push it to a registry. The registry gives it a permanent address — a URL — that any authorised machine can use to pull that exact image, byte for byte.

Registries are built around two key concepts. First, repositories: a named collection of related images — for example, all versions of your 'web-api' app live in one repository. Second, tags: labels that identify a specific version inside that repository, like 'v1.0', 'v2.3' or simply 'latest'. Together they form the full image address: registry-host/username/repository-name:tag.

Docker itself ships with a default registry address baked in — docker.io — which points to Docker Hub. So when you run 'docker pull nginx', Docker silently expands that to 'docker.io/library/nginx:latest' and fetches it from Docker Hub. That's why it just works with no extra configuration on a fresh machine.

understanding_image_addresses.sh · BASH
1234567891011121314151617181920212223242526
# Docker image names follow a predictable pattern:
# [registry-host]/[namespace]/[repository]:[tag]
#
# Let's break down some real examples:

# 1. Official Docker Hub image (short form — Docker fills in the rest automatically)
docker pull nginx
# Docker expands this to: docker.io/library/nginx:latest

# 2. A community image on Docker Hub — username/repository:tag
docker pull bitnami/postgresql:15.4.0
# registry-host = docker.io (default)
# namespace     = bitnami
# repository    = postgresql
# tag           = 15.4.0

# 3. An image on a private registry (e.g. your company's internal registry)
docker pull registry.mycompany.com/backend/payments-service:v3.1
# registry-host = registry.mycompany.com
# namespace     = backend
# repository    = payments-service
# tag           = v3.1

# 4. Check what images you already have locally
docker images
# This shows your local image cache — images already pulled from a registry
▶ Output
Using default tag: latest
latest: Pulling from library/nginx
a2abf6c4d29d: Pull complete
a9edb18cadd1: Pull complete
Status: Downloaded newer image for nginx:latest
docker.io/library/nginx:latest

REPOSITORY TAG IMAGE ID CREATED SIZE
nginx latest a6bd71f48f68 2 weeks ago 187MB
bitnami/postgresql 15.4.0 c3f19a7941e2 3 weeks ago 279MB
registry.mycompany.com/payments v3.1 9b72f9d12a45 1 month ago 312MB
🔥
The 'latest' Tag Trap:The 'latest' tag does NOT automatically mean the newest version. It's just a tag like any other — maintainers choose to apply it to their newest release, but some forget. Always pin your production images to a specific version tag like ':3.14.2' so a surprise update never breaks your deployment.

Docker Hub — Creating an Account and Pushing Your First Image

Docker Hub at hub.docker.com is the world's largest public container registry, hosting over 15 million images. It's free for public repositories and gives you one free private repository on the free tier. For most beginners and open-source projects, that's plenty.

The workflow is always the same three steps: build an image locally, tag it with your Docker Hub username and repository name, then push it. Pulling is even simpler — just docker pull with the full image address.

Before you can push anything, Docker needs to know who you are. You authenticate once per machine with 'docker login', which stores an encrypted token on your computer. From that point, every push and pull to your private repos works automatically.

The tagging step is critical and confuses many beginners. When you build an image, you can name it anything locally. But to push to Docker Hub it must follow the exact format: yourusername/repositoryname:tag. Docker uses that username prefix to know which Docker Hub account to push the image to. If the prefix doesn't match your logged-in account, the push is rejected.

push_first_image_to_dockerhub.sh · BASH
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152
# ── STEP 1: Write a simple Dockerfile for our demo app ──────────────────────
# Create a file called Dockerfile in an empty folder with these contents:

cat > Dockerfile << 'EOF'
# Start from an official, minimal Node.js base image
FROM node:20-alpine

# Set the working directory inside the container
WORKDIR /app

# Copy our simple server file into the container
COPY server.js .

# Tell Docker which port this app listens on (documentation only)
EXPOSE 3000

# The command that runs when the container starts
CMD ["node", "server.js"]
EOF

# Create the tiny Node.js server it references
cat > server.js << 'EOF'
const http = require('http');
const server = http.createServer((req, res) => {
  res.writeHead(200, { 'Content-Type': 'text/plain' });
  res.end('Hello from Docker Hub! Version 1.0\n');
});
server.listen(3000, () => console.log('Server running on port 3000'));
EOF

# ── STEP 2: Build the image locally ─────────────────────────────────────────
# Replace 'yourDockerHubUsername' with your actual Docker Hub username
docker build --tag yourDockerHubUsername/hello-web-server:v1.0 .
# --tag gives our image its Docker Hub address from the very start
# The dot (.) means 'use the Dockerfile in the current directory'

# ── STEP 3: Log in to Docker Hub ────────────────────────────────────────────
docker login
# Docker will prompt for your username and password
# After success it caches a token in ~/.docker/config.json

# ── STEP 4: Push the image to Docker Hub ────────────────────────────────────
docker push yourDockerHubUsername/hello-web-server:v1.0
# Docker uploads each layer of the image separately
# Layers that already exist on Docker Hub are skipped (they show 'Layer already exists')

# ── STEP 5: Pull it back down on any machine to verify it worked ─────────────
docker pull yourDockerHubUsername/hello-web-server:v1.0

# ── STEP 6: Run it to confirm everything works ───────────────────────────────
docker run --publish 3000:3000 yourDockerHubUsername/hello-web-server:v1.0
# Visit http://localhost:3000 in your browser — you should see the Hello message
▶ Output
── docker build output ──
[+] Building 12.3s (8/8) FINISHED
=> [1/3] FROM node:20-alpine
=> [2/3] WORKDIR /app
=> [3/3] COPY server.js .
=> exporting to image
Successfully tagged yourDockerHubUsername/hello-web-server:v1.0

── docker login output ──
Username: yourDockerHubUsername
Password: ****************
Login Succeeded

── docker push output ──
The push refers to repository [docker.io/yourDockerHubUsername/hello-web-server]
5f70bf18a086: Pushed
a3b179341f8d: Pushed
v1.0: digest: sha256:9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08 size: 1570

── docker run output ──
Server running on port 3000
⚠️
Tag Once, Push Many:You can apply multiple tags to the same image before pushing — for example tag it both ':v1.0' and ':latest' and push both. This way users who pull ':latest' get your newest version, while CI pipelines that pin ':v1.0' are never surprised by changes. Run: docker tag youruser/hello-web-server:v1.0 youruser/hello-web-server:latest && docker push youruser/hello-web-server:latest

Public vs Private Repositories — and When to Use a Self-Hosted Registry

Docker Hub public repositories are visible to the entire internet. Anyone can pull your image without logging in, which is perfect for open-source projects and public tools. Private repositories require authentication before anyone can pull — essential for proprietary application code.

Docker Hub's free tier gives you unlimited public repos but only one private repo. If your team needs multiple private repos, you either pay for Docker Hub Pro, or you run your own registry. Running your own gives you full control, no pull-rate limits, and keeps images inside your network for security compliance.

Docker ships a lightweight official registry image (called simply 'registry') that you can run anywhere with a single command. For production, teams use managed options like AWS Elastic Container Registry (ECR), Google Artifact Registry, or GitHub Container Registry — all of which integrate directly with their respective cloud platforms and CI/CD pipelines.

The choice comes down to three factors: cost, security requirements and operational overhead. Public open-source project? Docker Hub public repo, free, zero effort. Startup with a few private services? Docker Hub paid plan. Enterprise with compliance rules? Self-hosted or cloud-native registry inside your own infrastructure.

run_local_private_registry.sh · BASH
123456789101112131415161718192021222324252627282930313233343536
# ── Run your own private Docker registry locally in 60 seconds ──────────────
# This uses Docker's official 'registry' image — it IS a registry running inside Docker

# Start the registry container on port 5000
docker run \
  --detach \
  --publish 5000:5000 \
  --name my-private-registry \
  --restart always \
  --volume registry-image-data:/var/lib/registry \
  registry:2
# --detach            = run in background
# --publish 5000:5000 = expose registry on localhost port 5000
# --name              = friendly name so we can reference it easily
# --restart always    = auto-restart if the machine reboots
# --volume            = persist images to a named Docker volume (survive container restarts)

# ── Now push an image to YOUR local registry ─────────────────────────────────

# First, tag an existing local image with the local registry address as the prefix
docker tag yourDockerHubUsername/hello-web-server:v1.0 \
            localhost:5000/hello-web-server:v1.0
# 'localhost:5000' IS the registry address — Docker reads it and knows where to push

# Push to the local registry (no login needed for localhost)
docker push localhost:5000/hello-web-server:v1.0

# ── Query the registry's API to see what's stored ────────────────────────────
curl http://localhost:5000/v2/_catalog
# Returns a JSON list of all repositories stored in your local registry

curl http://localhost:5000/v2/hello-web-server/tags/list
# Returns all tags for the hello-web-server repository

# ── Pull from your local registry on the same machine (or same network) ───────
docker pull localhost:5000/hello-web-server:v1.0
▶ Output
── docker run (registry) output ──
Unable to find image 'registry:2' locally
2: Pulling from library/registry
Status: Downloaded newer image for registry:2
a3ed95caeb02e3b4f9b4b2a3b4c7d9e1

── docker push to local registry ──
The push refers to repository [localhost:5000/hello-web-server]
5f70bf18a086: Pushed
a3b179341f8d: Pushed
v1.0: digest: sha256:9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08 size: 1570

── curl catalog output ──
{"repositories":["hello-web-server"]}

── curl tags output ──
{"name":"hello-web-server","tags":["v1.0"]}
⚠️
Watch Out — Insecure Registry Error:Docker refuses to push to a non-localhost registry over plain HTTP by default. If your self-hosted registry is at 192.168.1.50:5000 instead of localhost, you'll get 'http: server gave HTTP response to HTTPS client'. Fix it by adding the address to Docker's insecure-registries list in /etc/docker/daemon.json: {"insecure-registries": ["192.168.1.50:5000"]} then restart Docker. In production always use HTTPS with a valid TLS certificate instead.

How Docker Pull-Rate Limits Work — and How to Stay Under the Radar

Since November 2020, Docker Hub enforces pull-rate limits to protect its infrastructure. Anonymous users (not logged in) get 100 pulls per 6 hours, tracked by IP address. Authenticated free-tier users get 200 pulls per 6 hours. This matters enormously in CI/CD pipelines where every build might pull a base image.

A shared CI runner with dozens of engineers behind a single corporate IP can hit 100 pulls shockingly fast and start seeing 'toomanyrequests' errors mid-build — bringing the entire pipeline down.

The fix has two parts. First, always authenticate your CI runners with 'docker login' using a Docker Hub account — even a free one doubles your limit. Second, use a pull-through cache: a local registry that sits in front of Docker Hub and serves cached copies of images your team has already pulled. The runners pull from the local cache; only cache misses go to Docker Hub.

Cloud registries like AWS ECR Public Gallery have no pull-rate limits for public images, which is why many teams mirror critical base images (like node, python, ubuntu) there and reference those mirrors in their Dockerfiles instead of pulling directly from Docker Hub.

check_and_fix_rate_limits.sh · BASH
1234567891011121314151617181920212223242526272829303132333435363738394041
# ── Check your current Docker Hub rate-limit status ─────────────────────────
# Pull a temporary token (anonymous request mirrors what an unauthenticated runner sees)
RATE_LIMIT_TOKEN=$(curl --silent \
  "https://auth.docker.io/token?service=registry.docker.io&scope=repository:ratelimitpreview/test:pull" \
  | python3 -c "import sys,json; print(json.load(sys.stdin)['token'])")

# Use that token to hit Docker Hub and read the rate-limit headers
curl --silent --head \
  --header "Authorization: Bearer ${RATE_LIMIT_TOKEN}" \
  https://registry-1.docker.io/v2/ratelimitpreview/test/manifests/latest \
  | grep --ignore-case ratelimit
# Look for RateLimit-Limit and RateLimit-Remaining in the response headers

# ── Configure a pull-through cache registry ──────────────────────────────────
# Add this to /etc/docker/daemon.json on EACH CI runner machine:
# This tells Docker: 'before going to docker.io, check our cache first'

cat > /etc/docker/daemon.json << 'EOF'
{
  "registry-mirrors": ["http://registry-cache.internal.mycompany.com:5000"]
}
EOF

# Restart Docker to pick up the new config
sudo systemctl restart docker

# ── Set up the pull-through cache registry itself (run this once on a central server) ──
docker run \
  --detach \
  --publish 5000:5000 \
  --name docker-hub-pull-cache \
  --restart always \
  --volume pull-cache-data:/var/lib/registry \
  --env REGISTRY_PROXY_REMOTEURL=https://registry-1.docker.io \
  registry:2
# REGISTRY_PROXY_REMOTEURL tells the registry to act as a transparent proxy/cache
# for Docker Hub. First pull fetches from Docker Hub; every subsequent pull is served
# from local cache — no rate-limit hit

# ── Verify the daemon picked up the mirror ───────────────────────────────────
docker info | grep -A3 "Registry Mirrors"
▶ Output
── Rate limit header check (anonymous) ──
ratelimit-limit: 100;w=21600
ratelimit-remaining: 76;w=21600
# 76 pulls remaining out of 100 in the current 6-hour window

── After configuring the mirror ──
docker info output:
Registry Mirrors:
http://registry-cache.internal.mycompany.com:5000/
# Docker will now check the cache before hitting Docker Hub
🔥
Interview Gold:Being able to explain Docker Hub pull-rate limits and describe a pull-through cache as the solution is a genuinely senior-level answer. Most candidates who've only used Docker locally have never hit the limit and don't know it exists. Mentioning this in an interview shows you've run Docker in a real team environment.
Feature / AspectDocker Hub (Free)Self-Hosted Registry (registry:2)Cloud Registry (e.g. AWS ECR)
Setup timeZero — sign up and go~5 minutes with Docker installed10–20 min (cloud account + CLI config)
CostFree for public repos, 1 free private repoFree software, you pay for the serverPay per GB stored + data transfer
Private repositories1 free, paid plans for moreUnlimitedUnlimited
Pull-rate limits100/6hr anon, 200/6hr free authNone — it's yoursNone for public ECR Gallery
AuthenticationDocker Hub accountOptional basic auth or token authIAM roles / service accounts
Image scanningPaid plans onlyManual / third-party toolsBuilt-in with paid tier
CI/CD integrationNative with most CI toolsManual configurationDeep integration with cloud CI tools
Best forOpen-source projects, learningAir-gapped / compliance environmentsTeams already using a cloud provider

🎯 Key Takeaways

  • A Docker registry is the storage server for images; Docker Hub is simply the most popular hosted registry — the two terms are not interchangeable.
  • Image addresses encode everything: registry-host/namespace/repository:tag — Docker Hub is the default host, so you don't see 'docker.io' unless you look for it.
  • The ':latest' tag is a convention, not a guarantee — always pin production images to a specific version tag or SHA digest to prevent surprise breakage.
  • Docker Hub pull-rate limits can silently kill CI/CD pipelines — authenticate runners and consider a pull-through cache registry before you're under deadline pressure.

⚠ Common Mistakes to Avoid

  • Mistake 1: Pushing without the correct username prefix — Symptom: 'denied: requested access to the resource is denied' error when running docker push — Fix: Your image tag MUST start with your exact Docker Hub username, e.g. 'johndoe/my-app:v1' not just 'my-app:v1'. Re-tag it with: docker tag my-app:v1 johndoe/my-app:v1 then push again.
  • Mistake 2: Trusting the ':latest' tag for reproducible builds — Symptom: A Docker pull that worked last month now pulls a different image with breaking changes, and your app fails in ways that are impossible to reproduce locally — Fix: Always pin base images and pulled images to a specific digest or version tag in Dockerfiles (e.g. FROM node:20.11.1-alpine3.19) and in any docker pull commands used in scripts. Never use ':latest' in production Dockerfiles.
  • Mistake 3: Storing secrets or credentials inside a pushed Docker image — Symptom: API keys, passwords or .env files are baked into an image layer and discoverable by anyone who pulls the image — even after you delete the file in a later layer, docker history and layer inspection tools can recover it — Fix: Never COPY .env or any secrets file into an image. Use Docker secrets, environment variables passed at runtime, or a secrets manager like HashiCorp Vault. Use .dockerignore to explicitly exclude sensitive files before building.

Interview Questions on This Topic

  • QWhat is the difference between a Docker image, a Docker repository and a Docker registry? Can you describe how they relate to each other?
  • QYour CI/CD pipeline is failing with 'toomanyrequests: You have reached your pull rate limit' errors on Docker Hub. Walk me through how you'd diagnose and permanently fix this without upgrading to a paid Docker Hub plan.
  • QIf a developer accidentally pushes a Docker image containing a hardcoded AWS secret key to a public Docker Hub repository and then immediately deletes the tag, is the secret safe? Why or why not — and what should they do?

Frequently Asked Questions

What is the difference between Docker Hub and a Docker Registry?

A Docker Registry is any server that stores and serves Docker images — it's a generic term for the software and infrastructure. Docker Hub is a specific, hosted registry service run by Docker Inc at hub.docker.com. Every Docker Hub is a registry, but not every registry is Docker Hub. You can run your own private registry using the official 'registry:2' Docker image without touching Docker Hub at all.

Is Docker Hub free to use?

Docker Hub is free for unlimited public repositories. The free tier also includes one private repository. Pull rates for anonymous users are capped at 100 pulls per 6 hours per IP address, and authenticated free users get 200 pulls per 6 hours. Paid plans (Pro and Team) remove private repo limits and increase pull-rate allowances significantly.

Do I need Docker Hub to use Docker?

No. Docker Hub is a convenience, not a requirement. You can build and run Docker images entirely on your local machine without ever touching a registry. You only need a registry when you want to share images between machines or team members. You can also use alternative registries like GitHub Container Registry, AWS ECR, Google Artifact Registry, or self-host the official 'registry:2' image.

🔥
TheCodeForge Editorial Team Verified Author

Written and reviewed by senior developers with real-world experience across enterprise, startup and open-source projects. Every article on TheCodeForge is written to be clear, accurate and genuinely useful — not just SEO filler.

← PreviousDocker ComposeNext →Docker Security Best Practices
Forged with 🔥 at TheCodeForge.io — Where Developers Are Forged