You closed module 1 with the tools in hand and the problem measured: fifteen manual steps to get Aurora Libros up and running. Module 2 starts tearing them down, and it does so from the very beginning of the chain: where images come from. So far you have typed docker pull node:22-alpine or docker run nginx:alpine without asking yourself too much about who is on the other side, who published that image or why you should trust it. In this lesson you open that box. You are going to understand what exactly a registry is and what a repository is inside it, you will authenticate with docker login and see where your credentials end up (with an unpleasant surprise included), you will learn to tell an official image from an image somebody uploaded on a Tuesday in 2019 and never touched again, you will learn about the download limits that can cut off a build at the worst possible moment, and you will run your own registry in a container to see that there is no magic to the mechanism. At the end you will choose the repository where the aurora-api image will live for the rest of the course.

Contents

  1. Registry, repository and tag: the three layers of the warehouse
  2. Docker Hub: account and interface
  3. docker login, docker logout and where your credentials end up
  4. Types of image on Docker Hub and how to assess their trustworthiness
  5. Download limits: anonymous versus authenticated
  6. Searching for images from the terminal with docker search
  7. Public and private repositories
  8. Alternatives to Docker Hub
  9. Your own registry in a container
  10. The Aurora Libros repository

  1. Registry, repository and tag: the three layers of the warehouse

In lesson 01-05 you learned to read a full image name:

registry/user/repository:tag@sha256:digest

Now it is time to understand what lies behind each segment, because they are three different things that people confuse constantly when talking.

  • A registry is a service: a server with an HTTP API that stores and serves images. Docker Hub is a registry. ghcr.io is another. The one you will run on your laptop in section 9 is one too. A registry contains many repositories.
  • A repository is a collection of related images that share a name and are told apart by their tag. library/nginx is a repository: inside it live nginx:1.27, nginx:alpine, nginx:latest… they are all "nginx", in different versions or variants.
  • A tag is a human-readable reference that points to a specific manifest, which in turn is identified by an immutable sha256:… digest.
flowchart TB
    subgraph REG["Registry · docker.io (Docker Hub)"]
        subgraph R1["Repository: library/node"]
            T1["tag 22-alpine"]
            T2["tag 22"]
            T3["tag latest"]
        end
        subgraph R2["Repository: auroralibros/aurora-api"]
            T4["tag 1.0.0"]
            T5["tag latest"]
        end
    end

    D1["sha256:9f2c…<br/>manifest + layers"]
    D2["sha256:41ab…<br/>manifest + layers"]

    T1 --> D1
    T2 --> D2
    T3 --> D2
    T4 --> D1
    T5 --> D1

Notice two details in the diagram that explain behavior you have already seen:

  • Several tags can point to the same digest. 22 and latest are, almost always, the same image under two names. That is why in lesson 01-05 docker image ls showed you two rows with the same IMAGE ID: they were not two images, but one with two references. You will come back to this in detail in lesson 02-06.
  • The digest is the only immutable part. The 22-alpine tag points to one manifest today and may point to a different one in three weeks' time (a Node security patch, an Alpine update). The digest sha256:9f2c… is always exactly the same bytes.

A useful analogy: the registry is the library; the repository is the shelf dedicated to one work; tags are the stickers ("first edition", "latest edition", "paperback edition") stuck onto specific copies. Moving a sticker does not change the copy; it only changes which copy that name points to.

The full name and its abbreviations

When you type docker pull nginx:alpine, Docker expands the name like this:

What you type What Docker actually understands
nginx docker.io/library/nginx:latest
nginx:alpine docker.io/library/nginx:alpine
auroralibros/aurora-api:1.0.0 docker.io/auroralibros/aurora-api:1.0.0
ghcr.io/auroralibros/aurora-api:1.0.0 as written: explicit registry
localhost:5000/aurora-api:1.0.0 as written: local registry on port 5000

Three rules follow from the table:

  1. If you do not specify a registry, Docker Hub is assumed (docker.io). It is a historical decision of the Docker client, not a standard; other tools (Podman, for example, lesson 07-05) may ask you which registry you want.
  2. If you do not specify a user/organization, library/ is assumed, the namespace reserved for official images. That is why nginx and library/nginx are the same thing, and why nobody else can publish a repository called simply nginx.
  3. If you do not specify a tag, latest is assumed, with all the dangers you already know about.

