رفتن به محتوا
LoopX

tag و push به registry

توی این درس یاد می‌گیری image ای که ساخته‌ای را چطور به یک registry ببری تا همکارانت و سرورها بتوانند آن را pull کنند: ساختار اسم کامل (registry/namespace/repo:tag)، docker tag برای اسم‌گذاری، docker login برای ورود و docker push برای ارسال. با یک registry خصوصی محلی (که واقعاً اجرا می‌کنی و تست می‌شود) تمرین می‌کنی، و بعد با Docker Hub و GitHub Container Registry (GHCR) آشنا می‌شوی. در پایان استراتژی tag گذاری را می‌بینی: 1.2.3، 1.2، latest و هش commit.

تشبیه: اداره‌ی پست و آدرس کامل

Section titled “تشبیه: اداره‌ی پست و آدرس کامل”

برای فرستادن یک بسته باید آدرس کامل روی آن بنویسی: کشور، شهر، خیابان، پلاک. اسم کامل image همین است: registry (شهر)، namespace (خیابان: حساب یا سازمان)، repository (پلاک: اسم image) و tag (واحد: نسخه). docker tag برچسب آدرس را روی بسته می‌چسباند و docker push آن را می‌فرستد.

image با build ساخته می‌شود، با tag نشانی registry را می‌گیرد، با push ارسال می‌شود و دیگران با pull دریافتش می‌کنند.

مثال ۱: یک registry خصوصی روی سیستم خودت

Section titled “مثال ۱: یک registry خصوصی روی سیستم خودت”

خود registry هم یک image رسمی است (registry:2). یکی را روی پورت 5301 بالا می‌آوریم:

Terminal window
docker run -d --name lx-reg -p 5301:5000 registry:2 >/dev/null
sleep 2
curl -s localhost:5301/v2/
echo
curl -s -o /dev/null -w "HTTP %{http_code}\n" localhost:5301/v2/
خروجی
{}
HTTP 200

جواب {} و 200 یعنی registry زنده است و API نسخه‌ی ۲ را می‌فهمد. هنوز هیچ image ای ندارد.

مثال ۲: ساخت و برچسب‌زدن (docker tag)

Section titled “مثال ۲: ساخت و برچسب‌زدن (docker tag)”
app/Dockerfile
FROM alpine
CMD ["echo", "سلام از image منتشرشده"]
Terminal window
docker build -q -t lx-app:1.0 ./app >/dev/null 2>&1
docker tag lx-app:1.0 localhost:5301/lx/app:1.0
docker images --format 'table {{.Repository}}\t{{.Tag}}\t{{.ID}}' | grep -E "REPOSITORY|lx-app|lx/app"
خروجی
REPOSITORY TAG IMAGE ID
lx-app 1.0 5f24288299e2
localhost:5301/lx/app 1.0 5f24288299e2

اسم کامل localhost:5301/lx/app:1.0 را بخوان: registry = localhost:5301، namespace = lx، repository = app، tag = 1.0. docker tag فقط یک اسم تازه روی همان image می‌گذارد (ID یکی است)؛ هنوز چیزی ارسال نشده. اگر بخش registry را ننویسی، پیش‌فرض Docker Hub (docker.io) است.

Terminal window
docker push localhost:5301/lx/app:1.0 2>&1 | awk '!s[$0]++' | cut -c1-110 | head -8
خروجی
The push refers to repository [localhost:5301/lx/app]
1caaaf6c63d7: Waiting
44136fa355b3: Waiting
a9986cd6f37d: Waiting
1caaaf6c63d7: Pushed
44136fa355b3: Pushed
a9986cd6f37d: Pushed
1.0: digest: sha256:5f24288299e2454931be29f3e410d586178ebfe783bb9ac5b0278367c8ca0900 size: 855

هر لایه جدا ارسال می‌شود (Pushed)، و در پایان digest و اندازه‌ی manifest چاپ می‌شود. حالا ببین registry چه دارد:

Terminal window
curl -s localhost:5301/v2/_catalog; echo
curl -s localhost:5301/v2/lx/app/tags/list; echo
خروجی
{"repositories":["lx/app"]}
{"name":"lx/app","tags":["1.0"]}

فهرست repository ها (_catalog) و tag های lx/app را از خود registry خواندیم، پس واقعاً ارسال شده.

⚡ بررسی سریع

در localhost:5301/lx/app:1.0 کدام بخش «registry» است؟

مثال ۴: چند tag، یک image (و ارسال هوشمند)

