2.3 · Versioning and publishing your images

Level 2 · Dockerfile & build

2.3Versioning and publishing your images

Goal: Tag your images cleanly and publish them to a registry to share them.
Estimated time: 11 min · self-paced reading · If you only have 5 min: read “The essentials in 30s” + the diagram.

The essentials in 30 seconds

  • Tag at build time, account included: account/image:version is the full address — no namespace, no push.
  • Immutable per release: one frozen, tested tag per version (1.2.0), never rewritten — moving aliases are only for comfort.
  • Never push a secret: everything entering the image leaves on push — a public push is irreversible.

Running mini-case — Idir and the prod pulling latest

Idir's lean image (2.2) runs locally, but the team shares it by hand, as latest, with no version: prod already drifted twice. Idir tags at build time with the account (moncompte/mon-app:1.2.0 plus the 1.2 alias), logs in to the registry, pushes both tags, then pulls 1.2.0 on another machine: build here, run elsewhere, frozen version. He checks the repository visibility before pushing. In 2.4, he will make this image configurable; in 2.5, he will check it holds no secret before any new push.

Following the 2.2 thread: Idir's lean image travels with a version number — and continues in 2.4 and 2.5.

1. Tagging like a pro

Tag at build time (-t moncompte/mon-app:1.2.0) and add useful aliases (1.2, latest in dev). Rule: one immutable tag per release (1.2.0), never rewritten; moving aliases are only for comfort. Prod always pulls the frozen, tested tag, never latest. Idir freezes 1.2.0 and lets 1.2 follow the fixes.

2. Publishing: login, push, pull

docker login authenticates, docker push moncompte/mon-app:1.2.0 publishes, any machine fetches it with pull. The account/image:tag name is the full address: without an account (namespace), the push is refused. Idir pushes his two tags then, on another machine (or after a local rmi), pulls 1.2.0 and runs it: proof the image travels.

3. Public or private?

By default on the Hub, a repository is public: perfect for open source, forbidden for internal code. For internal code, a private repository (paid Hub, GHCR, company registry). And never a secret in the image: a COPYed or ENV secret stays in the layers and leaves on push — a public push is irreversible. Idir controls visibility before every push and keeps secrets out of the image (2.5).

Common misinterpretation: "Everything in latest is simpler". No: latest moves on every push, prod drifts without warning and nobody knows what is running. Idir's rule: one immutable tag (1.2.0) per release for prod, moving aliases (1.2, latest) for comfort in dev — never the other way round.
From local image to registry Tag at build time, authenticate, push build -t account/app:1.2 Tag atbuild time, account includeddocker login Authenticateto the registrydocker push Publish, thenpull elsewhere
Tag at build time, authenticate, push
Text version of the diagram (SEO / screen readers / translation): 1) Idir TAGS: at build time, with the namespace (moncompte/mon-app:1.2.0) plus a comfort alias (1.2) — 1.2.0 is frozen, never rewritten. 2) He PUSHES: docker login then docker push of both tags, repository visibility checked (public for open source, private for internal). 3) He PULLS: on another machine, docker pull of 1.2.0 then run — build here, run elsewhere. Lesson: prod pulls a frozen, tested tag, never latest.

🔎 Concrete example — 🔎 Concrete example — Publishing 1.2.0

Idir's image, from the local machine to the registry:

docker build -t moncompte/mon-app:1.2.0 -t moncompte/mon-app:1.2 .
docker login
docker push moncompte/mon-app:1.2.0
docker push moncompte/mon-app:1.2
# on another machine:
docker pull moncompte/mon-app:1.2.0

Reading: 1.2.0 is frozen and tested, 1.2 follows the fixes, latest stays a dev comfort.

Lesson: prod pulls 1.2.0, never latest — reproducible and traceable.

Section vocabulary

Namespace
The account or organization before the image (moncompte/mon-app): the full address required to push.

Simple example: Idir prefixes with his account from the build on.

What it is not: the version tag alone.

Reflex: no namespace = no push.

Immutable tag
A frozen, never-rewritten version (1.2.0): what prod pulls to stay reproducible.

Simple example: Idir freezes the tested 1.2.0 for prod.

What it is not: a moving alias like latest.

Reflex: one release = one frozen tag.

Alias
A moving comfort tag (1.2, latest) following the fixes; handy in dev, forbidden as a prod reference.

Simple example: Idir lets 1.2 follow the patches, latest for trials.

What it is not: the reference deployed in production.

Reflex: comfort in dev, never in prod.

push / pull
Publishing to the registry (push) and fetching from the registry (pull): the sharing trio with login.

Simple example: Idir pushes 1.2.0 and pulls it on another machine.

What it is not: a local build never shared.

Reflex: login, push, pull: share versioned.

In practice — In practice — Publishing your app

  1. Rebuild your app with two tags (frozen 1.0.0 and 1.0 alias).
  2. Log in to the registry (docker login).
  3. Push both tags (docker push), visibility checked.
  4. On another machine (or after rmi), pull 1.0.0 and run it.
Your images travel: build here, run elsewhere, frozen version.

Script ready to say — enforcing the frozen tag (30 s)

"We no longer deploy latest: every release gets its frozen, tested tag, never rewritten — 1.2.0 runs in prod, we know exactly what. Aliases like 1.2 or latest stay for comfort in dev. Build here, pull elsewhere, same version everywhere."

Say while showing Idir's 1.2.0 pull on a second machine.

Copy-paste template — tagging and publishing

docker build -t moncompte/mon-app:1.2.0 -t moncompte/mon-app:1.2 .
docker login
docker push moncompte/mon-app:1.2.0
docker push moncompte/mon-app:1.2
docker pull moncompte/mon-app:1.2.0

Push refused

"Denied" push: Idir checks the login, the account/image namespace and his rights on the repository.

Internal code in public

Internal code pushed to a public repository: Idir switches back to private (or a company registry) before pushing.

Rewritten tag

A 1.2.0 pushed again differently after deployment: Idir freezes 1.2.1 instead of rewriting history.

Push on every commit

Every trial pushed pollutes the registry: Idir pushes only tested versions, the CI will sort the rest (level 4).

✓ Best practices

  • One immutable tag per release, never rewritten.
  • Prefix with the account from the build on.
  • Check visibility (public/private) before pushing.

✗ Common pitfalls

  • Rewriting an already deployed tag (prod drift).
  • Pushing internal code in public.
  • Including secrets in the pushed image.

Key takeaways

  • We tag at build time, account included (full address).
  • Immutable per release (1.2.0), aliases (1.2, latest) for comfort.
  • login → push → pull: the versioned sharing trio.
  • Never a secret in a pushed image (public push is irreversible).

48h challenge — apply tomorrow

Publish your lean image (2.2): build with two tags (1.0.0 + 1.0), login + push with visibility checked, then pull + run 1.0.0 elsewhere.

Section exit checklist
  • I can tag at build time (immutable + alias, account included).
  • I can publish (login, push) and fetch (pull) elsewhere.
  • I control public/private and exclude secrets before every push.
Self-assessment 1-5: does my prod pull a frozen tag?do I check visibility before every push?

Frequently asked questions

Why is my push refused (denied)?

Often a name without namespace, or no login, or no rights on the repository. Check docker login, the account/image:tag name and your write rights on the repository before retrying.

Should I push on every commit?

No: push tested versions (frozen tags), not every trial. The CI (level 4) will automate this sorting: it builds on every commit but publishes only validated versions.

Check your understanding

Which tag policy for production?

Additional resources (optional, to go further)

Level 2 examFresh, harder questions to test what you've learned