You can verify the first and second rules with a command you already handle:

docker pull nginx:alpine
docker image ls nginx --format "table {{.Repository}}\t{{.Tag}}\t{{.Size}}"
REPOSITORY   TAG       SIZE
nginx        alpine    52.5MB

Docker shows you the short form because the registry is the default one. If you do the same with an image from another registry, the name will appear in full, with ghcr.io/… in front. That asymmetry in the output is a quick clue for working out where each image on your machine came from.

  1. Docker Hub: account and interface

Docker Hub (hub.docker.com) is Docker's default public registry: it hosts millions of repositories and it is where every image you have downloaded so far came from. You will need an account for two things: pushing the aurora-api image (lesson 02-06) and — more immediately — doubling your download quota.

Creating the account

  1. Go to https://hub.docker.com and click Sign Up.
  2. Choose a Docker ID. Think it through: it will be the namespace of all your public images and it cannot be changed afterwards. In this course we will use auroralibros as the student's fictional Docker ID; you will use your real one wherever it appears.
  3. Verify your email address.
  4. Enable two-factor authentication (Account Settings → Security). In practice it is not optional: your account may end up publishing images that other people run, and a compromised account is a textbook attack vector.

A tour of the interface

Once inside, these are the areas you will use:

Area What it is for
Repositories Your repositories: create, view, change visibility, delete
Explore Search engine for public images, with filters by type (Official, Verified Publisher…)
Organizations Spaces shared by a team, with per-member permissions
Account Settings → Security Password, 2FA and Personal Access Tokens
Usage Your account's download and storage consumption

A repository's page

Open https://hub.docker.com/_/node (the underscore marks an official image) and look at the structure, because it is identical in every repository:

  • Description: what the image is, how it is used, which environment variables it accepts, examples. In well-maintained images, this page is the product's real documentation.
  • Pull command at the top right: the docker pull node ready to copy.
  • Tags tab: the catalog of available tags. It is the tab you will use the most. For each tag it shows you the compressed size, the available architectures (linux/amd64, linux/arm64…) and when it was last updated. Looking up "22-alpine" there before typing it into a FROM saves you the classic manifest unknown caused by a tag that does not exist.
  • Link to the Dockerfile: in serious images, each tag links to the GitHub repository holding the Dockerfile that built it. Being able to read that file is one of the best trust signals there is.

A two-minute exercise worth doing right now: go to the Tags tab of node, look for 22-alpine and write down its compressed size and its update date. Compare it with plain 22. The size difference explains why in lesson 01-07 you chose Alpine for aurora-api.

  1. docker login, docker logout and where your credentials end up

To push images or to pull from private repositories you need to authenticate. The command is straightforward:

docker login
Log in with your Docker ID or email address to push and pull images from Docker Hub.
Username: auroralibros
Password:
Login Succeeded

With no arguments, docker login points at Docker Hub. For another registry, you pass it as an argument:

docker login ghcr.io
docker login registry.gitlab.com
docker login localhost:5000

And to log out:

docker logout
docker logout ghcr.io

Where the credentials end up (and why it should worry you)

After a successful login, Docker saves the credential in ~/.docker/config.json. Take a look:

cat ~/.docker/config.json
{
  "auths": {
    "https://index.docker.io/v1/": {
      "auth": "YXVyb3JhbGlicm9zOmRja3JfcGF0X2V4YW1wbGU="
    }
  }
}

That auth field is not encrypted. It is simply user:password encoded in base64, which is a reversible encoding, not a protection system. Check it for yourself:

echo 'YXVyb3JhbGlicm9zOmRja3JfcGF0X2V4YW1wbGU=' | base64 -d
auroralibros:dckr_pat_example

Anyone with read access to your ~/.docker/config.json — another system user with permissions, a malicious process, a poorly protected backup or a container you carelessly mounted the directory into — has your credentials in the clear. In fact, Docker warns you about it when you log in:

WARNING! Your password will be stored unencrypted in /home/joan/.docker/config.json.
Configure a credential helper to remove this warning. See
https://docs.docker.com/go/credential-store/

Three concrete measures to mitigate it, from least to most effort:

a) Use a personal access token instead of your password. On Docker Hub, Account Settings → Personal Access Tokens → Generate new token, with minimal permissions (Public Repo Read-only if you only pull, Read & Write if you are going to publish). The advantages: if it leaks, you revoke that token without changing your password, it gives no access to the website or to the account settings, and you can have one per machine. You use it the same way:

