توی این درس یاد میگیری چرا image کوچکتر بهتر است (pull سریعتر، deploy سریعتر، سطح حملهی کمتر) و چطور اندازهاش را با docker images و docker history بسنجی. بعد چهار ابزار اصلی کوچک کردن را عملی میبینی: انتخاب base image مناسب (full، slim، alpine)، ترکیب دستورهای RUN و پاککردن کش در همان لایه، multi-stage و ساخت از scratch، و آشنایی با distroless. در تمرین اصلی حجم یک image را حداقل نصف میکنی.
مسئله: چرا حجم مهم است؟
Section titled “مسئله: چرا حجم مهم است؟”هر بار که یک سرور جدید به سرویس اضافه میشود، یا CI یک build تازه میکند، image باید دانلود شود. image چند صد مگابایتی یعنی دقایق انتظار، هزینهی ترافیک و فضای دیسک؛ و هر ابزار اضافه داخلش (شل، کامپایلر، curl) یک راه اضافه برای مهاجم است. در شبکهای که دانلود کند یا گران است (مثل خیلی از جاها در ایران)، فرقش محسوستر هم هست.
تشبیه: چمدان سفر
Section titled “تشبیه: چمدان سفر”وقتی برای سفر چمدان میبندی، تصمیم میگیری چه چیزی لازم است. image هم یک چمدان است: اگر کل خانه را ببری (سیستمعامل کامل، کامپایلر، مستندات) حملونقلش سخت است. فقط آنچه اپ برای اجرا لازم دارد را ببر؛ ابزار ساخت را همانجا که ساختی بگذار.
مثالهای عملی
Section titled “مثالهای عملی”مثال ۱: اندازهی base های رایج
Section titled “مثال ۱: اندازهی base های رایج”docker image ls --format '{{.Repository}}:{{.Tag}} {{.Size}}' | grep -E '^(ubuntu:24.04|python:3.12-slim|python:3.12-alpine|node:22-alpine) ' | sort -k2 -h | column -techo "alpine:latest $(( $(docker image inspect alpine --format '{{.Size}}') / 1000000 )) MB (اندازهی واقعی برای معماری این ماشین)"python:3.12-alpine 88.5MBubuntu:24.04 141MBpython:3.12-slim 216MBnode:22-alpine 234MBalpine:latest 13 MB (اندازهی واقعی برای معماری این ماشین)همان زبان پایتون در دو base: python:3.12-slim (Debian کوچکشده) حدود ۲۱۰ مگابایت و python:3.12-alpine (Alpine) حدود ۹۰ مگابایت. و alpine خالی فقط چند مگابایت. انتخاب base بزرگترین اهرم کوچک کردن است.
| base | معمول | نکته |
|---|---|---|
python:3.12 (کامل) |
~۱ گیگابایت | کامپایلر و ابزار زیاد؛ فقط برای build |
python:3.12-slim |
~۲۰۰ مگابایت | Debian با glibc؛ سازگاری بالا، نقطهی شروع خوب |
python:3.12-alpine |
~۹۰ مگابایت | Alpine با musl؛ کوچک ولی بعضی کتابخانههای باینری نیاز به ساخت دارند |
alpine / busybox |
۱–۱۵ مگابایت | پایهی کوچک برای ابزارها |
scratch |
۰ | خالی؛ فقط برای باینری استاتیک |
مثال ۲: همان اپ، سه ساخت
Section titled “مثال ۲: همان اپ، سه ساخت”یک اپ Flask کوچک:
from flask import Flask
app = Flask(__name__)
@app.get("/")def home(): return "hello\n"flask==3.0.3نسخهی اول، ساده و بدون فکر (کش pip در image میماند):
FROM python:3.12-slimWORKDIR /appCOPY app/requirements.txt .RUN pip install -r requirements.txtCOPY app/app.py .CMD ["python", "-m", "flask", "run", "--host", "0.0.0.0"]نسخهی دوم: همان base، ولی --no-cache-dir:
FROM python:3.12-slimWORKDIR /appCOPY app/requirements.txt .RUN pip install --no-cache-dir -r requirements.txtCOPY app/app.py .CMD ["python", "-m", "flask", "run", "--host", "0.0.0.0"]نسخهی سوم: روی alpine:
FROM python:3.12-alpineWORKDIR /appCOPY app/requirements.txt .RUN pip install --no-cache-dir -r requirements.txtCOPY app/app.py .CMD ["python", "-m", "flask", "run", "--host", "0.0.0.0"]cd szdocker build -q -f slim.Dockerfile -t lx-sz-slim . >/dev/nulldocker build -q -f slim-nocache.Dockerfile -t lx-sz-slim-nocache . >/dev/nulldocker build -q -f alpine.Dockerfile -t lx-sz-alpine . >/dev/nulldocker image ls --format '{{.Repository}} {{.Size}}' | grep '^lx-sz-' | sort | column -tlx-sz-alpine 108MBlx-sz-slim 237MBlx-sz-slim-nocache 235MBسه عدد را مقایسه کن: --no-cache-dir چند مگابایت (کش دانلودهای pip) را حذف کرد و alpine بقیهی تفاوت را. اپ همان است، فقط base و یک flag فرق کرد. (روی alpine ممکن است بعضی بستههای پایتون که وابسته به glibc یا کامپایلر هستند مشکل بسازند؛ همیشه اپ را بعد از تغییر base تست کن.)
کدام یک معمولاً بیشترین کاهش حجم را در image پایتونی میدهد؟
base بزرگترین بخش حجم است.
مثال ۳: دیدن لایهها با docker history
Section titled “مثال ۳: دیدن لایهها با docker history”docker history lx-sz-slim --format '{{.Size}}\t{{.CreatedBy}}' | cut -c1-90 | head -80B CMD ["python" "-m" "flask" "run" "--host" "0…12.3kB COPY app/app.py . # buildkit16.9MB RUN /bin/sh -c pip install -r requirements.t…12.3kB COPY app/requirements.txt . # buildkit8.19kB WORKDIR /app0B CMD ["python3"]16.4kB RUN /bin/sh -c set -eux; for src in idle3 p…44.6MB RUN /bin/sh -c set -eux; savedAptMark="$(a…هر ردیف یک لایه است: اندازهاش و دستوری که ساختش. لایهی pip install در نسخهی اول را پیدا کن و با نسخهی دوم مقایسه کن:
for t in lx-sz-slim lx-sz-slim-nocache; do echo "$t: $(docker history $t --format '{{.Size}} {{.CreatedBy}}' | grep 'pip install' | awk '{print $1}')"donelx-sz-slim: 16.9MBlx-sz-slim-nocache: 15.5MBلایهی pip نسخهی بدون کش کوچکتر است. docker history اولین ابزار تشخیص است: بزرگترین لایه را پیدا کن و بپرس «این چرا اینقدر بزرگ است؟». (ابزار گرافیکی dive هم همین را تعاملی نشان میدهد.)
مثال ۴: حذف در لایهی بعد، حجم را کم نمیکند
Section titled “مثال ۴: حذف در لایهی بعد، حجم را کم نمیکند”یک base سنگینتر (Ubuntu) با نصب یک بسته با apt. نسخهی بد: سه RUN جدا و پاک کردن در لایهی آخر:
FROM ubuntu:24.04RUN apt-get updateRUN apt-get install -y -o Acquire::Retries=5 curlRUN rm -rf /var/lib/apt/lists/*نسخهی درست: یک RUN، بدون بستههای پیشنهادی (--no-install-recommends)، و پاککردن کش در همان لایه:
FROM ubuntu:24.04RUN apt-get update \ && apt-get install -y --no-install-recommends -o Acquire::Retries=5 curl ca-certificates \ && rm -rf /var/lib/apt/lists/*cd szdocker build -q -f apt1.Dockerfile -t lx-sz-apt1 . >/dev/nulldocker build -q -f apt2.Dockerfile -t lx-sz-apt2 . >/dev/nulldocker image ls --format '{{.Repository}} {{.Size}}' | grep -E '^lx-sz-apt' | sort | column -techo "--- لایههای apt1 (rm در لایهی جدا):"docker history lx-sz-apt1 --format '{{.Size}}\t{{.CreatedBy}}' | grep -E 'apt-get|rm -rf' | cut -c1-80lx-sz-apt1 260MBlx-sz-apt2 161MB--- لایههای apt1 (rm در لایهی جدا):20.5kB RUN /bin/sh -c rm -rf /var/lib/apt/lists/* #…17.5MB RUN /bin/sh -c apt-get install -y -o Acquire…57.4MB RUN /bin/sh -c apt-get update # buildkitدر نسخهی اول لایهی rm -rf تقریباً صفر بایت است؛ فایلهای پاکشده هنوز در لایهی apt-get update هستند و حجم کم نشد. در نسخهی دوم فایلها هیچوقت در یک لایه ثبت نمیشوند (ساخت و پاککردن در یک لایه)، و --no-install-recommends بستههای اضافی را هم نیاورد. (این همان اصل «لایه فقط جمع میشود» است.)
مثال ۵: multi-stage و scratch
Section titled “مثال ۵: multi-stage و scratch”برنامهای که باینری استاتیک میسازد (مثل C یا Go) در مرحلهی build به کامپایلر نیاز دارد، ولی برای اجرا هیچ چیز لازم نیست. یک برنامهی C:
#include <stdio.h>
int main(void) { puts("hello from a tiny image"); return 0;}نسخهی بدون multi-stage (کامپایلر در image میماند):
FROM alpineRUN apk add --no-cache build-baseCOPY hello.c .RUN gcc -static -o /hello hello.cCMD ["/hello"]نسخهی multi-stage با image نهایی scratch (یعنی کاملاً خالی، فقط باینری):
FROM alpine AS buildRUN apk add --no-cache build-baseCOPY hello.c .RUN gcc -static -o /hello hello.c
FROM scratchCOPY --from=build /hello /helloCMD ["/hello"]cd sz/cdocker build -q -f full.Dockerfile -t lx-sz-c-full . >/dev/nulldocker build -q -f scratch.Dockerfile -t lx-sz-c-scratch . >/dev/nulldocker image ls --format '{{.Repository}} {{.Size}}' | grep -E '^lx-sz-c-' | sort | column -tdocker run --rm lx-sz-c-scratchlx-sz-c-full 359MBlx-sz-c-scratch 184kBhello from a tiny imageنسخهی کامل با کامپایلر چند صد مگابایت است؛ نسخهی scratch فقط حدود ۱۸۰ کیلوبایت: همان برنامه، همان خروجی. نسخهی scratch هیچ شل، ls یا مدیر بسته ندارد؛ برای مهاجم چیزی نمیماند (و برای دیباگ هم سختتر است: docker exec ... sh کار نمیکند).
docker run --rm --entrypoint sh lx-sz-c-scratch -c 'echo hi' 2>&1 | grep -o 'exec: .*'exec: "sh": executable file not found in $PATHمثال ۶: distroless: حد وسط (نمونه)
Section titled “مثال ۶: distroless: حد وسط (نمونه)”distroless imageهایی از گوگل هستند که فقط runtime زبان را دارند (بدون شل و مدیر بسته): امن و کوچک، ولی برای زبانهایی که به کتابخانههای سیستمی (glibc، گواهیهای CA) نیاز دارند آسانتر از scratch است. استفادهی معمول:
FROM python:3.12-slim AS buildWORKDIR /appCOPY requirements.txt .RUN pip install --no-cache-dir --target /deps -r requirements.txt
FROM gcr.io/distroless/python3-debian12COPY --from=build /deps /depsCOPY app.py /app/app.pyENV PYTHONPATH=/depsCMD ["/app/app.py"]این مثال اجرا نشده؛ دلیلش این است که registry آن (gcr.io) روی شبکهی من 403 Forbidden میداد (درس «محدودیت Docker Hub» را ببین). اگر به آن دسترسی داری امتحانش کن؛ اگر نه، slim یا alpine همراه کاربر غیر root انتخابهای خوبی هستند.
پشت پرده
Section titled “پشت پرده”image مجموعهای از لایههای فقط-افزایشی (union filesystem) است. هر دستور RUN، COPY و ADD یک لایهی جدید میسازد که تفاوت نسبت به لایهی قبل را نگه میدارد. پاک کردن فایل در لایهی بعدی یک «نشانهی حذف» (whiteout) میگذارد که فایل را از دید کانتینر پنهان میکند، ولی بایتهای فایل هنوز در لایهی اصلیاش میمانند. به همین دلیل مثال ۴ حجم را کم نکرد.
نتیجهی عملی: ۱) ساخت و پاککردن در یک RUN؛ ۲) ابزار ساخت را در مرحلهی جدا (multi-stage) نگه دار و فقط نتیجه را COPY --from کن؛ ۳) base را کوچک انتخاب کن؛ ۴) فقط فایلهای لازم را کپی کن (.dockerignore).
یک نکتهی تکمیلی: حجمی که docker images نشان میدهد حجم باز-شده (فضای دیسک) است، و حجم دانلود از registry معمولاً کوچکتر است چون لایهها فشرده منتقل میشوند. و لایههای مشترک بین image ها فقط یک بار روی دیسک مینشینند.
جدولهای مرجع
Section titled “جدولهای مرجع”| ترفند | چه کار میکند | هزینه |
|---|---|---|
base کوچکتر (slim، alpine) |
مهمترین کاهش | سازگاری (musl) و دیباگ |
pip install --no-cache-dir |
کش pip در image نمیماند | ندارد |
apt-get install --no-install-recommends |
بستههای پیشنهادی نمیآیند | ممکن است بستهای کم بیاید |
پاککردن کش در همان RUN |
rm -rf /var/lib/apt/lists/* |
ندارد |
| multi-stage | ابزار ساخت داخل image نهایی نمیماند | چند مرحله در Dockerfile |
scratch |
خالی، فقط باینری استاتیک | بدون شل؛ فقط برنامههای استاتیک |
.dockerignore |
فایل اضافی (.git، node_modules) کپی نمیشود |
ندارد |
| دستور | کار |
|---|---|
docker images / docker image ls |
حجم image ها |
docker history IMG |
حجم و دستور هر لایه |
docker image inspect IMG --format '{{.Size}}' |
حجم دقیق به بایت |
docker system df |
مصرف دیسک داکر (image، کانتینر، volume، کش build) |
اشتباهات رایج
Section titled “اشتباهات رایج”۱) حذف فایل در لایهی جدا
Section titled “۱) حذف فایل در لایهی جدا”مثال ۴: rm در لایهی بعد حجم را کم نکرد. راهحل: ساخت و حذف در یک RUN.
۲) base کامل برای اجرا
Section titled “۲) base کامل برای اجرا”FROM python:3.12 (حدود ۱ گیگابایت) فقط برای اجرای یک اپ ساده. راهحل: slim یا alpine، یا multi-stage.
۳) رفتن روی alpine بدون تست
Section titled “۳) رفتن روی alpine بدون تست”mkdir -p m3 && printf 'FROM alpine\nRUN apk add --no-cache bash >/dev/null\nCMD ["/bin/bash","-c","echo ok"]\n' > m3/Dockerfiledocker build -q -t lx-sz-m3 m3 >/dev/null && docker run --rm lx-sz-m3docker run --rm --entrypoint /bin/bash alpine -c 'echo hi' 2>&1 | grep -o 'exec: .*'docker rmi lx-sz-m3 >/dev/nullokexec: "/bin/bash": stat /bin/bash: no such file or directoryalpine بهطور پیشفرض bash ندارد (فقط sh از busybox)؛ آخرین خط بالا خطای اجرای /bin/bash در image خالی alpine است. اسکریپتی که #!/bin/bash دارد روی آن خطا میدهد. راهحل: apk add bash یا اسکریپت را با sh سازگار کن؛ و همیشه بعد از تغییر base تست کن.
۴) ابزار ساخت در image نهایی
Section titled “۴) ابزار ساخت در image نهایی”اگر gcc و build-essential در image اجرایی بمانند هم حجم بیشتر میشود هم سطح حمله. راهحل: multi-stage (مثال ۵).
۵) فراموش کردن .dockerignore
Section titled “۵) فراموش کردن .dockerignore”COPY . . همهی پوشهی پروژه (از جمله .git و node_modules محلی) را وارد image میکند. راهحل: .dockerignore (درس مربوطه).
حجم lx-sz-slim را با docker image inspect --format بر حسب بایت بگیر و با docker image ls مقایسه کن.
دیدن جواب
docker image inspect lx-sz-slim --format 'بایت: {{.Size}}'docker image ls lx-sz-slim --format 'نمایش docker image ls: {{.Size}}'بایت: 236961480نمایش docker image ls: 237MBتمرین اصلی: حجم یک image را حداقل نصف کن. اپ پایتونی با FROM ubuntu:24.04 و نصب python3 با apt (بدون ترفند) را بساز، سپس نسخهی بهینهی آن را با python:3.12-alpine بنویس و نسبت حجمها را چاپ کن.
دیدن جواب
mkdir -p e2 && cd e2printf 'print("ok")\n' > main.pycat > before.Dockerfile <<'LXEOF'FROM ubuntu:24.04RUN apt-get updateRUN apt-get install -y -o Acquire::Retries=5 python3COPY main.py /main.pyCMD ["python3", "/main.py"]LXEOFcat > after.Dockerfile <<'LXEOF'FROM python:3.12-alpineCOPY main.py /main.pyCMD ["python", "/main.py"]LXEOFdocker build -q -f before.Dockerfile -t lx-sz-before . >/dev/nulldocker build -q -f after.Dockerfile -t lx-sz-after . >/dev/nullb=$(docker image inspect lx-sz-before --format '{{.Size}}')a=$(docker image inspect lx-sz-after --format '{{.Size}}')echo "قبل: $((b/1000000)) مگابایت — بعد: $((a/1000000)) مگابایت"echo "نسبت بعد/قبل: $((a*100/b))%"[ $((a*2)) -le $b ] && echo "حجم حداقل نصف شد"docker run --rm lx-sz-afterdocker rmi lx-sz-before lx-sz-after >/dev/nullقبل: 306 مگابایت — بعد: 87 مگابایتنسبت بعد/قبل: 28%حجم حداقل نصف شدokبا multi-stage، برنامهی C مثال ۵ را طوری بساز که image نهایی کمتر از ۱ مگابایت باشد و ثابت کن (۱) اجرا میشود، (۲) شل ندارد، (۳) تعداد لایههایش یک است.
دیدن جواب
docker image inspect lx-sz-c-scratch --format 'حجم: {{.Size}} بایت'[ "$(docker image inspect lx-sz-c-scratch --format '{{.Size}}')" -lt 1000000 ] && echo "کمتر از ۱ مگابایت"docker run --rm lx-sz-c-scratchdocker run --rm --entrypoint /bin/sh lx-sz-c-scratch 2>&1 | grep -o 'exec: .*'echo "تعداد لایهها: $(docker image inspect lx-sz-c-scratch --format '{{len .RootFS.Layers}}')"حجم: 181776 بایتکمتر از ۱ مگابایتhello from a tiny imageexec: "/bin/sh": stat /bin/sh: no such file or directoryتعداد لایهها: 1آزمونک
Section titled “آزمونک”چرا rm -rf در یک RUN جدا حجم image را کم نمیکند؟
ساخت و پاککردن را در یک RUN انجام بده.
کدام ابزار حجم هر لایه را نشان میدهد؟
بزرگترین لایه را پیدا کن.
multi-stage چه کمکی میکند؟
COPY --from فقط نتیجه را میبرد.
scratch برای چه چیزی مناسب است؟
scratch خالی است: بدون شل و کتابخانه.
ریسک رایج استفاده از alpine؟
بعد از تغییر base حتماً تست کن.
pip install --no-cache-dir چه میکند؟
چند ده مگابایت کمتر.
جمعبندی
Section titled “جمعبندی”- اندازهگیری:
docker imagesبرای کل،docker historyبرای لایهها. - بزرگترین اهرم: base کوچک (
slim،alpine)، بعد multi-stage وscratch. - لایهها فقط جمع میشوند: ساخت و پاککردن در یک
RUN؛--no-cache-dirو--no-install-recommends. - ابزار ساخت را از image نهایی دور نگه دار؛ فقط artefact را
COPY --fromکن. - بعد از هر تغییر base برنامه را تست کن؛ کوچکتر همیشه یعنی «تحملناپذیرتر برای ابزار و دیباگ».
| دستور | کاری که میکند |
|---|---|
docker image ls | فهرست و حجم image ها |
docker history IMG | حجم و دستور هر لایه |
docker image inspect IMG --format '{{.Size}}' | حجم دقیق (بایت) |
FROM python:3.12-slim | base متوسط و سازگار |
FROM python:3.12-alpine | base کوچک (musl) |
pip install --no-cache-dir -r requirements.txt | بدون کش pip |
apt-get install -y --no-install-recommends X && rm -rf /var/lib/apt/lists/* | نصب و پاککردن در یک لایه |
COPY --from=build /out /out | برداشتن فقط نتیجه از مرحلهی build |
FROM scratch | image خالی برای باینری استاتیک |