From lesson 3-1, the -t in docker build -t myapp:v1 . tagged the resulting image with a name and version.
It names the image myapp and tags it v1, and so far that image exists only on the machine that built it.
This lesson is about moving it to a registry, so other machines, including the CI pipeline in unit 7, can pull it.
Registries
So far the image exists only on the machine that built it, which means nothing else can run it: not the server being deployed to, not a teammate, not the test automation of unit 7.
Moving images between machines is what a registry is for, a server that stores and serves images, the equivalent of git hosting but for images instead of code. You have already used one, since docker run hello-world in lesson 2-1 pulled from Docker Hub, the default public registry. The other one that comes up constantly is GitHub Container Registry, GHCR, which lives next to your repos.
A full image reference has up to four parts:
ghcr.io/ada/shop:v2 └─────┘ └─┘ └──┘ └┘ registry owner name tag
| Part | Example | Default when omitted |
|---|---|---|
| registry | ghcr.io | docker.io |
| owner | ada | library |
| name | shop | none, always required |
| tag | v2 | latest |
Writing just nginx makes Docker fill in those defaults, giving docker.io/library/nginx:latest.
Shipping an image is three commands:
docker login ghcr.io # authenticate once docker tag myapp:v1 ghcr.io/ada/shop:v1 # give the image its full remote name docker push ghcr.io/ada/shop:v1 # upload it
Any other machine then runs docker pull ghcr.io/ada/shop:v1. Thanks to layers from lesson 3-2, pushes and pulls only transfer layers the other side does not already have, which is why the second push of a small change is fast.
Building references from parts
References are plain strings, so a shell can assemble them from a repo and a tag.
repo="ghcr.io/ada/shop" tag="v2" echo "$repo:$tag" echo "$repo:latest"
Output
ghcr.io/ada/shop:v2 ghcr.io/ada/shop:latest
The reference format is repo:tag, joined by a colon, and echo "$repo:$tag" expands both variables around it. A colon is not a valid character in a bash variable name, which is why no braces are needed here.
| Line | Produces |
|---|---|
echo "$repo:$tag" | the versioned reference |
echo "$repo:latest" | the moving reference |
Release scripts do exactly this, pushing both the immutable version tag and the moving latest tag from the same build.
How ordinary the latest tag really is
Nothing is special about latest. It is simply the default tag name, and it only moves when someone explicitly pushes to it.
latest is an ordinary tag that happens to be assumed when no tag is given. Nobody updates it automatically, and teams can and do push older builds to it by accident.
Belief about latest | Reality |
|---|---|
| always the newest build | only if someone pushed it there |
| updated automatically | no |
| immutable | no, it moves |
That is why lesson 2-1 insisted on pinning real version tags, and why rollbacks in lesson 8-3 rely on immutable tags such as v41. A deploy that references latest cannot be reproduced later, because the same string will point at different bytes.
Expanding a bare image name
A plain nginx in a docker run command actually resolves to docker.io/library/nginx:latest.
Bare names get three defaults filled in: the registry docker.io, which is Docker Hub, the owner library, which is the namespace for official images, and the tag latest.
| Written | Resolved |
|---|---|
nginx | docker.io/library/nginx:latest |
ada/shop | docker.io/ada/shop:latest |
ghcr.io/ada/shop:v1 | unchanged, fully specified |
Knowing the expansion matters the day another registry is used, where every part must be written out. It also explains why an official image and a personal one with the same name never collide, since the owner differs.
The subcommand that uploads an image
The subcommand is docker push, the same verb git uses to upload commits, and its opposite is docker pull.
After docker login and docker tag give the image its full remote name, docker push uploads any layers the registry does not already have.
| Step | Command |
|---|---|
| authenticate | docker login ghcr.io |
| name it for the remote | docker tag myapp:v1 ghcr.io/ada/shop:v1 |
| upload | docker push ghcr.io/ada/shop:v1 |
In unit 7 the CI pipeline runs this command on every merge, so pushing by hand becomes something you do mainly while learning or debugging.