docker login -u auroralibros
# At the "Password:" prompt you paste the token, not your password

b) Avoid docker login -p in scripts. Writing the password on the command line leaves it in the shell history and in the process list. The correct way is via standard input:

echo "$DOCKER_TOKEN" | docker login -u auroralibros --password-stdin

--password-stdin reads the credential from the pipe. It is the mandatory pattern in CI/CD (you will see it applied in lesson 06-02).

c) Configure a credential helper. It is an external program that stores credentials in the operating system's secure store instead of in a text file:

System Helper Actual store
Linux (desktop) docker-credential-secretservice GNOME Keyring / KWallet via D-Bus
Linux (server, no GUI) docker-credential-pass pass, backed by GPG
macOS docker-credential-osxkeychain macOS Keychain (ships with Docker Desktop)
Windows docker-credential-wincred Windows Credential Manager

Installing and enabling it on desktop Linux:

sudo apt install -y golang-docker-credential-helpers

And edit ~/.docker/config.json to add the helper line:

{
  "auths": {},
  "credsStore": "secretservice"
}

Now authenticate again:

docker logout
docker login -u auroralibros
cat ~/.docker/config.json
{
  "auths": {
    "https://index.docker.io/v1/": {}
  },
  "credsStore": "secretservice"
}

The auths object is empty: the credential is no longer in the file but in the system keyring, and the warning is gone. Docker retrieves it from the helper every time it needs it.

  1. Types of image on Docker Hub and how to assess their trustworthiness

Pulling an image and running it is, deep down, running a stranger's software on your machine. Docker Hub labels repositories into categories that help you decide who to trust.

Type Badge on the website Who maintains it Namespace When to use it
Docker Official Image Docker Official Image The Docker team plus designated maintainers, with public, reviewed Dockerfiles library/ (no user: nginx, node, postgres) Always the first choice for base software: languages, databases, web servers
Verified Publisher Verified Publisher The company that owns the software, with an identity verified by Docker The company's own (grafana/grafana, bitnami/…) When you need that vendor's commercial product
Sponsored OSS Sponsored OSS A recognized open source project, sponsored by Docker The project's own Established open projects with no official image
Community none Anyone with an account Any Only after auditing it, or never in production

For Aurora Libros this translates into something very concrete: node, postgres, redis and nginx are official, and that is why they are the project's four base images. There is no reason to use someuser/node-22-with-everything instead.

Practical assessment criteria

Badges do not cover every case: sooner or later you will need a community image. This is the check worth applying before writing it into a FROM:

  1. Date of the last update. It is in the Tags tab. An image that has not been touched in a year carries a year of unpatched vulnerabilities from the base system. It is the criterion that rules out the most candidates.
  2. Is the Dockerfile public? If you cannot read how it was built, you are running a black box. Look for the GitHub link in the description.
  3. Size. A 2 GB image to serve a 10 MB binary tells you it carries half an operating system inside, with tools you do not need that widen the attack surface.
  4. Number and content of the layers. With what you learned in 01-05 you can audit this without downloading anything beyond the image itself:
docker pull redis:7-alpine
docker image history redis:7-alpine --no-trunc --format "table {{.Size}}\t{{.CreatedBy}}" | head -12

What you are looking for in that output: understandable, justifiable instructions. A layer with a curl http://…/installer.sh | sh pointing at an unknown domain is an immediate red flag; it means the image downloads and runs arbitrary third-party code at build time.

  1. Downloads and stars, with reservations. A million downloads tells you a lot of people use it, not that it is safe; the numbers are easily inflated with automation. Use it as a weak signal, never as your main argument.
  2. Does it publish several architectures? If you work on a Mac with Apple Silicon or on an ARM server, you need linux/arm64. The Tags tab tells you. If there is only amd64, it will work through emulation, which is slower (multi-architecture manifests are covered in lesson 05-05).

All of this is manual, up-front assessment. Automated analysis of an image's content looking for known vulnerabilities (docker scout, scanners in the pipeline) and cryptographic image signing are a different discipline, and they are covered in lesson 05-03.

  1. Download limits: anonymous versus authenticated

Docker Hub limits how many images you can pull per unit of time. It is a real limit that cuts off builds in production, and it is worth knowing about before it happens to you.

