Docker from Basics to Production · Operate Containers

Registries, Tags and CI Pipelines

Push images to a registry with immutable tags, pull by digest, and build the same way in CI every time.

Create a free account to save this page, take notes and track your progress. Sign in

What you'll build

A local registry, an image pushed with an immutable commit tag and a moving tag, then pulled back by digest, plus the CI steps that do this on every merge.

The idea

  • Tag every build with something immutable: the Git commit SHA or a release version. Moving tags (main, 1.4, latest) are for humans.
  • Deploy by digest (image@sha256:…) or immutable tag, so every environment runs identical bytes.
  • In CI: build with cache (registry or CI cache), run tests inside the built image, scan it, push, then record the digest for deployment.
  • Registries can enforce tag immutability and retention policies; turn them on.
# a typical CI job (GitHub Actions style)
- uses: docker/setup-buildx-action@v3
- uses: docker/build-push-action@v6
  with:
    push: true
    tags: ghcr.io/acme/api:${{ github.sha }}
    cache-from: type=gha
    cache-to: type=gha,mode=max

The code

Push, retag and pull by digest with a local registry
trap 'docker rm -f rc-registry > /dev/null 2>&1; docker image rm -f rc-app:dev localhost:5055/acme/app:9f2c1e7 localhost:5055/acme/app:main > /dev/null 2>&1' EXIT
docker run -d --name rc-registry -p 127.0.0.1:5055:5000 registry:3 > /dev/null
for i in $(seq 20); do curl -sf localhost:5055/v2/ > /dev/null && break; sleep 0.5; done
printf 'FROM alpine:3.24\nCMD ["echo", "app build 9f2c1e7"]\n' > Dockerfile
docker build -q -t rc-app:dev . > /dev/null
docker tag rc-app:dev localhost:5055/acme/app:9f2c1e7
docker tag rc-app:dev localhost:5055/acme/app:main
docker push -q localhost:5055/acme/app:9f2c1e7 > /dev/null
docker push -q localhost:5055/acme/app:main > /dev/null
echo "tags in the registry: $(curl -s localhost:5055/v2/acme/app/tags/list)"
digest=$(docker buildx imagetools inspect localhost:5055/acme/app:9f2c1e7 --format '{{json .Manifest}}' | python3 -c 'import json,sys; print(json.load(sys.stdin)["digest"])')
echo "both tags point to the same digest: $( [ "$digest" = "$(docker buildx imagetools inspect localhost:5055/acme/app:main --format '{{json .Manifest}}' | python3 -c 'import json,sys; print(json.load(sys.stdin)["digest"])')" ] && echo yes)"
docker image rm -f localhost:5055/acme/app:9f2c1e7 > /dev/null
docker run --rm "localhost:5055/acme/app@$digest"

Output

tags in the registry: {"name":"acme/app","tags":["9f2c1e7","main"]}
both tags point to the same digest: yes
app build 9f2c1e7

Run this on your computer — see “Get Started” for setup.

Common mistakes

  • Deploying latest or a branch tag, so two nodes can run different builds.
  • Rebuilding the image for each environment instead of promoting the same digest.
  • Running tests on the host instead of in the image you ship.
  • Unlimited image retention: registries fill up; set lifecycle policies.

Key points

  • Immutable tags (commit SHA, version) plus deploy-by-digest.
  • CI builds once with cache, tests and scans the image, pushes, and records the digest.
  • Promote the same image through environments.

Further reading: Docker build GitHub Actions · Distribution registry

Practice questions

Why deploy images by digest rather than tag?

Answer

A digest identifies exact content and can't be moved, so every environment and node runs the same bytes; tags can be re-pushed.

What should a CI pipeline do with a container image before pushing it?

Answer

Build it once with caching, run tests inside it, scan it for vulnerabilities, then push with an immutable tag and record the digest for deployment.

What is image promotion?

Answer

Moving the same built image (same digest) from one environment to the next instead of rebuilding, so what was tested is what runs in production.

Open in the app to mark lessons complete, take notes and track your progress.

Was this page helpful?