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
- Registry, repository and tag: the three layers of the warehouse
- Docker Hub: account and interface
docker login,docker logoutand where your credentials end up- Types of image on Docker Hub and how to assess their trustworthiness
- Download limits: anonymous versus authenticated
- Searching for images from the terminal with
docker search - Public and private repositories
- Alternatives to Docker Hub
- Your own registry in a container
- The Aurora Libros repository
- Registry, repository and tag: the three layers of the warehouse
In lesson 01-05 you learned to read a full image name:
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.iois 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/nginxis a repository: inside it livenginx: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.
22andlatestare, almost always, the same image under two names. That is why in lesson 01-05docker image lsshowed 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-alpinetag points to one manifest today and may point to a different one in three weeks' time (a Node security patch, an Alpine update). The digestsha256: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:
- 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. - If you do not specify a user/organization,
library/is assumed, the namespace reserved for official images. That is whynginxandlibrary/nginxare the same thing, and why nobody else can publish a repository called simplynginx. - If you do not specify a tag,
latestis 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}}"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.
- 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
- Go to
https://hub.docker.comand click Sign Up. - 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
auroralibrosas the student's fictional Docker ID; you will use your real one wherever it appears. - Verify your email address.
- 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 nodeready 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 aFROMsaves you the classicmanifest unknowncaused 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.
docker login, docker logout and where your credentials end up
docker login, docker logout and where your credentials end upTo push images or to pull from private repositories you need to authenticate. The command is straightforward:
Log in with your Docker ID or email address to push and pull images from Docker Hub.
Username: auroralibros
Password:
Login SucceededWith no arguments, docker login points at Docker Hub. For another registry, you pass it as an argument:
And to log out:
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:
{
"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:
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:
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:
--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:
And edit ~/.docker/config.json to add the helper line:
Now authenticate again:
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.
- 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:
- 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.
- 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.
- 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.
- 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 -12What 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.
- 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.
- 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 onlyamd64, 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.
- 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-limitAnd 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 loginon 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 onmcr.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 ratelimitRead 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.
- Searching for images from the terminal with
docker search
docker searchYou do not need to open a browser to search Docker Hub:
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 8The 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 58Two important limitations of docker search you need to be clear about:
- It only searches Docker Hub. It does not query
ghcr.ioor any other registry, even if you have an open session with them. It is a limitation of the command itself, not of your configuration. - It does not list tags. It tells you the
noderepository 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'"' -f4This 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.
- 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.
- 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.
- 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.
Unable to find image 'registry:2' locally
2: Pulling from library/registry
...
Status: Downloaded newer image for registry:2
7c9a1e4f0b2d8a3c6f1b5e2d4a8c7b9e3f1a6d2c8b4e7a9f1c3d5b8e2a4f6c9dGo 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:
/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.21The push refers to repository [localhost:5000/alpine]
6e771e15690e: Pushed
3.21: digest: sha256:21dc6063fd678b478f57c0e13f47560d0ea4eeba26dfc947b2a4f81f686b9f45 size: 528What just happened, step by step:
docker tagdoes not copy or duplicate anything: it creates a new name for the same local image.alpine:3.21andlocalhost:5000/alpine:3.21share 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:
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-releaseYou 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 clientThe 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:
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:
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.
- 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:
Which you will write in its short form:
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 failedpush, 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.ioand 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 myPasswordin a script. It stays in the shell history and it is visible inpsto any user on the machine. Always use--password-stdin.- Anonymous builds in CI. It is the number one cause of the
toomanyrequeststhat 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 themanifest unknownhalfway through a build. - Tip: a local registry is the best learning tool. Whenever something in the
tag/push/pullflow does not add up, reproduce it againstlocalhost: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
- Start a local registry on port 5001 (not 5000, to practice the mapping) called
aurora-registry-lab. - Pull
redis:7-alpinefrom Docker Hub. - Publish it to your local registry under the name
localhost:5001/aurora-cache:7. - Query the registry's catalog and the repository's tag list with
curl. - Delete both local references and pull the image again from your registry.
- Start a container with it and check that Redis responds.
- 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 authoritySolutions
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,openssland the rest of the system are not. - With no public Dockerfile you cannot know what is inside it, nor audit it.
latestas the only tag is a symptom of an unmaintained project: there is no versioning, so you cannot pin anything or roll back.amd64only 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:2Notice -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:7The 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# 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:7Step 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# 7. Cleanup
docker stop cache-lab aurora-registry-lab
docker rm cache-lab aurora-registry-lab
docker image rm localhost:5001/aurora-cache:7Solution 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:
- The name has no user in it:
aurora-apiexpands todocker.io/library/aurora-api, andlibrary/is reserved for official images, so it does not exist. Correct:docker pull auroralibros/aurora-api:1.0.0. - The repository is private and you have no session:
docker login. - 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.0General 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:
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 dockerThe 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
- What Is Docker?
- Installing Docker
- Docker Architecture
- Basic Docker Commands
- Understanding Docker Images
- Creating Your First Docker Container
- The Course Project: The Aurora Libros Platform
Module 2: Working with Docker Images
- Docker Hub and Repositories
- Building Docker Images
- Dockerfile Basics
- Advanced Dockerfile Instructions
- Managing Docker Images
- Tagging and Publishing Images
Module 3: Docker Containers
- Running Containers
- Container Lifecycle
- Managing Containers
- Inspecting and Debugging Containers
- Docker Networking
- Data Persistence with Volumes
- Resource Limits and Restart Policies
Module 4: Docker Compose
- Introduction to Docker Compose
- Defining Services in Docker Compose
- Docker Compose Commands
- Multi-Container Applications
- Environment Variables in Docker Compose
- Profiles, Overrides and Multiple Environments
- Local Development with Docker Compose
Module 5: Advanced Docker Concepts
- Docker Networking Deep Dive
- Docker Storage Options
- Docker Security Best Practices
- Optimizing Docker Images
- Advanced Builds with BuildKit and Buildx
- Logging and Monitoring in Docker
- The Runtime Inside: Namespaces, Cgroups and Layers
Module 6: Docker in Production
- Preparing an Image for Production
- CI/CD with Docker
- Orchestrating Containers with Docker Swarm
- Introduction to Kubernetes
- Deploying Docker Containers in Kubernetes
- Scaling and Load Balancing
- Deployment Strategies and Rollback