Situation Indicative limit How you are identified
Anonymous (no docker login) The lowest of all, counted per IP address By outgoing public IP
Authenticated free account Noticeably higher than anonymous By user account
Paid account (Pro / Team / Business) Very high or with no practical limit By account

Docker revises the exact figures periodically, so always check the official pricing page before sizing anything. What does not change is the structure of the problem, and there is a critical nuance: in the anonymous case the counter is per IP. If you are in an office, at a university or behind a cloud provider's NAT, you share the quota with everybody else. An entire company behind a single public IP can exhaust the limit in minutes without anyone doing anything unusual.

When you go over it, the error looks like this:

Error response from daemon: toomanyrequests: You have reached your pull rate limit.
You may increase the limit by authenticating and upgrading:
https://www.docker.com/increase-rate-limit

And if it happens in the middle of a build, you will see it like this:

ERROR: failed to solve: node:22-alpine: failed to resolve source metadata for
docker.io/library/node:22-alpine: toomanyrequests: You have reached your pull rate limit.

How to avoid it, in order of effectiveness:

  • Always authenticate, even to pull public images. A simple docker login on your machine and on your CI agents doubles the quota and isolates it per account instead of per IP.
  • Take advantage of the local cache. Layers that are already downloaded are not fetched again. An ephemeral CI agent that always starts from scratch downloads everything every time: it is the scenario that consumes the most.
  • Use a mirror registry or a pull-through cache in your organization. The registry from section 9 can be configured for exactly that.
  • Consume base images from another registry. Many images are mirrored on ghcr.io, on ECR Public or on mcr.microsoft.com, with different limit policies.

You can check where you stand by querying the header the registry returns when you request a manifest, without downloading the image:

TOKEN=$(curl -s "https://auth.docker.io/token?service=registry.docker.io&scope=repository:ratelimitpreview/test:pull" | grep -o '"token":"[^"]*' | cut -d'"' -f4)
curl -s --head -H "Authorization: Bearer $TOKEN" https://registry-1.docker.io/v2/ratelimitpreview/test/manifests/latest | grep -i ratelimit
ratelimit-limit: 100;w=21600
ratelimit-remaining: 76;w=21600

Read it like this: a quota of 100 pulls in a window of 21,600 seconds (6 hours), of which you have 76 left. It is a useful check when a pipeline starts failing intermittently with no changes in the code.

  1. Searching for images from the terminal with docker search

You do not need to open a browser to search Docker Hub:

docker search postgres --limit 5
NAME                DESCRIPTION                                     STARS   OFFICIAL
postgres            The PostgreSQL object-relational database ...   14231   [OK]
bitnami/postgresql  Bitnami container image for PostgreSQL          202
timescale/timescaledb  TimescaleDB - time-series database ...        130
postgrest/postgrest  REST API for any PostgreSQL database           102
paradedb/paradedb   Postgres for Search and Analytics                 8

The columns: repository name, description, stars and whether it is official. The first row has no user in front of it, the unmistakable mark of something living in library/.

Available filters and formatting:

# Official images only
docker search --filter is-official=true node

# Only repositories with at least 100 stars
docker search --filter stars=100 redis

# With custom formatting, reusing what you learned in 01-04
docker search nginx --limit 5 --format "table {{.Name}}\t{{.StarCount}}\t{{.IsOfficial}}"
NAME                STARS     OFFICIAL
nginx               21045     [OK]
nginxinc/nginx-unprivileged  145
nginxproxy/nginx-proxy       130
bitnami/nginx                191
nginx/nginx-prometheus-exporter  58

Two important limitations of docker search you need to be clear about:

  1. It only searches Docker Hub. It does not query ghcr.io or any other registry, even if you have an open session with them. It is a limitation of the command itself, not of your configuration.
  2. It does not list tags. It tells you the node repository exists, but not which versions are inside it. For that, use the website's Tags tab or a query against the registry API:
curl -s "https://hub.docker.com/v2/repositories/library/node/tags?page_size=100&name=22-alpine" \
  | grep -o '"name":"[^"]*' | cut -d'"' -f4
22-alpine
22-alpine3.21
22-alpine3.20

This command queries Docker Hub's public API and extracts the names of the tags containing 22-alpine. It is the quick way to verify from the terminal that a tag exists before putting it in a FROM and finding out about the mistake halfway through the build.

  1. Public and private repositories

