توی این درس یاد میگیری 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 آن را میفرستد.
مسیر انتشار
Section titled “مسیر انتشار”مثالهای عملی
Section titled “مثالهای عملی”مثال ۱: یک registry خصوصی روی سیستم خودت
Section titled “مثال ۱: یک registry خصوصی روی سیستم خودت”خود registry هم یک image رسمی است (registry:2). یکی را روی پورت 5301 بالا میآوریم:
docker run -d --name lx-reg -p 5301:5000 registry:2 >/dev/nullsleep 2curl -s localhost:5301/v2/echocurl -s -o /dev/null -w "HTTP %{http_code}\n" localhost:5301/v2/{}HTTP 200جواب {} و 200 یعنی registry زنده است و API نسخهی ۲ را میفهمد. هنوز هیچ image ای ندارد.
مثال ۲: ساخت و برچسبزدن (docker tag)
Section titled “مثال ۲: ساخت و برچسبزدن (docker tag)”FROM alpineCMD ["echo", "سلام از image منتشرشده"]docker build -q -t lx-app:1.0 ./app >/dev/null 2>&1docker tag lx-app:1.0 localhost:5301/lx/app:1.0docker images --format 'table {{.Repository}}\t{{.Tag}}\t{{.ID}}' | grep -E "REPOSITORY|lx-app|lx/app"REPOSITORY TAG IMAGE IDlx-app 1.0 5f24288299e2localhost: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) است.
مثال ۳: ارسال (docker push)
Section titled “مثال ۳: ارسال (docker push)”docker push localhost:5301/lx/app:1.0 2>&1 | awk '!s[$0]++' | cut -c1-110 | head -8The push refers to repository [localhost:5301/lx/app]1caaaf6c63d7: Waiting44136fa355b3: Waitinga9986cd6f37d: Waiting1caaaf6c63d7: Pushed44136fa355b3: Pusheda9986cd6f37d: Pushed1.0: digest: sha256:5f24288299e2454931be29f3e410d586178ebfe783bb9ac5b0278367c8ca0900 size: 855هر لایه جدا ارسال میشود (Pushed)، و در پایان digest و اندازهی manifest چاپ میشود. حالا ببین registry چه دارد:
curl -s localhost:5301/v2/_catalog; echocurl -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» است؟
بخش قبل از اولین اسلش، آدرس registry است؛ بعدش namespace (lx)، repository (app) و در آخر tag (1.0).
مثال ۴: چند tag، یک image (و ارسال هوشمند)
Section titled “مثال ۴: چند tag، یک image (و ارسال هوشمند)”docker tag lx-app:1.0 localhost:5301/lx/app:1.0.1docker tag lx-app:1.0 localhost:5301/lx/app:1docker tag lx-app:1.0 localhost:5301/lx/app:latestfor 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; donecurl -s localhost:5301/v2/lx/app/tags/list; echoa9986cd6f37d: Layer already exists1.0.1: digest: sha256:5f24288299e2454931be29f3e410d586178ebfe783bb9ac5b0278367c8ca0900 siza9986cd6f37d: Layer already exists1: digest: sha256:5f24288299e2454931be29f3e410d586178ebfe783bb9ac5b0278367c8ca0900 size: 8a9986cd6f37d: Layer already existslatest: 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 میآید، نسخههای محلی را پاک میکنیم:
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>&1docker images --format '{{.Repository}}' | grep -c "lx/app"docker pull localhost:5301/lx/app:1.0 2>&1 | tail -2docker run --rm localhost:5301/lx/app:1.00Status: Downloaded newer image for localhost:5301/lx/app:1.0localhost:5301/lx/app:1.0سلام از image منتشرشدهشمارنده 0 یعنی هیچ نسخهی محلی نمانده؛ بعد docker pull آن را از registry آورد و اجرا شد. این همان کاری است که سرور production میکند.
مثال ۶: Docker Hub و GHCR (نمونه، بدون اجرا)
Section titled “مثال ۶: Docker Hub و GHCR (نمونه، بدون اجرا)”برای Docker Hub یا GHCR حساب و رمز لازم است و من نمیتوانم روی حساب واقعی چیزی منتشر کنم؛ پس این دستورها «نمونه» هستند (طبق مستندات رسمی). الگو در هر دو یکی است:
# ورود (توکن را از stdin بده تا در تاریخچهی shell نماند)echo "$TOKEN" | docker login ghcr.io -u USERNAME --password-stdin
# برچسب و ارسال به GitHub Container Registrydocker tag myapp:1.0 ghcr.io/USERNAME/myapp:1.0docker 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 کن.
پشت پرده
Section titled “پشت پرده”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 گذاری
Section titled “استراتژی tag گذاری”| 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) را فقط برای راحتی نگه دار.
جدولهای مرجع
Section titled “جدولهای مرجع”| دستور | کاربرد |
|---|---|
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 و … |
اشتباهات رایج
Section titled “اشتباهات رایج”۱) push بدون ورود
Section titled “۱) push بدون ورود”docker tag alpine lxfake/app:1docker push lxfake/app:1 2>&1 | tail -2 | cut -c1-140docker rmi lxfake/app:1 >/dev/null 2>&1a9986cd6f37d: Unavailablepush 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 که متعلق به حساب خودت باشد.
۲) اسم با حروف بزرگ
Section titled “۲) اسم با حروف بزرگ”docker tag alpine MyUser/MyApp:1 2>&1 | cut -c1-140error 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 که وجود ندارد”docker push localhost:5301/lx/nothere:9 2>&1 | tail -2 | cut -c1-130The push refers to repository [localhost:5301/lx/nothere]tag does not exist: localhost:5301/lx/nothere:9image با آن اسم روی سیستم نیست (فقط با docker tag یا build ساخته میشود). راهحل: docker images را ببین و اسم دقیق را برچسب بزن.
۴) registry خاموش است
Section titled “۴) registry خاموش است”docker tag alpine localhost:5399/lx/app:1.0docker push localhost:5399/lx/app:1.0 2>&1 | tail -1 | cut -c1-150docker rmi localhost:5399/lx/app:1.0 >/dev/nullfailed 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 هر دو یکی است.
دیدن جواب
docker tag alpine localhost:5301/lx/mytool:2.0docker images --format '{{.Repository}}:{{.Tag}} {{.ID}}' | grep -E "^(alpine:latest|localhost:5301/lx/mytool:2.0) "docker rmi localhost:5301/lx/mytool:2.0 >/dev/nullalpine:latest 294b683cb724localhost:5301/lx/mytool:2.0 294b683cb724image alpine را با اسم localhost:5301/lx/tools:1.0 به registry محلی push کن و با _catalog ثابت کن lx/tools در آن هست.
دیدن جواب
docker tag alpine localhost:5301/lx/tools:1.0docker push localhost:5301/lx/tools:1.0 >/dev/null 2>&1curl -s localhost:5301/v2/_catalogechodocker 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 بگیر.)
دیدن جواب
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}')"; doned=$(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:5f24288299e2454931be29f3e410d586178ebfe783bb9ac5b0278367c8ca0900latest: sha256:5f24288299e2454931be29f3e410d586178ebfe783bb9ac5b0278367c8ca0900سلام از image منتشرشدهاگر digest هر دو یکی باشد یعنی هر دو tag به یک محتوا اشاره میکنند؛ و pull با digest دقیقاً همان محتوا را میدهد، حتی اگر tag بعداً جابهجا شود.
آزمونک
Section titled “آزمونک”در ghcr.io/ali/shop:1.2 کدام بخش registry است؟
بخش قبل از اولین اسلش، آدرس registry است.
docker tag چه میکند؟
ارسال با push است.
پیام «Layer already exists» یعنی…
لایهها بر پایهی هش محتوا ذخیره میشوند و بین image ها مشترکاند.
کدام برای production مناسبتر است؟
قابلردیابی و قابلبازگشت.
چرا برای docker login رمز را با --password-stdin میدهیم؟
اطلاعات ورود حساساند.
جمعبندی
Section titled “جمعبندی”- اسم کامل:
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:2 | registry محلی |
curl localhost:5000/v2/_catalog | فهرست repository ها |
curl localhost:5000/v2/NS/REPO/tags/list | tag های یک repository |
docker logout | پاک کردن اعتبار ورود |