Section titled “مثال ۴: چند tag، یک image (و ارسال هوشمند)”
Terminal window
docker tag lx-app:1.0 localhost:5301/lx/app:1.0.1
docker tag lx-app:1.0 localhost:5301/lx/app:1
docker tag lx-app:1.0 localhost:5301/lx/app:latest
for t in 1.0.1 1 latest; do docker push localhost:5301/lx/app:$t 2>&1 | grep -E "Layer already exists|digest" | head -2 | cut -c1-90; done
curl -s localhost:5301/v2/lx/app/tags/list; echo
خروجی
a9986cd6f37d: Layer already exists
1.0.1: digest: sha256:5f24288299e2454931be29f3e410d586178ebfe783bb9ac5b0278367c8ca0900 siz
a9986cd6f37d: Layer already exists
1: digest: sha256:5f24288299e2454931be29f3e410d586178ebfe783bb9ac5b0278367c8ca0900 size: 8
a9986cd6f37d: Layer already exists
latest: digest: sha256:5f24288299e2454931be29f3e410d586178ebfe783bb9ac5b0278367c8ca0900 si
{"name":"lx/app","tags":["1.0","1.0.1","1","latest"]}

بقیه‌ی tag ها به همان image اشاره می‌کنند؛ لایه‌ها قبلاً ارسال شده بودند (Layer already exists) و فقط یک «برچسب» تازه ثبت شد. چهار tag، یک محتوا. همه‌ی آن‌ها digest یکسان دارند (sha256:... در خط‌های بالا).

مثال ۵: دریافت از registry (docker pull)

Section titled “مثال ۵: دریافت از registry (docker pull)”

برای اینکه ثابت کنیم واقعاً از registry می‌آید، نسخه‌های محلی را پاک می‌کنیم:

Terminal window
docker rmi lx-app:1.0 localhost:5301/lx/app:1.0 localhost:5301/lx/app:1.0.1 localhost:5301/lx/app:1 localhost:5301/lx/app:latest >/dev/null 2>&1
docker images --format '{{.Repository}}' | grep -c "lx/app"
docker pull localhost:5301/lx/app:1.0 2>&1 | tail -2
docker run --rm localhost:5301/lx/app:1.0
خروجی
0
Status: Downloaded newer image for localhost:5301/lx/app:1.0
localhost:5301/lx/app:1.0
سلام از image منتشرشده

شمارنده 0 یعنی هیچ نسخه‌ی محلی نمانده؛ بعد docker pull آن را از registry آورد و اجرا شد. این همان کاری است که سرور production می‌کند.

مثال ۶: Docker Hub و GHCR (نمونه، بدون اجرا)

Section titled “مثال ۶: Docker Hub و GHCR (نمونه، بدون اجرا)”

برای Docker Hub یا GHCR حساب و رمز لازم است و من نمی‌توانم روی حساب واقعی چیزی منتشر کنم؛ پس این دستورها «نمونه» هستند (طبق مستندات رسمی). الگو در هر دو یکی است:

Terminal window
# ورود (توکن را از stdin بده تا در تاریخچه‌ی shell نماند)
echo "$TOKEN" | docker login ghcr.io -u USERNAME --password-stdin
# برچسب و ارسال به GitHub Container Registry
docker tag myapp:1.0 ghcr.io/USERNAME/myapp:1.0
docker push ghcr.io/USERNAME/myapp:1.0

برای Docker Hub: docker login (بدون آدرس)، بعد docker tag myapp:1.0 USERNAME/myapp:1.0 و docker push USERNAME/myapp:1.0. در GHCR باید یک Personal Access Token با اجازه‌ی نوشتن بسته (write:packages) بسازی و به‌جای رمز استفاده کنی. بعد از اولین push، می‌توانی در تنظیمات بسته، نمایش آن را public یا private کنی. اسم‌ها باید حروف کوچک باشند.

تمرین اصلی درس (انتشار روی GHCR) را با همین الگو انجام بده: image خودت را بساز، با اسم ghcr.io/نام-کاربری-گیتهاب/اسم:1.0 برچسب بزن، با توکن وارد شو و push کن.

registry چطور ذخیره می‌کند؟

Section titled “registry چطور ذخیره می‌کند؟”

registry (طبق «Registry API v2») دو چیز نگه می‌دارد: blob ها (لایه‌ها و config، هرکدام با هش sha256 محتوایشان آدرس‌دهی می‌شوند) و manifest ها (فهرست blob های یک image). یک tag فقط یک اشاره‌گر قابل‌تغییر از یک اسم به یک manifest است. به همین دلیل:

  • Layer already exists: اگر blob با همان هش قبلاً هست، دوباره ارسال نمی‌شود (صرفه‌جویی و سرعت).
  • tag را می‌شود به محتوای دیگری منتقل کرد (دوباره push)؛ digest هرگز عوض نمی‌شود. برای تضمین، image@sha256:... را pull کن.