When you create a repository on Docker Hub you choose its visibility, and the decision has practical consequences:

Aspect Public Private
Who can pull Anyone, without authenticating Only accounts with permission, always authenticated
Who can push Only you or your organization Only you or your organization
Shows up in searches Yes No
Cost Free and unlimited Limited allowance on the free plan; paid beyond that
Consumes the puller's download quota Yes Yes
Typical use Free software, sample images, training Company applications, anything with business logic

The golden rule: a public image is published code. Everything inside it — your server.js, your internal paths, the comments in the code, any file that slipped into the COPY — can be inspected and extracted by anyone. This connects directly with the warning in exercise 3 of lesson 01-07: credentials never go into the image, and with a public repository that rule goes from being a good practice to being critical.

For this course, auroralibros/aurora-api will be public: that way you can share the link and run it from any machine without authenticating, which is exactly what you want to demonstrate. In lesson 02-06 you will see how to change the visibility to private and how to grant access to a team, which is what Aurora Libros would really do.

  1. Alternatives to Docker Hub

Docker Hub is not mandatory. These are the registries you will run into in the real world:

Registry Prefix Strong point Authentication
Docker Hub docker.io/ (implicit) The largest public catalog; official images Docker ID + token
GitHub Container Registry ghcr.io/ Integrated with the code repository and with GitHub Actions; unlimited free private repos GitHub user + personal access token with the write:packages scope
Amazon ECR <id>.dkr.ecr.<region>.amazonaws.com/ IAM-based permissions, proximity to AWS workloads, built-in scanning Temporary token via aws ecr get-login-password
GitLab Container Registry registry.gitlab.com/ Integrated with GitLab CI, one registry per project User + deploy token or CI token
Harbor your domain CNCF self-hosted registry: replication, retention policies, fine-grained access control LDAP/OIDC or local users
Registry (registry:2) your domain or localhost:5000 The reference implementation, minimal and with no web interface Optional (basic or token)

The good news is that they all speak the same protocol, the OCI Distribution Specification. That is why the commands are identical across all of them: change the prefix of the name and nothing else. A docker push to Docker Hub and one to Harbor are the same operation against different servers. You are about to verify it.

  1. Your own registry in a container

Nothing clarifies what a registry is better than running one. The official registry:2 image implements the distribution protocol and fits in a few megabytes.

docker run -d \
  --name aurora-registry \
  -p 5000:5000 \
  registry:2
Unable to find image 'registry:2' locally
2: Pulling from library/registry
...
Status: Downloaded newer image for registry:2
7c9a1e4f0b2d8a3c6f1b5e2d4a8c7b9e3f1a6d2c8b4e7a9f1c3d5b8e2a4f6c9d

Go over the options, all of them familiar from lesson 01-06: -d leaves it in the background, --name gives it a manageable name and -p 5000:5000 publishes the registry's port on your machine. Check that it responds:

curl http://localhost:5000/v2/_catalog
{"repositories":[]}

/v2/ is the root of the OCI distribution API, and _catalog lists the repositories. It is empty: you have just started it up. Let's put something inside.

# 1. Pull a small image from Docker Hub
docker pull alpine:3.21

# 2. Create a new reference pointing at your registry
docker tag alpine:3.21 localhost:5000/alpine:3.21

# 3. Push it
docker push localhost:5000/alpine:3.21
The push refers to repository [localhost:5000/alpine]
6e771e15690e: Pushed
3.21: digest: sha256:21dc6063fd678b478f57c0e13f47560d0ea4eeba26dfc947b2a4f81f686b9f45 size: 528

What just happened, step by step:

  • docker tag does not copy or duplicate anything: it creates a new name for the same local image. alpine:3.21 and localhost:5000/alpine:3.21 share an IMAGE ID. It is the mechanism you will study in depth in 02-06.
  • The localhost:5000/ prefix is what determines the destination of the push. Docker has no "upload to this server" command: the server is part of the image name. This is the key idea of the whole lesson.
  • The output shows the layer that was uploaded and the resulting digest, the same immutable identifier from section 1.

Check the catalog and the repository's tags:

curl -s http://localhost:5000/v2/_catalog
curl -s http://localhost:5000/v2/alpine/tags/list
{"repositories":["alpine"]}
{"name":"alpine","tags":["3.21"]}

