Registries and CI: tag, push and ship
An image on your laptop helps nobody else. Tag it with a registry address, push it, and let CI build every release the same way.
? What do the diagram colours mean
key idea / Docker itselfcontainer / good pathhost / watch outproblem / blockedneutral
What is in an image name
Chapter 1 drew a registry; this is where you use one. The name of an image is also its address.
Tag, push, pull
$ docker build -t shop-api:dev .
$ docker tag shop-api:dev ghcr.io/acme/shop-api:1.4.2
$ docker login ghcr.io # a token, never your password
$ docker push ghcr.io/acme/shop-api:1.4.2
# on the server
$ docker pull ghcr.io/acme/shop-api:1.4.2| Registry | Host | Typical use |
|---|---|---|
| Docker Hub | docker.io | public images, the default |
| GitHub Container Registry | ghcr.io | images next to the code that builds them |
| Cloud registries | ECR, Artifact Registry, ACR | private images close to where they run |
| Your own | registry:2 image | air-gapped networks, experiments |
A tagging strategy that survives production
- Tag every build with something unique, such as the Git commit (
sha-3f1c2a9). You can always trace a running container to its source. - Add a version tag on releases (
1.4.2). Treat it as immutable: never push different bytes to the same version. - Do not deploy
latest. It moves (chapter 2), so two servers can run different code under one name. - Pin by digest when it must not change:
image@sha256:….docker inspect --format '{{index .RepoDigests 0}}' imageprints it.
Apple silicon and Intel servers
An image is built for one CPU architecture. A Mac with Apple silicon builds arm64 by default; most servers are amd64. docker buildx can build both into one multi-platform tag, and each machine pulls the variant it needs.
$ docker buildx build --platform linux/amd64,linux/arm64 \
-t ghcr.io/acme/shop-api:1.4.2 --push .Build and push in CI
Images for servers should come from CI, not from a laptop: the build is repeatable, the credentials stay in the CI system, and every image is tied to a commit. This GitHub Actions workflow builds on every push to main and every version tag, for both architectures, with the layer cache kept between runs.
# .github/workflows/image.yml
name: image
on:
push:
branches: [main]
tags: ["v*"]
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write # allowed to push to ghcr.io
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/metadata-action@v5
id: meta
with:
images: ghcr.io/${{ github.repository }}
tags: |
type=sha
type=semver,pattern={{version}}
- uses: docker/build-push-action@v6
with:
push: true
platforms: linux/amd64,linux/arm64
tags: ${{ steps.meta.outputs.tags }}
cache-from: type=gha
cache-to: type=gha,mode=maxAdd a scan step (chapter 18) before the push to fail the build on serious vulnerabilities, and run your tests inside the image you are about to ship.
Try it yourself
-
Step 1 · Run a registry of your own
The official
registry:2image is a complete registry. Bound to 127.0.0.1, only your machine can reach it.$ docker run -d --name registry --rm -p 127.0.0.1:5000:5000 registry:2 -
Step 2 · Tag and push
The tag starts with the registry host, so
pushknows where to send it.$ docker pull alpine:3.20 $ docker tag alpine:3.20 localhost:5000/tools/alpine:3.20 $ docker push localhost:5000/tools/alpine:3.20 The push refers to repository [localhost:5000/tools/alpine] 3.20: digest: sha256:… size: 528 -
Step 3 · Ask the registry what it has
A registry is an HTTP API.
$ curl -s localhost:5000/v2/_catalog {"repositories":["tools/alpine"]} $ curl -s localhost:5000/v2/tools/alpine/tags/list {"name":"tools/alpine","tags":["3.20"]} -
Step 4 · Delete locally, pull back
$ docker rmi localhost:5000/tools/alpine:3.20 $ docker pull localhost:5000/tools/alpine:3.20 3.20: Pulling from tools/alpine Status: Downloaded newer image for localhost:5000/tools/alpine:3.20 $ docker stop registry
arm64 by default, and an amd64 server then fails with “exec format error”. Build in CI, or pass --platform.Recap
- An image name is an address: registry host / namespace / repository : tag.
docker tagadds a name;docker pushuploads only missing layers.- Tag every build uniquely (commit SHA), never reuse a version tag, and do not deploy
latest. - A digest is the immutable name of the exact bytes.
- Build and push from CI, for every architecture you run on.
Quick check
docker push myapp:1.0 fails with “requested access to the resource is denied”. Most likely?
Show answer
Answer: myapp means docker.io/library/myapp, which you cannot write to.
Tag it into a namespace you own, such as you/myapp:1.0 or ghcr.io/you/myapp:1.0, and log in.
Two servers run myapp:latest but behave differently. What is the fix?
Show answer
Answer: Deploy a unique tag (or a digest) per build.
latest moves. Tag each build with its commit or version, or pin @sha256, so every server runs the same bytes.
Registries and CI: common questions
Why does docker push say access denied?
An image name without a registry or namespace points at docker.io/library, which you cannot write to. Tag it as yourname/app or ghcr.io/you/app and log in first.