← Back to the section

You built an image on your laptop — now it needs to end up on a server. An image registry is exactly the place through which an image travels from one machine to another, and from which CI pulls and pushes images automatically.

That is also where the main trap hides: a name with a tag is not the image itself but a movable pointer to it.

what is in the registry and what a server gets by tag registry · ghcr.io/myorg/backend image A · sha256:9f3c… 240 MB latest sha-abc1234f movedimage B · sha256:2c7a…244 MBlatestsha-de56789a both tags point to one imagebuild #41 · commit abc1234fdocker build, then docker tag ×2docker push …:sha-abc1234fdocker push …:latesttwo tags — one set of layersno data is duplicated240 MB in the registry, not 480docker push with two tags — one image in the registry latest moved from A to Bbuild #42 · commit de56789asame Dockerfile, new commitdocker push …:sha-de56789adocker push …:latestlatest moved: A → Bsha-abc1234f still on Athe bytes of image A are unchangeda repeated push re-points latest to the new image latest is B now, was A yesterdaydeploy by the latest tagdocker pull …:latestserver-1 pulled before build #42 → Aserver-2 pulled after it → Bone tag — different imagesthe tag cannot tell what is in prodno rollback by latest: it is rewrittenone latest tag — two servers on different images a digest points at bytes, not a namedeploy by digestdocker pull …@sha256:9f3c…server-1 → image Aserver-2 → image Aa digest is a hash of contenta repeated push will not move itCI puts the digest in the manifestsha tag and digest never move — deploy by them

Two tags on one image duplicate nothing — the registry still holds the same 240 MB. But a repeated push re-points latest to the new image, and two servers on the same tag end up on different images; only the commit sha tag and the digest stay put.

What an image registry is

An image registry is a network-accessible store of Docker images. The idea is the same as a git repository: you send (push) an image to the registry, and then pull (pull) it onto any machine — locally, on a server, in Kubernetes.

The most common registries:

  • Docker Hub (hub.docker.com) — the default public registry. Free for public images; private ones are limited on the free plan. When you run docker pull nginx, Docker goes here.
  • GitHub Container Registry (ghcr.io) — GitHub's registry, convenient if your code is already there. Access is managed through GitHub tokens.
  • Private registries — you can run your own (for example, with registry:2 or Harbor) or use a cloud provider's registry (Amazon ECR, Google Artifact Registry, Yandex Container Registry).

They all work over a single protocol — the OCI Distribution Spec — so the docker push / docker pull commands are identical regardless of the registry.

Image naming and tags

A full image name looks like this:

<registry>/<namespace>/<name>:<tag>

A few examples:

Image nameWhere it lives
nginx:1.27Docker Hub, official image
myuser/myapp:latestDocker Hub, user image
ghcr.io/myorg/backend:v1.4.2GitHub Container Registry
registry.company.ru/team/api:sha-abc1234private registry

If no registry is specified — Docker Hub. If no tag is specified — latest.

A tag is simply a label on a specific image layer. Tags are mutable: latest today and latest a week from now may point to different images. That is why latest is convenient for development but dangerous in production.

Tagging strategy

Chaotic naming is a source of headaches: it is unclear what is deployed, hard to roll back, and difficult to trace which commit an image was built from.

Three durable strategies:

1. Semantic versioning (semver)

myapp:1.4.2
myapp:1.4
myapp:1

Human-readable, supports "floating" tags (1.4 → always the latest patch release). Suitable for libraries and public images.

2. Git commit SHA

myapp:sha-abc1234f

Unambiguously ties the image to the source code. Cannot be overwritten by accident. Recommended as the primary tag in CI — you always know which commit it was built from.

3. Combined

myapp:1.4.2          # for the release tag
myapp:sha-abc1234f   # for precise tracking

You can put both tags on a single image.

Short rule of thumb: never use latest in production — only a versioned or sha tag.

docker tag, push, and pull

Before sending an image to a registry, log in to it:

docker login ghcr.io -u USERNAME --password-stdin <<< "$GITHUB_TOKEN"

An image is tagged with the docker tag command:

# build first
docker build -t backend:local .

# add the registry "address"
docker tag backend:local ghcr.io/myorg/backend:sha-abc1234f
docker tag backend:local ghcr.io/myorg/backend:v1.4.2

Push:

docker push ghcr.io/myorg/backend:sha-abc1234f
docker push ghcr.io/myorg/backend:v1.4.2

Pull on another machine:

docker pull ghcr.io/myorg/backend:sha-abc1234f

A single image (one set of layers) can have any number of tags — this is cheap: tags are just pointers, the data is not duplicated.

Building and publishing in CI

Manual builds and push are fine for experiments. In a real project, the image is built automatically on every push to the main branch or when a release tag is created.

The general pipeline scheme:

code git push CI runs docker build docker tag docker push deployment

The image is built once and tagged with the commit hash. What travels down the chain is that image — not “let us rebuild it for production”.

An example configuration for GitHub Actions (.github/workflows/build.yml):

name: Build and publish image

on:
  push:
    branches: [main]

env:
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}

jobs:
  build-and-push:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write          # needed to publish to ghcr.io

    steps:
      - uses: actions/checkout@v4

      - name: Log in to GitHub Container Registry
        uses: docker/login-action@v3
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Build image and publish
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: |
            ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:sha-${{ github.sha }}
            ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest

What happens here:

  1. On a push to main, the job runs.
  2. CI logs in to the registry using the automatic GITHUB_TOKEN.
  3. The image is built from the Dockerfile at the repository root.
  4. Two tags are assigned: the commit sha (stable) and latest (for convenience during development).
  5. The image is pushed to the registry.

In a real project, the next step usually follows right after — updating a Kubernetes manifest or calling kubectl rollout restart — but that already goes beyond Docker as a build tool.

A word on multi-stage builds and image size

If you use a multi-stage build, CI does not need to do anything special — docker build handles it. For Spring Boot it looks like this: in the first stage you compile the jar, and in the second you copy only the jar on top of a base eclipse-temurin:21-jre. Only the final, lightweight image ends up in the registry.

# --- build stage ---
FROM eclipse-temurin:21-jdk AS build
WORKDIR /app
COPY . .
RUN ./gradlew bootJar -x test

# --- final image ---
FROM eclipse-temurin:21-jre
WORKDIR /app
COPY --from=build /app/build/libs/app.jar app.jar
ENTRYPOINT ["java", "-jar", "app.jar"]

Such an image takes up 200–300 MB instead of 600+ MB with the full JDK — and that is exactly what goes to the registry.

In short

  • An image registry is a store through which an image travels between machines; the most popular ones: Docker Hub, GitHub Container Registry, private registries from cloud providers.
  • The full image name: <registry>/<namespace>/<name>:<tag>; without a registry — Docker Hub, without a tag — latest.
  • In production, use a specific tag (semver or commit sha), not latest — otherwise it is unclear what is actually deployed.
  • docker tag adds a new pointer to an already-built image; the data is not copied.
  • The CI pipeline: build → tag → push; the next step is deployment (Kubernetes, systemd, Compose on a server).
  • In GitHub Actions, GITHUB_TOKEN is enough to publish to ghcr.io — no extra secrets are needed.