And now the acid test: delete the local image and get it back from your registry.

docker image rm localhost:5000/alpine:3.21 alpine:3.21
docker pull localhost:5000/alpine:3.21
docker run --rm localhost:5000/alpine:3.21 cat /etc/alpine-release
3.21.0

You have completed a full cycle — pull from Docker Hub, tag, push to your registry, local deletion, pull from your registry, run — with the same commands you would use against any registry in the world. The only difference between your toy registry and a multinational's is the prefix of the name and who administers the server.

One important detail you will discover as soon as you move beyond localhost: Docker requires HTTPS for any remote registry. localhost is exempt because it is local. If you try to use 192.168.1.50:5000 without TLS, you will get:

Error response from daemon: Get "https://192.168.1.50:5000/v2/": http: server gave HTTP response to HTTPS client

The correct fix is to put a valid certificate in front of the registry. The quick fix for a lab network is to declare it as an insecure registry in /etc/docker/daemon.json and restart the daemon:

{
  "insecure-registries": ["192.168.1.50:5000"]
}
sudo systemctl restart docker

Use it only in isolated, test environments: without TLS, anyone on the network can read and tamper with the images in transit.

When you are done experimenting, clean up:

docker stop aurora-registry && docker rm aurora-registry

Bear in mind that the images you pushed lived inside the container and disappear with it, because you did not mount any volume. A real registry needs persistent storage, and that is precisely the subject of lesson 03-06.

  1. The Aurora Libros repository

With all of the above, you can now make the decision that governs the rest of the module. The Aurora Libros API image will live at:

docker.io/auroralibros/aurora-api

Which you will write in its short form:

auroralibros/aurora-api

The rationale for each segment:

Segment Value Why
Registry docker.io (implicit) It is the default, it needs no configuration and it is the one most people can consume without friction
User auroralibros The organization's Docker ID. Replace it with your real one
Repository aurora-api One repository per service, not one per project: aurora-api and, if it were ever needed, aurora-web
Visibility Public So it can be run from any machine without authenticating, which is the module's demonstration

The "one repository per service" rule deserves a comment, because doing it the other way round is a frequent mistake: putting several different images in the same repository and telling them apart by the tag (aurora:api-1.0.0, aurora:web-1.0.0). It works technically, but it breaks everything else: you cannot grant different permissions per service, tags stop meaning "version" and the Tags tab becomes unreadable. One repository per deployable artifact.

As preparation for lesson 02-06, create the empty repository from the website right now (Repositories → Create repository), with the name aurora-api, public visibility and a short description such as "REST API for the Aurora Libros S.L. catalog". It is not strictly necessary — Docker Hub creates the repository automatically on the first push — but this way you fix the description and the visibility from the start and avoid accidentally publishing something private in public.

In 02-02 you will build the image that will fill that repository.

Common Mistakes and Tips

  • Confusing registry with repository. "Push the image to the Docker repository" does not mean anything precise. The registry is the server (docker.io); the repository is the name inside it (auroralibros/aurora-api). When you debug a failed push, the first question is always which registry the full image name points to.
  • Believing that docker login "switches registry". There is no active registry. You can have simultaneous sessions open with Docker Hub, ghcr.io and your local registry; the destination of each operation is decided by the prefix of the image name, not by any global state.
  • Leaving your real password in ~/.docker/config.json. Base64 is not encryption. Use a personal access token with minimal permissions and, if at all possible, a credential helper. On a shared server this is not a recommendation, it is a requirement.
  • docker login -p myPassword in a script. It stays in the shell history and it is visible in ps to any user on the machine. Always use --password-stdin.
  • Anonymous builds in CI. It is the number one cause of the toomanyrequests that shows up "out of nowhere" on a Tuesday afternoon: the provider's shared IP exhausted the quota. Authenticate your CI agents even if they only pull public images.
  • Trusting stars. A community image with lots of downloads and no update in fourteen months is a worse choice than a less popular official one. Prioritize in this order: official → verified → community with a public Dockerfile and recently updated.
  • Tip: always check the Tags tab before writing a FROM. Confirm that the tag exists, which architectures it covers and when it was updated. It saves you the manifest unknown halfway through a build.
  • Tip: a local registry is the best learning tool. Whenever something in the tag/push/pull flow does not add up, reproduce it against localhost:5000: it is instantaneous, it does not consume quota and it does not publish anything on the internet.

