← 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.

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:

nginx:1.27                         # Docker Hub, official image
myuser/myapp:latest                # Docker Hub, user image
ghcr.io/myorg/backend:v1.4.2      # GitHub Container Registry
registry.company.ru/team/api:sha-abc1234  # private 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)

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.
  • Multi-stage builds and image layers — how layers work, why instruction order affects image size and build speed.
  • Image best practices — security, a minimal base image, an unprivileged user.
  • Running Spring Boot in Docker — a full example: Dockerfile, JVM configuration, Spring profiles.