ورود (login) کجا ذخیره می‌شود؟

Section titled “ورود (login) کجا ذخیره می‌شود؟”

docker login اطلاعات را در ~/.docker/config.json (یا یک credential helper مثل keychain سیستم) ذخیره می‌کند. به همین دلیل برای رمز از --password-stdin استفاده می‌کنیم و فایل config را مثل رمز محافظت می‌کنیم. روی سرورهای CI توکن‌های کم‌دسترسی بساز.

tag معنی کاربرد
1.4.2 نسخه‌ی دقیق (SemVer) production؛ ثابت و قابل‌ردیابی
1.4 آخرین patch از 1.4 بروزرسانی خودکار patch
1 آخرین minor از 1 بروزرسانی خودکار minor
latest آخرین (طبق قرارداد) فقط توسعه؛ نه production
sha-3f2a1c9 هش commit ردیابی دقیق CI
2026-10-03 تاریخ build های شبانه

قاعده‌ی عملی: هر استقرار را با tag دقیق نسخه (یا digest) انجام بده، نه latest؛ tag های شناور (1.4، latest) را فقط برای راحتی نگه دار.

دستور کاربرد
docker tag SRC DST اسم/tag تازه برای یک image
docker login [registry] ورود
docker push NAME:TAG ارسال
docker pull NAME:TAG دریافت
docker logout [registry] خروج و پاک شدن اعتبار
curl REGISTRY/v2/_catalog فهرست repository ها (API v2)
registry نمونه‌ی اسم توضیح
Docker Hub user/app:1.0 پیش‌فرض (docker.io)
GHCR ghcr.io/user/app:1.0 وابسته به حساب GitHub
محلی/خصوصی localhost:5301/lx/app:1.0 registry:2 یا Harbor، GitLab Registry و …
Terminal window
docker tag alpine lxfake/app:1
docker push lxfake/app:1 2>&1 | tail -2 | cut -c1-140
docker rmi lxfake/app:1 >/dev/null 2>&1
خروجی
a9986cd6f37d: Unavailable
push access denied, repository does not exist or may require authorization: server message: insufficient_scope: authorization failed

بدون docker login (یا با حساب بی‌اجازه)، registry اجازه‌ی نوشتن نمی‌دهد (denied / authorization failed). راه‌حل: docker login با حساب درست و اسم repository که متعلق به حساب خودت باشد.

Terminal window
docker tag alpine MyUser/MyApp:1 2>&1 | cut -c1-140
خروجی
error parsing reference: "MyUser/MyApp:1" is not a valid repository/tag: invalid reference format: repository name (MyApp) must be lowercase

نام repository باید حروف کوچک باشد. راه‌حل: همه‌چیز را کوچک بنویس.

۳) push یک tag که وجود ندارد

Section titled “۳) push یک tag که وجود ندارد”
Terminal window
docker push localhost:5301/lx/nothere:9 2>&1 | tail -2 | cut -c1-130
خروجی
The push refers to repository [localhost:5301/lx/nothere]
tag does not exist: localhost:5301/lx/nothere:9

image با آن اسم روی سیستم نیست (فقط با docker tag یا build ساخته می‌شود). راه‌حل: docker images را ببین و اسم دقیق را برچسب بزن.

Terminal window
docker tag alpine localhost:5399/lx/app:1.0
docker push localhost:5399/lx/app:1.0 2>&1 | tail -1 | cut -c1-150
docker rmi localhost:5399/lx/app:1.0 >/dev/null
خروجی
failed to do request: Head "https://localhost:5399/v2/lx/app/blobs/sha256:33bee74c45f307e3268adc2010c0f55c48e7a6041e12cd12432bb1a46e498e43": dial tcp

در آدرس localhost:5399 registry ای گوش نمی‌دهد (پیام connection refused). راه‌حل: مطمئن شو registry بالاست (docker ps) و آدرس/پورت درست است.

۵) استفاده از latest در production

Section titled “۵) استفاده از latest در production”

نمی‌توانی بفهمی دقیقاً کدام نسخه اجرا می‌شود و بازگشت ممکن نیست. راه‌حل: tag دقیق نسخه یا digest.

۶) registry بدون HTTPS روی آدرسی جز localhost

Section titled “۶) registry بدون HTTPS روی آدرسی جز localhost”

داکر برای آدرس‌هایی غیر از localhost HTTPS می‌خواهد؛ یک registry HTTP ساده با خطای «http: server gave HTTP response to HTTPS client» رد می‌شود. راه‌حل: گواهی HTTPS بگذار (یا در محیط آزمایشی، آدرس را در insecure-registries فهرست کن، که توصیه نمی‌شود).