Exercises

Exercise 1: audit three images before trusting them

Choose three Docker Hub repositories: an official one (postgres), a Verified Publisher or Sponsored OSS one of your choice, and a community one you find by searching for "nodejs" in Explore. For each of them, fill in this table and decide whether you would use it as the base of an Aurora Libros service:

Criterion Image A Image B Image C
Full name
Type (official/verified/community)
Last update of the tag you would use
Compressed size
Public Dockerfile? URL
Available architectures
Would you use it in production? Why

Rely on the website for the first three criteria and on docker image history to inspect how they were built.

Exercise 2: run a registry and publish an image to it

  1. Start a local registry on port 5001 (not 5000, to practice the mapping) called aurora-registry-lab.
  2. Pull redis:7-alpine from Docker Hub.
  3. Publish it to your local registry under the name localhost:5001/aurora-cache:7.
  4. Query the registry's catalog and the repository's tag list with curl.
  5. Delete both local references and pull the image again from your registry.
  6. Start a container with it and check that Redis responds.
  7. Clean everything up.

Exercise 3: diagnose four registry problems

Explain the cause of each message and the exact command you would use to fix it:

a) Error response from daemon: pull access denied for aurora-api, repository does not
   exist or may require 'docker login': denied: requested access to the resource is denied

b) denied: requested access to the resource is denied
   (when running: docker push aurora-api:1.0.0)

c) Error response from daemon: toomanyrequests: You have reached your pull rate limit.

d) Error response from daemon: Get "https://registry.internal.local:5000/v2/":
   x509: certificate signed by unknown authority

Solutions

Solution to exercise 1

A typical result, with the values as of the time this lesson was written (yours will vary; what matters is the reasoning):

Criterion postgres:16-alpine grafana/grafana:11.4.0 someuser/nodejs:latest
Type Docker Official Image Verified Publisher Community
Last update Days ago Days ago 2 years ago
Compressed size ~110 MB ~180 MB ~380 MB
Public Dockerfile Yes, github.com/docker-library/postgres Yes, github.com/grafana/grafana No
Architectures amd64, arm64, and several more amd64, arm64 amd64 only
Production? Yes, it is the reference Yes, it is the vendor No

The reasoning for ruling out the third one is cumulative, and any of the points would be enough on its own:

  • Two years without an update means two years of unpatched CVEs in the base system. Even if the application code were perfect, libc, openssl and the rest of the system are not.
  • With no public Dockerfile you cannot know what is inside it, nor audit it.
  • latest as the only tag is a symptom of an unmaintained project: there is no versioning, so you cannot pin anything or roll back.
  • amd64 only makes it useless on a Mac with Apple Silicon or on ARM instances, short of emulation.
  • The size gives away a complete base system with unnecessary tools.

The practical conclusion for Aurora Libros: the project's four base images (node, postgres, redis, nginx) are official, and that is precisely the reason they were chosen in lesson 01-07.

Solution to exercise 2

# 1. Registry on the host's port 5001
docker run -d --name aurora-registry-lab -p 5001:5000 registry:2

Notice -p 5001:5000: the registry always listens on 5000 inside the container; what you change is the host port. It is exactly the host-port → container-port mapping from lesson 01-06.

# 2. Pull from Docker Hub
docker pull redis:7-alpine

# 3. Retag and publish
docker tag redis:7-alpine localhost:5001/aurora-cache:7
docker push localhost:5001/aurora-cache:7
The push refers to repository [localhost:5001/aurora-cache]
7: digest: sha256:0d1a3e0c... size: 1571
# 4. Query the registry
curl -s http://localhost:5001/v2/_catalog
curl -s http://localhost:5001/v2/aurora-cache/tags/list
{"repositories":["aurora-cache"]}
{"name":"aurora-cache","tags":["7"]}
# 5. Delete both local references and pull from your own registry
docker image rm redis:7-alpine localhost:5001/aurora-cache:7
docker image ls | grep -E "redis|aurora-cache"   # should return nothing
docker pull localhost:5001/aurora-cache:7

Step 5 is the one that proves the point of the exercise: by deleting both references, the image really does disappear from your machine (remember from 01-05 that two tags on the same IMAGE ID count as a single image: as long as one reference remains, the data is still there). If the subsequent pull works, the bytes necessarily come from your registry.

