توی این درس یاد میگیری یک image را بدون هیچ registry و بدون اینترنت از یک ماشین به ماشین دیگر ببری: با docker save آن را در یک فایل tar میگذاری، با docker load روی ماشین مقصد بازش میکنی، با gzip حجمش را کم میکنی، با scp یا یک خط لولهی ssh به سرور میفرستی و با هش مطمئن میشوی سالم رسیده. میفهمی داخل آن tar چیست، چطور چند image را یکجا ببری، و فرق docker save با docker export چیست (که اشتباه گرفتنشان رایج است).
مسئله: سروری که به registry نمیرسد
Section titled “مسئله: سروری که به registry نمیرسد”سرور production پشت یک شبکهی بسته است: اینترنت ندارد، یا به Docker Hub و registry شرکت دسترسی ندارد. ولی تو image را روی لپتاپت ساختهای. راهحل: image را مثل یک فایل معمولی ببر، همانطور که یک فایل فشرده را روی فلش میبری.
تشبیه: بستهبندی برای حمل
Section titled “تشبیه: بستهبندی برای حمل”registry مثل انبار مرکزی است که هر کس خودش سفارش میدهد. docker save مثل بستهبندی کردن کالا در یک کارتن مهر و مومشده است: کارتن را با هر وسیلهای (فلش، ssh، دست) میبری و در مقصد باز میکنی؛ محتوا و مهر (هش) همان است.
مثالهای عملی
Section titled “مثالهای عملی”مثال ۱: ساخت یک image برای انتقال
Section titled “مثال ۱: ساخت یک image برای انتقال”یک اپ کوچک وب (فقط با کتابخانهی استاندارد پایتون) را میسازیم:
from http.server import BaseHTTPRequestHandler, HTTPServer
class Handler(BaseHTTPRequestHandler): def do_GET(self): self.send_response(200) self.send_header("Content-Type", "text/plain; charset=utf-8") self.end_headers() self.wfile.write("سلام از image منتقلشده\n".encode())
HTTPServer(("0.0.0.0", 8000), Handler).serve_forever()FROM python:3.12-alpineWORKDIR /appCOPY server.py .EXPOSE 8000CMD ["python", "server.py"]docker build -q -t lx-app:1.0 off/app >/dev/nulldocker image ls lx-app --format 'image: {{.Repository}}:{{.Tag}} حجم: {{.Size}}'docker image inspect lx-app:1.0 --format 'معماری: {{.Os}}/{{.Architecture}}'image: lx-app:1.0 حجم: 87.9MBمعماری: linux/arm64به معماری (مثلاً arm64 روی مکهای Apple Silicon یا amd64 روی بیشتر سرورها) دقت کن: image ساختهشده برای یک معماری روی معماری دیگر درست اجرا نمیشود (مثال ۹).
مثال ۲: docker save
Section titled “مثال ۲: docker save”docker save -o app.tar lx-app:1.0ls -l app.tar | awk '{print "app.tar:", $5, "بایت"}'echo "--- محتوای فایل (برخی ورودیها):"tar -tf app.tar | grep -vE '^blobs/sha256/.{20,}' | head -8echo "تعداد بلابها (لایهها و فایلهای JSON): $(tar -tf app.tar | grep -c '^blobs/sha256/.')"app.tar: 21432320 بایت--- محتوای فایل (برخی ورودیها):blobs/blobs/sha256/index.jsonmanifest.jsonoci-layoutتعداد بلابها (لایهها و فایلهای JSON): 12خروجی یک tar عادی است که میتوانی با tar -tf ببینی. شامل manifest.json و index.json (فهرست image ها و لایهها) و پوشهی blobs/sha256/ که هر لایه و هر فایل پیکربندی در آن با هش محتوایش نامگذاری شده است. -o مقصد فایل را میدهد؛ بهجای آن میتوانی با > خروجی را به جایی هدایت کنی.
اگر docker save را بدون -o اجرا کنی و خروجی را به gzip پایپ کنی، داده کجا میرود؟
save بدون -o روی stdout مینویسد؛ پس با | و > میشود دستکاریاش کرد.
مثال ۳: فشردهسازی با gzip
Section titled “مثال ۳: فشردهسازی با gzip”docker save lx-app:1.0 | gzip > app.tar.gzls -l app.tar app.tar.gz | awk '{print $9 ":", $5, "بایت"}'gzip -t app.tar.gz && echo "فایل gzip سالم است"app.tar: 21432320 بایتapp.tar.gz: 21300863 بایتفایل gzip سالم استdocker save بدون -o روی stdout مینویسد، پس میتوانی با | به gzip بدهی. فایل کوچکتر شد (لایههای image معمولاً از قبل فشردهاند، پس صرفهجویی گاهی کم است؛ برای imageهای بزرگ ممکن است قابلتوجه باشد). بهجای gzip میتوان از xz (فشردهتر، کندتر) یا zstd (سریع) هم استفاده کرد.
مثال ۴: سرور آزمایشی با ssh
Section titled “مثال ۴: سرور آزمایشی با ssh”برای تمرین، یک «سرور» جدا میسازیم: یک daemon داکر داخل یک کانتینر (بدون هیچ image از قبل) که ssh هم دارد. یک کلید ssh موقت هم فقط برای همین آزمایش میسازیم:
docker run -d --privileged --name lx-srv -p 2299:22 -e DOCKER_TLS_CERTDIR= docker:dind >/dev/nulluntil docker exec lx-srv docker info >/dev/null 2>&1; do sleep 1; donedocker exec lx-srv sh -c 'apk add --no-cache openssh >/dev/null 2>&1; ssh-keygen -A >/dev/null; sed -i "s/^root:!:/root:*:/" /etc/shadow; mkdir -p /root/.ssh'ssh-keygen -q -t ed25519 -N '' -C lx-demo -f off/keydocker cp off/key.pub lx-srv:/root/.ssh/authorized_keysdocker exec lx-srv sh -c 'chown root:root /root/.ssh/authorized_keys; chmod 700 /root/.ssh; chmod 600 /root/.ssh/authorized_keys; /usr/sbin/sshd'sleep 1SSHOPT="-i $PWD/off/key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR -o BatchMode=yes"ssh $SSHOPT -p 2299 root@localhost 'echo "ssh به سرور آزمایشی وصل شد"; docker images -q | wc -l | sed "s/^/image ها روی سرور: /"'ssh به سرور آزمایشی وصل شدimage ها روی سرور: 0سرور هیچ imageای ندارد (صفر). کلید خصوصی فقط در یک پوشهی موقت است و بعد از درس پاک میشود. (در واقعیت، رمز و کلید خودت را هرگز در درس یا Git نگذار؛ فقط از کلید عمومی در authorized_keys سرور استفاده میشود.)
مثال ۵: انتقال با scp و بازکردن با docker load
Section titled “مثال ۵: انتقال با scp و بازکردن با docker load”SSHOPT="-i $PWD/off/key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR -o BatchMode=yes"scp $SSHOPT -P 2299 app.tar.gz root@localhost:/var/tmp/app.tar.gzecho "--- روی سرور:"ssh $SSHOPT -p 2299 root@localhost 'docker load -i /var/tmp/app.tar.gz; docker images --format "{{.Repository}}:{{.Tag}}"'--- روی سرور:Loaded image: lx-app:1.0lx-app:1.0scp فایل را از طریق ssh کپی کرد و docker load -i آن را روی سرور بازگرداند: پیام Loaded image: lx-app:1.0 و image حالا در فهرست سرور است. نکته: docker load فایل .tar.gz را هم مستقیم میخواند؛ لازم نیست اول باز کنی.
مثال ۶: اجرا روی سرور و تأیید یکپارچگی
Section titled “مثال ۶: اجرا روی سرور و تأیید یکپارچگی”آیا آنچه روی سرور هست دقیقاً همان است؟ شناسهی image (هش پیکربندی) را در دو طرف مقایسه میکنیم و اپ را روی سرور اجرا میکنیم:
SSHOPT="-i $PWD/off/key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR -o BatchMode=yes"mine=$(docker image inspect lx-app:1.0 --format '{{.Id}}')theirs=$(ssh $SSHOPT -p 2299 root@localhost "docker image inspect lx-app:1.0 --format '{{.Id}}'")[ "$mine" = "$theirs" ] && echo "شناسهی image در مبدأ و مقصد یکی است"echo "شناسه: ${mine:0:19}…"ssh $SSHOPT -p 2299 root@localhost 'docker run -d --name lx-off1 lx-app:1.0 >/dev/null; sleep 2; docker exec lx-off1 python -c "import urllib.request; print(urllib.request.urlopen(\"http://localhost:8000\").read().decode(), end=\"\")"'echo "sha256 فایل: $(shasum -a 256 app.tar.gz | cut -c1-16)… (قبل از ارسال؛ بعد از ارسال هم همین را بسنج)"شناسهی image در مبدأ و مقصد یکی استشناسه: sha256:e39b1daff10a…سلام از image منتقلشدهsha256 فایل: 34973e6b04b0dfd2… (قبل از ارسال؛ بعد از ارسال هم همین را بسنج)شناسهی image برابر است و اپ روی سرور (بدون هیچ اتصال به registry) اجرا شد. برای فایل خودِ tar هم قبل و بعد از انتقال sha256sum (لینوکس) یا shasum -a 256 (مک) بگیر و مقایسه کن؛ این کار خرابی نیمهراه را پیدا میکند.
مثال ۷: بدون فایل میانی: save | ssh load
Section titled “مثال ۷: بدون فایل میانی: save | ssh load”میشود هیچ فایلی روی دیسک نماند: خروجی save را مستقیم از ssh به load سرور بدهی. اول یک نسخهی جدید (۲.۰) بسازیم و بفرستیم:
SSHOPT="-i $PWD/off/key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR -o BatchMode=yes"docker tag lx-app:1.0 lx-app:2.0docker save lx-app:2.0 | gzip | ssh $SSHOPT -p 2299 root@localhost 'gunzip | docker load'ssh $SSHOPT -p 2299 root@localhost 'docker images --format "{{.Repository}}:{{.Tag}}" | sort'Loaded image: lx-app:2.0lx-app:1.0lx-app:2.0خط لولهی save | gzip | ssh ... 'gunzip | docker load' همهچیز را بدون فایل موقت منتقل میکند. دو نکته: ۱) image جدید همان شناسهی قبلی را دارد (فقط tag تازه است)، پس load سریع بود؛ ۲) برای یک ارتباط ناپایدار، فایل میانی بهتر است چون میتوانی انتقال را از وسط ادامه بدهی (rsync --partial).
مثال ۸: چند image در یک فایل
Section titled “مثال ۸: چند image در یک فایل”docker save -o both.tar lx-app:1.0 alpinels -l app.tar both.tar | awk '{print $9 ":", $5, "بایت"}'echo "فهرست image های داخل both.tar:"tar -xOf both.tar index.json | python3 -c "import json,sys; [print(' ', m.get('annotations',{}).get('io.containerd.image.name') or m.get('annotations',{}).get('org.opencontainers.image.ref.name','?')) for m in json.load(sys.stdin)['manifests']]"app.tar: 21432320 بایتboth.tar: 25479168 بایتفهرست image های داخل both.tar: docker.io/library/lx-app:1.0 docker.io/library/alpine:latestبا نوشتن چند نام بعد از save همهی آنها در یک tar جمع میشوند و لایههای مشترک فقط یک بار ذخیره میشوند. این برای بردن یک پروژهی چندسرویسی (اپ + دیتابیس + nginx) در یک فایل عالی است. با docker load -i both.tar همه با هم برمیگردند.
مثال ۹: معماری درست برای سرور
Section titled “مثال ۹: معماری درست برای سرور”اگر لپتاپ تو arm64 است و سرور amd64، image ساختهشده روی لپتاپ روی سرور اجرا نمیشود. هنگام pull یا build معماری مقصد را مشخص کن:
docker pull --platform linux/amd64 alpine >/dev/null 2>&1docker pull --platform linux/arm64 alpine >/dev/null 2>&1docker image inspect --platform linux/amd64 alpine --format 'نسخهی اول: {{.Os}}/{{.Architecture}}'docker image inspect --platform linux/arm64 alpine --format 'نسخهی دوم: {{.Os}}/{{.Architecture}}'echo "معماری daemon من: $(docker version --format '{{.Server.Arch}}')"echo "--- اجرای نسخهی معماری دیگر:"docker run --rm --platform linux/amd64 alpine uname -m 2>&1 | tail -1docker run --rm --platform linux/arm64 alpine uname -m 2>&1 | tail -1نسخهی اول: linux/amd64نسخهی دوم: linux/arm64معماری daemon من: arm64--- اجرای نسخهی معماری دیگر:exec /bin/uname: exec format erroraarch64--platform معماری مشخص را میکشد (و docker image inspect --platform همان را نشان میدهد). روی ماشین من (Apple Silicon، معماری arm64) نسخهی amd64 اجرا نمیشود و خطای exec format error میدهد؛ یعنی بدون شبیهساز، باینری برای CPU دیگر قابل اجرا نیست. روی ماشین amd64 نتیجه برعکس است. برای ساخت image مخصوص سرور: docker build --platform linux/amd64 -t myapp . (با شبیهساز کار میکند ولی کندتر است). و بعد از load روی سرور: docker image inspect IMG --format '{{.Architecture}}'.
مثال ۱۰: save یا export؟
Section titled “مثال ۱۰: save یا export؟”docker export محتوای فایلسیستم یک کانتینر را در tar میریزد؛ نه لایهها، نه CMD، نه ENV. docker import آن را به یک image تکلایه و بدون متادیتا تبدیل میکند:
docker create --name lx-exp lx-app:1.0 >/dev/nulldocker export lx-exp -o exp.tardocker import exp.tar lx-imported:1.0 >/dev/nullecho "CMD در image اصلی: $(docker image inspect lx-app:1.0 --format '{{.Config.Cmd}}')"echo "CMD بعد از export/import: $(docker image inspect lx-imported:1.0 --format '{{.Config.Cmd}}')"echo "لایهها: اصلی=$(docker image inspect lx-app:1.0 --format '{{len .RootFS.Layers}}') / importشده=$(docker image inspect lx-imported:1.0 --format '{{len .RootFS.Layers}}')"docker rm lx-exp >/dev/null; docker rmi lx-imported:1.0 >/dev/nullCMD در image اصلی: [python server.py]CMD بعد از export/import: []لایهها: اصلی=6 / importشده=1بعد از export/import، CMD از دست رفت ([]) و لایهها یکی شدند. پس برای جابهجایی image همیشه save و load؛ export فقط وقتی که واقعاً فقط فایلهای یک کانتینر را میخواهی.
پشت پرده
Section titled “پشت پرده”image در داکر لایههایی با هش است. docker save یک آرشیو استاندارد (OCI) مینویسد: index.json و manifest.json میگویند «این image از کدام لایهها و با چه پیکربندی ساخته شده»، و هر لایه و هر فایل پیکربندی در blobs/sha256/<هش> است. چون اسم هر بلاب هش محتوایش است، load میتواند درستی را بسنجد و لایههایی که از قبل دارد را دوباره ننویسد. همین است که ارسال نسخهی ۲.۰ در مثال ۷ تقریباً فوری بود.
نکتههای مهم:
saveوloadtag ها را هم میبرند. اگر دو بار load کنی با نامی که از قبل هست، tag به image جدید منتقل میشود و image قبلی بدون tag (dangling) میماند.- حجم فایل تقریباً برابر مجموع لایههاست، پس image کوچک (درس کوچک کردن image) انتقال را هم سادهتر میکند.
docker loadبرای معماری کنترلی ندارد؛ فایل را بدون بررسی میپذیرد. خطا بعداً هنگام اجرا میآید.- فایل tar حاوی همهی محتوای image است، از جمله رازهایی که احتمالاً داخلش مانده (درس امنیت). آن را مثل یک فایل حساس نگه دار.
جدولهای مرجع
Section titled “جدولهای مرجع”| دستور | کار |
|---|---|
docker save -o f.tar IMG[:TAG] ... |
ذخیرهی یک یا چند image در فایل |
docker save IMG | gzip > f.tar.gz |
ذخیره و فشردهسازی |
docker load -i f.tar[.gz] |
بازگرداندن image ها از فایل |
docker load < f.tar |
همان، از stdin |
docker save IMG | ssh host 'docker load' |
انتقال مستقیم بدون فایل میانی |
docker export C -o f.tar |
فایلسیستم یک کانتینر (بدون متادیتا) |
docker import f.tar NAME |
ساخت image تکلایه از tar فایلسیستم |
save/load |
export/import |
|---|---|
image با لایهها، tag و پیکربندی (CMD، ENV،…) |
فقط فایلهای یک کانتینر |
| image را همانطور که هست میبرد | یک image تکلایهی بیمتادیتا میسازد |
| برای انتقال image | برای گرفتن «عکس فایلها» |
اشتباهات رایج
Section titled “اشتباهات رایج”۱) نام image اشتباه در save
Section titled “۱) نام image اشتباه در save”docker save -o fake.tar lx-no-such-image:9 2>&1 | head -1 | cut -c1-140Error response from daemon: No such image: lx-no-such-image:9نام یا tag وجود ندارد. راهحل: docker image ls را ببین و نام دقیق را بنویس.
۲) save بدون -o روی ترمینال (نمونه)
Section titled “۲) save بدون -o روی ترمینال (نمونه)”اگر docker save IMG را بدون -o و بدون > یا | در یک ترمینال تعاملی بزنی، داکر دادهی باینری را روی صفحه نمیریزد و خطا میدهد (این حالت در اسکریپت خودکار قابل اجرا نیست، پس متن زیر «نمونه» است و اجرا نشده):
cowardly refusing to save to a terminal. Use the -o flag or redirect.راهحل: همیشه -o فایل یا > فایل یا | ... بگذار.
۳) load از فایلی که image نیست
Section titled “۳) load از فایلی که image نیست”echo "این tar نیست" > fake.tardocker load -i fake.tar 2>&1 | head -1 | cut -c1-120unexpected EOFفایل فرمت درست ندارد (مثلاً دانلود ناقص یا فایلی که با export ساخته شده). راهحل: هش فایل را با مبدأ مقایسه کن؛ فایل export را با import باز کن نه load.
۴) معماری نامطابق
Section titled “۴) معماری نامطابق”مثال ۹: image یک معماری روی معماری دیگر اجرا نمیشود (یا با شبیهساز کند اجرا میشود). راهحل: با --platform برای معماری سرور بساز یا بکش.
۵) گرفتن فایلهای بزرگ در یک مرحله از شبکهی ناپایدار
Section titled “۵) گرفتن فایلهای بزرگ در یک مرحله از شبکهی ناپایدار”یک انتقال چندگیگابایتی ممکن است وسط قطع شود. راهحل: فایل را روی دیسک بگیر و با rsync --partial -e ssh (ادامهپذیر) بفرست؛ بعد هش را بسنج.
image alpine را با docker save در فایلی به اسم a.tar بریز، حجمش را ببین و با tar -tf بررسی کن فایل manifest.json در آن هست.
دیدن جواب
docker save -o a.tar alpinels -l a.tar | awk '{print "a.tar:", $5, "بایت"}'tar -tf a.tar | grep -x manifest.jsona.tar: 8240128 بایتmanifest.jsonتمرین اصلی: یک image را روی سیستم خودت بساز و روی «سرور» load کن. یک Dockerfile ساده (alpine با یک فایل متنی و CMD ["cat","/note.txt"]) بساز، با save | gzip به سرور آزمایشی (lx-srv از مثال ۴) بفرست، روی آن load کن و آن را اجرا کن.
دیدن جواب
mkdir -p e2 && printf 'FROM alpine\nRUN echo "سلام از سرور بدون اینترنت" > /note.txt\nCMD ["cat", "/note.txt"]\n' > e2/Dockerfiledocker build -q -t lx-note:1.0 e2 >/dev/nullSSHOPT="-i $PWD/off/key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR -o BatchMode=yes"docker save lx-note:1.0 | gzip | ssh $SSHOPT -p 2299 root@localhost 'gunzip | docker load'ssh $SSHOPT -p 2299 root@localhost 'docker run --rm lx-note:1.0'docker rmi lx-note:1.0 >/dev/nullLoaded image: lx-note:1.0سلام از سرور بدون اینترنتدو image (lx-app:1.0 و alpine) را در یک فایل بههمراه یک فایل هش (SHA256SUMS) بفرست و روی سرور اول هش را بسنج (sha256sum -c) و بعد load کن؛ در پایان ثابت کن هر دو image روی سرور هستند. (روی سرور sha256sum از busybox هست.)
دیدن جواب
SSHOPT="-i $PWD/off/key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR -o BatchMode=yes"docker save -o pack.tar lx-app:1.0 alpineshasum -a 256 pack.tar | sed 's# pack.tar# /var/tmp/pack.tar#' > SHA256SUMSscp -q $SSHOPT -P 2299 pack.tar SHA256SUMS root@localhost:/var/tmp/ssh $SSHOPT -p 2299 root@localhost 'cd /var/tmp && sha256sum -c SHA256SUMS && docker load -i pack.tar && docker images --format "{{.Repository}}:{{.Tag}}" | sort'/var/tmp/pack.tar: OKLoaded image: lx-app:1.0Loaded image: alpine:latestalpine:latestlx-app:1.0lx-app:2.0lx-note:1.0آزمونک
Section titled “آزمونک”docker save چه چیزی را ذخیره میکند؟
برای انتقال image از save و load استفاده کن.
فرق export با save؟
بعد از export/import مثلاً CMD از بین میرود.
دستور مقصد برای فایل app.tar.gz؟
load فایل gzip را مستقیم میخواند.
کدام یک بدون ساخت فایل میانی منتقل میکند؟
خروجی save روی stdout به ورودی load روی سرور میرود.
مهمترین چیزی که باید قبل از انتقال به سرور بررسی کنی؟
image معماری نامطابق درست اجرا نمیشود.
چرا ارسال دوبارهی نسخهی جدید با لایههای مشترک سریع است؟
content-addressable بودن لایهها.
جمعبندی
Section titled “جمعبندی”docker save -o f.tar IMG(یا| gzip) image را در فایل میگذارد؛docker load -i f.tar[.gz]برمیگرداند؛ tag ها همراه میروند.- انتقال:
scp، یا خط لولهیdocker save IMG | gzip | ssh host 'gunzip | docker load'بدون فایل میانی. - درستی را با
sha256sumفایل و مقایسهی شناسهی image (docker image inspect --format '{{.Id}}') بسنج. - معماری مبدأ و سرور باید یکی باشد؛ با
--platformبساز یا بکش. export/importفقط فایلهای یک کانتینر را میبرد (بدونCMDو لایهها)؛ برای انتقال image نه.- فایل tar همهی محتوای image را دارد؛ مثل یک فایل حساس نگهش دار.
| دستور | کاری که میکند |
|---|---|
docker save -o app.tar myapp:1.0 | ذخیرهی image در فایل |
docker save myapp:1.0 | gzip > app.tar.gz | ذخیرهی فشرده |
docker load -i app.tar.gz | بازگرداندن image از فایل |
docker save myapp | gzip | ssh host 'gunzip | docker load' | انتقال مستقیم بدون فایل میانی |
scp app.tar.gz user@host:/path/ | کپی فایل به سرور |
docker save -o pack.tar a b | چند image در یک فایل |
docker image inspect IMG --format '{{.Architecture}}' | معماری image |
docker pull --platform linux/amd64 IMG | گرفتن نسخهی معماری مشخص |
docker export C -o f.tar | فایلسیستم کانتینر (نه برای انتقال image) |