✎ تمرینآسان

image alpine را با اسم localhost:5301/lx/mytool:2.0 برچسب بزن و ثابت کن ID هر دو یکی است.

دیدن جواب
Terminal window
docker tag alpine localhost:5301/lx/mytool:2.0
docker images --format '{{.Repository}}:{{.Tag}} {{.ID}}' | grep -E "^(alpine:latest|localhost:5301/lx/mytool:2.0) "
docker rmi localhost:5301/lx/mytool:2.0 >/dev/null
خروجی
alpine:latest 294b683cb724
localhost:5301/lx/mytool:2.0 294b683cb724
✎ تمرینمتوسط

image alpine را با اسم localhost:5301/lx/tools:1.0 به registry محلی push کن و با _catalog ثابت کن lx/tools در آن هست.

دیدن جواب
Terminal window
docker tag alpine localhost:5301/lx/tools:1.0
docker push localhost:5301/lx/tools:1.0 >/dev/null 2>&1
curl -s localhost:5301/v2/_catalog
echo
docker rmi localhost:5301/lx/tools:1.0 >/dev/null
خروجی
{"repositories":["lx/app","lx/tools"]}
✎ تمرینسخت

ثابت کن 1.0 و latest در registry یک digest دارند، بعد با digest (نه tag) image را pull کن و اجرا کن. (راهنمایی: digest را از docker image inspect --format '{{index .RepoDigests 0}}' یا از هدر Docker-Content-Digest در API بگیر.)

دیدن جواب
Terminal window
for t in 1.0 latest; do echo "$t: $(curl -sI -H 'Accept: application/vnd.oci.image.index.v1+json, application/vnd.oci.image.manifest.v1+json, application/vnd.docker.distribution.manifest.v2+json' localhost:5301/v2/lx/app/manifests/$t | tr -d '\r' | awk -F': ' 'tolower($1)=="docker-content-digest"{print $2}')"; done
d=$(curl -sI -H 'Accept: application/vnd.oci.image.index.v1+json, application/vnd.oci.image.manifest.v1+json, application/vnd.docker.distribution.manifest.v2+json' localhost:5301/v2/lx/app/manifests/1.0 | tr -d '\r' | awk -F': ' 'tolower($1)=="docker-content-digest"{print $2}')
docker run --rm "localhost:5301/lx/app@$d"
خروجی
1.0: sha256:5f24288299e2454931be29f3e410d586178ebfe783bb9ac5b0278367c8ca0900
latest: sha256:5f24288299e2454931be29f3e410d586178ebfe783bb9ac5b0278367c8ca0900
سلام از image منتشرشده

اگر digest هر دو یکی باشد یعنی هر دو tag به یک محتوا اشاره می‌کنند؛ و pull با digest دقیقاً همان محتوا را می‌دهد، حتی اگر tag بعداً جابه‌جا شود.

؟ آزمونک
  1. در ghcr.io/ali/shop:1.2 کدام بخش registry است؟

  2. docker tag چه می‌کند؟

  3. پیام «Layer already exists» یعنی…

  4. کدام برای production مناسب‌تر است؟

  5. چرا برای docker login رمز را با --password-stdin می‌دهیم؟

  • اسم کامل: registry/namespace/repo:tag؛ بدون registry یعنی Docker Hub؛ حروف کوچک.
  • جریان: docker build ← docker tag ← docker login ← docker push ← (دیگران) docker pull.
  • registry خصوصی محلی با registry:2 ساده راه می‌افتد؛ curl .../v2/_catalog محتوایش را نشان می‌دهد.
  • tag اشاره‌گر قابل‌تغییر است؛ digest ثابت. در production tag دقیق نسخه یا digest، نه latest.
  • لایه‌ها با هش ذخیره می‌شوند؛ Layer already exists یعنی دوباره ارسال نمی‌شود.
  • رمز را با --password-stdin بده و توکن کم‌دسترسی بساز.
برگه‌ی تقلب این درس
دستورکاری که می‌کند
docker tag SRC REG/NS/REPO:TAGاسم کامل به image می‌دهد
docker login ghcr.io -u USER --password-stdinورود با توکن از stdin
docker push REG/NS/REPO:TAGارسال به registry
docker pull REG/NS/REPO:TAGدریافت
docker pull REG/NS/REPO@sha256:...دریافت با digest
docker run -d -p 5000:5000 registry:2registry محلی
curl localhost:5000/v2/_catalogفهرست repository ها
curl localhost:5000/v2/NS/REPO/tags/listtag های یک repository
docker logoutپاک کردن اعتبار ورود