# 6. Start it and check
docker run -d --name cache-lab -p 6379:6379 localhost:5001/aurora-cache:7
docker exec cache-lab redis-cli ping
PONG
# 7. Cleanup
docker stop cache-lab aurora-registry-lab
docker rm cache-lab aurora-registry-lab
docker image rm localhost:5001/aurora-cache:7

Solution to exercise 3

(a) pull access denied ... may require 'docker login'. The message mixes two causes because the registry does not distinguish between "it does not exist" and "you do not have permission": doing so would let you find out which private repositories an organization has. The real causes, by frequency:

  1. The name has no user in it: aurora-api expands to docker.io/library/aurora-api, and library/ is reserved for official images, so it does not exist. Correct: docker pull auroralibros/aurora-api:1.0.0.
  2. The repository is private and you have no session: docker login.
  3. There is simply a typo in the name.

(b) denied when pushing. You are trying to push to docker.io/library/aurora-api, a space where nobody has write permission. The image must carry your namespace:

docker tag aurora-api:1.0.0 auroralibros/aurora-api:1.0.0
docker login
docker push auroralibros/aurora-api:1.0.0

General rule: if the push says denied, look at the name before the credentials. A malformed name is more frequent than an expired session.

(c) toomanyrequests. You have exhausted your pull quota. Immediate fix:

docker login -u auroralibros

If you were already authenticated, the exhausted quota is your account's and you have to wait for the window to reset, use a mirror or upgrade your plan. In CI, the cause is almost always that the agents pull anonymously: add a login step with --password-stdin at the start of the pipeline (lesson 06-02). To confirm the diagnosis, use the ratelimit-* header query from section 5.

(d) x509: certificate signed by unknown authority. The registry does speak HTTPS, but its certificate is signed by an authority your machine does not recognize (typically a company's internal CA). It is not a credentials problem. The correct fix is to install that CA's certificate where Docker looks for it:

sudo mkdir -p /etc/docker/certs.d/registry.internal.local:5000
sudo cp company-ca.crt /etc/docker/certs.d/registry.internal.local:5000/ca.crt
sudo systemctl restart docker

The directory name must match exactly the host and port you use in the image name. The alternative of adding it to insecure-registries does not fix this: it would disable verification instead of resolving it, and it is only acceptable in an isolated lab. Tell it apart from the error in section 9 (server gave HTTP response to HTTPS client), which is the opposite case: there the server has no TLS at all.

Conclusion

You now know where the images you run come from. A registry is a server that speaks the OCI distribution protocol; inside it there are repositories, collections of images that share a name; and inside each repository, tags are movable pointers to immutable digests. That three-layer model explains why nginx really means docker.io/library/nginx:latest, why two tags can share an IMAGE ID and why the destination of a push is not a parameter of the command but part of the image name.

On top of that, you now have a Docker Hub account, you know that docker login leaves your credentials in ~/.docker/config.json in base64 — not encrypted — and how to avoid that with personal access tokens, --password-stdin and a credential helper. You can tell a Docker Official Image from an abandoned community image and apply a real trust check: update date, size, public Dockerfile, understandable layers and architectures. You know about download limits, why they hit anonymous builds behind a shared IP hardest and exactly which error they produce. And you have run your own registry with registry:2 to confirm, with a full cycle of tag, push, deletion and pull, that there is nothing magical about the mechanism. The Aurora Libros API image now has an address: auroralibros/aurora-api.

The only thing missing is the image itself. In the next lesson, Building Docker Images, you will go from consuming other people's images to manufacturing your own: you will see what docker build does exactly, what that trailing dot in the command means — the famous build context — how the .dockerignore file promised back in 01-07 trims that context, and how BuildKit's layer cache turns a one-minute build into a two-second one if you order your instructions properly. By the end of it you will have auroralibros/aurora-api:0.1.0 built on your machine, running in a container and responding to curl http://localhost:3000/health. The first of the fifteen steps starts to fall.

Docker: From Beginner to Advanced

Module 1: Introduction to Docker

Module 2: Working with Docker Images

Module 3: Docker Containers

Module 4: Docker Compose

Module 5: Advanced Docker Concepts

Module 6: Docker in Production

Module 7: Docker Ecosystem and Tools

© Copyright 2026. All rights reserved