توی این درس یاد میگیری هرچه در دوره دیدی را در یک چکلیست جمع کنی که برای هر پروژهی داکر قابلاستفاده است: چهار گروه Dockerfile، Compose، امنیت و عملکرد. فقط فهرست نمیخوانی: چکلیست را با ابزار خودکار میسنجی، یعنی Dockerfile را با hadolint، فایل Compose را با یک چککنندهی سادهی خودت، و اثر ترتیب COPY و .dockerignore را با اندازهگیری واقعی ثابت میکنی. در تمرین اصلی پروژهی خودت را با چکلیست بررسی میکنی.
مسئله: از «کار میکند» تا «درست است»
Section titled “مسئله: از «کار میکند» تا «درست است»”بیشتر Dockerfile ها و فایلهای Compose «کار میکنند» ولی چیزهایی دارند که بعدها درد میشوند: latest، اجرا با root، بدون healthcheck، دیتابیسی که روی اینترنت باز است، یا build ای که با هر تغییر یک خط کد ۵ دقیقه طول میکشد. هر کدام را جدا یاد گرفتی؛ حالا یک فهرست واحد لازم است که سر هر پروژه نگاهش کنی و ابزارهایی که تکرارش را خودکار کنند.
تشبیه: چکلیست خلبان
Section titled “تشبیه: چکلیست خلبان”خلبانهای باتجربه هم قبل از پرواز چکلیست را مرور میکنند؛ نه چون بلد نیستند، چون در تکرار، آدمها چیزهای ساده را فراموش میکنند. چکلیست حافظهی تیم است. و چیزی که خودکار بررسی شود، هیچوقت فراموش نمیشود.
مثالهای عملی
Section titled “مثالهای عملی”مثال ۱: بررسی خودکار Dockerfile با hadolint
Section titled “مثال ۱: بررسی خودکار Dockerfile با hadolint”hadolint یک linter برای Dockerfile است (مثل ESLint برای JS) و بر اساس قواعدی که در این دوره دیدی هشدار میدهد. یک Dockerfile پر از عادت بد:
FROM python:latestRUN apt-get updateRUN apt-get install -y curlADD . /appWORKDIR /appRUN pip install flaskCMD python app.pydocker run --rm -i hadolint/hadolint hadolint --no-color - < bp/bad/Dockerfile 2>&1 | sed -E 's/^-:/خط /' | cut -c1-135خط 1 DL3007 warning: Using latest is prone to errors if the image will ever update. Pin the version explicitly to a release tagخط 2 DL3009 info: Delete the apt lists (/var/lib/apt/lists) after installing somethingخط 3 DL3059 info: Multiple consecutive `RUN` instructions. Consider consolidation.خط 3 DL3008 warning: Pin versions in apt get install. Instead of `apt-get install <package>` use `apt-get install <package>=<version>خط 3 DL3015 info: Avoid additional packages by specifying `--no-install-recommends`خط 4 DL3020 error: Use COPY instead of ADD for files and foldersخط 6 DL3013 warning: Pin versions in pip. Instead of `pip install <package>` use `pip install <package>==<version>` or `pip install -خط 6 DL3042 warning: Avoid use of cache directory with pip. Use `pip install --no-cache-dir <package>`خط 7 DL3025 warning: Use arguments JSON notation for CMD and ENTRYPOINT argumentsهر خط یک قاعده است با کد (DL3007…) و شدت (error، warning، info): latest استفاده نکن، لایهها را ترکیب کن، کش apt را پاک کن، COPY بهتر از ADD است، نسخهی بستهها را ثابت کن، --no-cache-dir، و CMD را به شکل JSON (exec form) بنویس. نسخهی اصلاحشده:
FROM python:3.12-slimWORKDIR /appCOPY requirements.txt .RUN pip install --no-cache-dir -r requirements.txtCOPY . .RUN useradd -r -u 10001 appUSER 10001CMD ["python", "app.py"]docker run --rm -i hadolint/hadolint hadolint --no-color - < bp/good/Dockerfile 2>&1 | sed -E 's/^-:/خط /'echo "کد خروج hadolint: $?"کد خروج hadolint: 0بدون هیچ هشدار (برای USER از uid عددی استفاده کردم؛ hadolint اسم کاربر را «ممکن است روی میزبان قابلحل نباشد» میداند). در CI معمولاً hadolint را طوری میگذارند که فقط روی error (یا warning) شکست بخورد (--failure-threshold warning). برای رد کردن یک قاعدهی آگاهانه، کامنت # hadolint ignore=DL3008 بالای همان خط.
چرا CMD ['python','app.py'] (فرم JSON) بهتر از CMD python app.py است؟
در فرم shell سیگنال به برنامه نمیرسد و docker stop بعد از مهلت KILL میزند.
مثال ۲: یک چککنندهی ساده برای Compose
Section titled “مثال ۲: یک چککنندهی ساده برای Compose”برای Compose ابزار استانداردِ همهجا نیست، اما docker compose config فایل را به شکل یکسان (JSON) بیرون میدهد و چند خط پایتون چکلیست ما را اجرا میکند:
import jsonimport subprocessimport sys
cfg = json.loads(subprocess.check_output( ["docker", "compose", "-f", sys.argv[1], "config", "--format", "json"], text=True))services = cfg.get("services", {})fails = 0
def check(ok, text): global fails print(("PASS " if ok else "FAIL ") + text) fails += 0 if ok else 1
for name, s in services.items(): image = s.get("image", "") if image: tag = image.split(":")[-1] if ":" in image.rsplit("/", 1)[-1] else "" check(tag not in ("", "latest"), f"{name}: tag دقیق ({image or 'بدون image'})") check("healthcheck" in s, f"{name}: healthcheck تعریف شده") check("restart" in s, f"{name}: restart policy دارد") check("mem_limit" in s or "deploy" in s, f"{name}: سقف حافظه دارد") check(not s.get("privileged"), f"{name}: privileged نیست") vols = [v.get("source", "") for v in s.get("volumes", []) if isinstance(v, dict)] check(not any("docker.sock" in v for v in vols), f"{name}: docker.sock را mount نکرده") if "db" in name or "redis" in name or "postgres" in name: check(not s.get("ports"), f"{name}: پورت دیتابیس به بیرون باز نیست")sys.exit(1 if fails else 0)یک Compose با چند اشتباه و یکی درست:
name: lxbpbadservices: web: image: nginx ports: ["8365:80"] db: image: redis:latest ports: ["6390:6379"] privileged: truename: lxbpgoodservices: web: image: nginx:1.27-alpine ports: ["8366:80"] restart: unless-stopped mem_limit: 128m healthcheck: test: ["CMD", "wget", "-qO-", "http://localhost/"] db: image: redis:7.4-alpine restart: unless-stopped mem_limit: 256m healthcheck: test: ["CMD", "redis-cli", "ping"]cd bpecho "=== bad:"; python3 check_compose.py bad.compose.yaml; echo "کد خروج: $?"echo "=== good:"; python3 check_compose.py good.compose.yaml; echo "کد خروج: $?"=== bad:FAIL db: tag دقیق (redis:latest)FAIL db: healthcheck تعریف شدهFAIL db: restart policy داردFAIL db: سقف حافظه داردFAIL db: privileged نیستPASS db: docker.sock را mount نکردهFAIL db: پورت دیتابیس به بیرون باز نیستFAIL web: tag دقیق (nginx)FAIL web: healthcheck تعریف شدهFAIL web: restart policy داردFAIL web: سقف حافظه داردPASS web: privileged نیستPASS web: docker.sock را mount نکردهکد خروج: 1=== good:PASS db: tag دقیق (redis:7.4-alpine)PASS db: healthcheck تعریف شدهPASS db: restart policy داردPASS db: سقف حافظه داردPASS db: privileged نیستPASS db: docker.sock را mount نکردهPASS db: پورت دیتابیس به بیرون باز نیستPASS web: tag دقیق (nginx:1.27-alpine)PASS web: healthcheck تعریف شدهPASS web: restart policy داردPASS web: سقف حافظه داردPASS web: privileged نیستPASS web: docker.sock را mount نکردهکد خروج: 0نسخهی بد چندین FAIL گرفت (tag، healthcheck، restart، سقف حافظه، privileged، پورت دیتابیس)؛ نسخهی خوب همه PASS. کد خروج غیرصفر برای CI. این اسکریپت آموزشی است؛ قواعد را به سلیقهی تیم خودت اضافه کن. (برای Compose میتوانی trivy config و docker compose config --quiet را هم به CI بدهی.)
مثال ۳: ترتیب COPY و کش build، اندازهگیری واقعی
Section titled “مثال ۳: ترتیب COPY و کش build، اندازهگیری واقعی”دو Dockerfile که هر دو یک اپ را میسازند؛ یکی کل کد را اول کپی میکند، دیگری وابستگیها را اول:
print("hello")flask==3.0.3FROM python:3.12-slimWORKDIR /appCOPY app/ .RUN pip install --no-cache-dir -r requirements.txtFROM python:3.12-slimWORKDIR /appCOPY app/requirements.txt .RUN pip install --no-cache-dir -r requirements.txtCOPY app/app.py .هر کدام را میسازیم، کد را یک خط عوض میکنیم و دوباره میسازیم؛ میشماریم چند مرحله از کش آمد (CACHED):
cd bp/cachefor f in slow fast; do docker build -q -f $f.Dockerfile -t lx-bp-$f . >/dev/null 2>&1 echo "print('changed $f')" > app/app.py out=$(docker build --progress=plain -f $f.Dockerfile -t lx-bp-$f . 2>&1) id=$(echo "$out" | grep 'RUN pip install' | head -1 | grep -oE '^#[0-9]+') if echo "$out" | grep -q "^$id CACHED"; then pip="از کش آمد"; else pip="دوباره اجرا شد"; fi echo "$f: pip install $pip (مرحلههای CACHED: $(echo "$out" | grep -c ' CACHED'))" echo "print('hello')" > app/app.pydonedocker rmi lx-bp-slow lx-bp-fast >/dev/null 2>&1slow: pip install دوباره اجرا شد (مرحلههای CACHED: 2)fast: pip install از کش آمد (مرحلههای CACHED: 4)در slow با تغییر کد، لایهی COPY app/ باطل شد و pip install دوباره اجرا شد (دقیقهها در پروژهی واقعی). در fast فقط آخرین COPY دوباره انجام شد و pip از کش آمد. این اصل «کمتغییر پایین، پرتغییر بالا» است و بزرگترین صرفهجویی زمانی عمر روزمرهی تو.
مثال ۴: .dockerignore و اندازهی context
Section titled “مثال ۴: .dockerignore و اندازهی context”mkdir -p bp/ctx && cd bp/ctxprintf 'FROM alpine\nCOPY . /ctx\n' > Dockerfileecho hi > note.txthead -c 30000000 /dev/zero > junk.binfor v in 1 2; do if [ $v = 2 ]; then printf 'junk.bin\n' > .dockerignore; label="با .dockerignore "; else label="بدون .dockerignore"; fi size=$(docker build --no-cache --progress=plain -t lx-bp-ctx$v . 2>&1 | grep -oE 'transferring context: [0-9.]+[kMG]?B' | tail -1) echo "$label: $size"donedocker rmi lx-bp-ctx1 lx-bp-ctx2 >/dev/nullبدون .dockerignore: transferring context: 30.01MBبا .dockerignore : transferring context: 105Bفایل ۳۰ مگابایتی که اصلاً در image لازم نبود، بدون .dockerignore به builder فرستاده شد. با .dockerignore فقط چند بایت. (در Dockerfile بالا COPY . /ctx همهی پوشه را میخواهد. BuildKit اگر Dockerfile فقط چند فایل مشخص را COPY کند، فقط همانها را میفرستد؛ ولی COPY . . رایج است و در پروژههای واقعی با node_modules یا .git تفاوت چند ده تا چند صد مگابایت میشود.)
مثال ۵: base را با digest ثابت کن
Section titled “مثال ۵: base را با digest ثابت کن”D=$(docker image inspect python:3.12-slim --format '{{index .RepoDigests 0}}')mkdir -p bp/pin && printf "FROM $D\nCMD [\"python\", \"--version\"]\n" > bp/pin/Dockerfilehead -1 bp/pin/Dockerfile | sed -E 's/(sha256:[0-9a-f]{12})[0-9a-f]+/\1…/'docker build -q -t lx-bp-pin bp/pin >/dev/null && docker run --rm lx-bp-pindocker rmi lx-bp-pin >/dev/nullFROM python@sha256:dddfd7e07f9d…Python 3.12.15با FROM image@sha256:… همیشه دقیقاً همان base ساخته میشود، حتی اگر tag فردا چیز دیگری شود (تکرارپذیری و امنیت زنجیرهی تأمین). بهای آن: باید digest را بهروز کنی (ابزارهایی مثل Renovate/Dependabot این کار را خودکار میکنند) تا بهروزرسانی امنیتی را از دست ندهی.
چکلیست نهایی
Section titled “چکلیست نهایی”این چکلیست را برای پروژهات تیک بزن (تیکها فقط در همین مرورگر میمانند):
پشت پرده
Section titled “پشت پرده”هر آیتم چکلیست جواب یک حالت خرابی واقعی است:
- latest / بدون tag: دو build در دو زمان، دو image متفاوت و رفتار غیرقابلردیابی.
- root: هر باگ در برنامه، دست مهاجم را با اختیار root کانتینر باز میکند.
- بدون healthcheck/restart: خرابی ساکت؛ کسی نمیفهمد تا مشتری شکایت کند.
- پورت دیتابیس باز: رباتها ظرف دقایق پیدایش میکنند.
- بدون سقف لاگ/حافظه: یک کانتینر کل میزبان را از کار میاندازد.
- بکاپ آزمایشنشده: وقتی لازم شود کار نمیکند.
اتوماسیون مهمتر از حفظ کردن است: چیزی که در CI بررسی شود (hadolint، Trivy، چککنندهی Compose)، سلیقهی افراد نیست، قانون تیم است. و چکلیست باید با تجربهی تیم زنده بماند: هر حادثهای که رخ داد، یک خط به آن اضافه کن.
جدولهای مرجع
Section titled “جدولهای مرجع”| ابزار | چه چیزی را میسنجد | دستور |
|---|---|---|
| hadolint | قواعد Dockerfile | docker run --rm -i hadolint/hadolint < Dockerfile |
docker compose config |
معتبر بودن Compose و مقادیر نهایی | docker compose config --quiet |
| Trivy config | اشتباهات امنیتی Dockerfile/Compose | trivy config DIR |
| Trivy image | آسیبپذیریهای image | trivy image --input x.tar |
docker history |
لایهها و حجم | docker history IMG |
docker build --progress=plain |
کش و اندازهی context | خطهای CACHED و transferring context |
| اشتباه | اثر | جایگزین |
|---|---|---|
FROM x:latest |
غیرقابلتکرار | tag دقیق یا digest |
ADD |
رفتار پنهان (tar، URL) | COPY |
CMD python app.py |
سیگنال نمیرسد | CMD ["python","app.py"] |
COPY . . قبل از install |
کش باطل | requirements اول |
ENV PASSWORD= |
لو رفتن | --secret / env زمان اجرا |
ports: 5432:5432 |
دیتابیس باز | بدون ports، شبکهی داخلی |
اشتباهات رایج
Section titled “اشتباهات رایج”۱) فقط یک بار چک کردن
Section titled “۱) فقط یک بار چک کردن”چکلیستی که فقط روز اول اجرا شود، با تغییر کد از بین میرود. راهحل: ابزار خودکار در CI.
۲) نادیده گرفتن همهی هشدارها
Section titled “۲) نادیده گرفتن همهی هشدارها”docker run --rm -i hadolint/hadolint hadolint --no-color --ignore DL3007 --ignore DL3009 --ignore DL3059 --ignore DL3008 --ignore DL3015 --ignore DL3020 --ignore DL3013 --ignore DL3042 --ignore DL3025 - < bp/bad/Dockerfile 2>&1 | wc -l | tr -d ' ' | sed 's/^/هشدارهای باقیمانده پس از ignore همه: /'هشدارهای باقیمانده پس از ignore همه: 0با ignore کردن همهی قاعدهها، لینتر ساکت میشود ولی مشکلها برقرارند. راهحل: فقط استثنای آگاهانه با دلیل (کامنت) و محدود به همان خط.
۳) قاعدهی بدون دلیل
Section titled “۳) قاعدهی بدون دلیل”اگر تیم نداند چرا USER لازم است، با اولین خطا آن را حذف میکند. راهحل: هر قاعده کنارش یک جمله دلیل (مثل «پشت پرده» همین درس).
۴) بزرگنمایی کوچک کردن
Section titled “۴) بزرگنمایی کوچک کردن”printf 'FROM scratch\nCOPY note.txt /note.txt\n' > bp/ctx/scratch.Dockerfiledocker build -q -f bp/ctx/scratch.Dockerfile -t lx-bp-s bp/ctx >/dev/null 2>&1 && docker run --rm lx-bp-s cat /note.txt 2>&1 | head -1 | grep -o 'exec: .*' || echo "image ساخته شد ولی چیزی برای اجرا ندارد"docker rmi lx-bp-s >/dev/null 2>&1exec: "cat": executable file not found in $PATHscratch کوچکترین است ولی چیزی برای اجرا (شل، cat) ندارد و دیباگ را سخت میکند. راهحل: کوچک بودن را با قابلیت نگهداری بسنج.
۵) بررسی فقط image، نه تنظیم اجرا
Section titled “۵) بررسی فقط image، نه تنظیم اجرا”Dockerfile کامل است ولی docker run --privileged یا docker.sock در Compose همهچیز را میشکند. راهحل: چکلیست Compose/اجرا را جدا و خودکار بسنج (مثال ۲).
hadolint را روی یک Dockerfile یکخطی FROM ubuntu اجرا کن و بگو کدام قاعده میگیرد.
دیدن جواب
printf 'FROM ubuntu\n' | docker run --rm -i hadolint/hadolint hadolint --no-color - 2>&1 | sed -E 's/^-:/خط /'خط 1 DL3006 warning: Always tag the version of an image explicitlyتمرین اصلی: پروژهی خودت را با چکلیست بررسی کن. اینجا «پروژهی تو» یک Dockerfile و یک Compose کوچک است: Dockerfile را با hadolint و Compose را با check_compose.py بسنج، و هر FAIL یا هشدار را رفع کن تا هر دو تمیز شوند.
دیدن جواب
mkdir -p mine && cd mineprintf 'FROM python:3.12\nCOPY . .\nCMD python main.py\n' > Dockerfileprintf 'print("ok")\n' > main.pyprintf 'name: lxmine\nservices:\n app:\n image: nginx\n' > compose.yamlecho "--- قبل:"docker run --rm -i hadolint/hadolint hadolint --no-color - < Dockerfile 2>&1 | sed -E 's/^-:/خط /' | cut -c1-110python3 ../bp/check_compose.py compose.yaml | grep FAIL | head -4echo "--- بعد از رفع:"cat > Dockerfile <<'LXEOF'FROM python:3.12-slimWORKDIR /appCOPY main.py .RUN useradd -r -u 10001 appUSER 10001CMD ["python", "main.py"]LXEOFcat > compose.yaml <<'LXEOF'name: lxmineservices: app: image: nginx:1.27-alpine restart: unless-stopped mem_limit: 128m healthcheck: test: ["CMD", "wget", "-qO-", "http://localhost/"]LXEOFdocker run --rm -i hadolint/hadolint hadolint --no-color - < Dockerfile && echo "hadolint: تمیز"python3 ../bp/check_compose.py compose.yaml | grep -c FAIL | sed 's/^/تعداد FAIL در Compose: /'--- قبل:خط 2 DL3045 warning: `COPY` to a relative destination without `WORKDIR` set.خط 3 DL3025 warning: Use arguments JSON notation for CMD and ENTRYPOINT argumentsFAIL app: tag دقیق (nginx)FAIL app: healthcheck تعریف شدهFAIL app: restart policy داردFAIL app: سقف حافظه دارد--- بعد از رفع:hadolint: تمیزتعداد FAIL در Compose: 0یک اسکریپت gate.sh بنویس که هر دو بررسی را اجرا کند (hadolint روی Dockerfile و check_compose.py روی Compose)، هر شکست را بشمارد و اگر مجموع شکستها صفر نبود با کد ۱ تمام شود؛ روی پروژهی bad (باید کد ۱) و good (باید ۰) تستش کن.
دیدن جواب
cat > gate.sh <<'LXEOF'#!/bin/sh# استفاده: gate.sh DOCKERFILE COMPOSEfail=0docker run --rm -i hadolint/hadolint hadolint --no-color --failure-threshold warning - < "$1" >/dev/null 2>&1 || { echo "FAIL hadolint ($1)"; fail=1; }python3 bp/check_compose.py "$2" >/dev/null 2>&1 || { echo "FAIL compose ($2)"; fail=1; }[ $fail -eq 0 ] && echo "PASS همهی بررسیها"exit $failLXEOFsh gate.sh bp/bad/Dockerfile bp/bad.compose.yaml; echo "کد خروج bad: $?"sh gate.sh bp/good/Dockerfile bp/good.compose.yaml; echo "کد خروج good: $?"FAIL hadolint (bp/bad/Dockerfile)FAIL compose (bp/bad.compose.yaml)کد خروج bad: 1PASS همهی بررسیهاکد خروج good: 0آزمونک
Section titled “آزمونک”چرا در Dockerfile ابتدا requirements و بعد کد را COPY میکنیم؟
کمتغییر پایین، پرتغییر بالا.
hadolint چه کاری میکند؟
مثل ESLint برای Dockerfile.
کدام یک اشتباه Compose است؟
دیتابیس فقط روی شبکهی داخلی.
FROM image@sha256:… چه مزیتی دارد؟
بهایش: بهروزرسانی دستی/خودکار digest.
اثر .dockerignore بر build؟
فرستادن .env و .git به build خطرناک هم هست.
مهمترین چیز دربارهی چکلیست؟
چیزی که خودکار بررسی شود فراموش نمیشود.
جمعبندی
Section titled “جمعبندی”- چهار گروه چکلیست: Dockerfile (base دقیق و کوچک، ترتیب لایه، multi-stage، exec form)، Compose (tag دقیق، healthcheck، شبکهی داخلی، volume، restart، سقف منابع)، امنیت (غیر root، رازها بیرون، حداقل دسترسی، اسکن)، عملکرد (کش، اندازه،
.dockerignore، لاگ). - ابزار خودکار: hadolint برای Dockerfile، یک چککننده برای Compose، Trivy برای image و config؛ همه در CI.
- اثبات با اندازهگیری: ترتیب
COPYتعداد مراحل کششده را عوض میکند و.dockerignoreحجم context را. - هر قاعده با دلیل بنویس؛ استثنا فقط آگاهانه و محدود.
- چکلیست با هر حادثه یک خط بیشتر شود.
| دستور | کاری که میکند |
|---|---|
docker run --rm -i hadolint/hadolint < Dockerfile | لینت Dockerfile |
hadolint --failure-threshold warning - | شکست فقط از warning به بالا |
# hadolint ignore=DL3008 | استثنای آگاهانه برای خط بعد |
docker compose -f f.yaml config --quiet | اعتبارسنجی Compose |
docker compose config --format json | خروجی ماشینخوان برای چککننده |
trivy config DIR | اسکن اشتباهات امنیتی Dockerfile/Compose |
docker build --progress=plain … | دیدن CACHED و transferring context |
FROM python:3.12-slim@sha256:… | ثابت کردن base با digest |