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

بهترین شیوه‌ها و چک‌لیست نهایی

توی این درس یاد می‌گیری هرچه در دوره دیدی را در یک چک‌لیست جمع کنی که برای هر پروژه‌ی داکر قابل‌استفاده است: چهار گروه Dockerfile، Compose، امنیت و عملکرد. فقط فهرست نمی‌خوانی: چک‌لیست را با ابزار خودکار می‌سنجی، یعنی Dockerfile را با hadolint، فایل Compose را با یک چک‌کننده‌ی ساده‌ی خودت، و اثر ترتیب COPY و .dockerignore را با اندازه‌گیری واقعی ثابت می‌کنی. در تمرین اصلی پروژه‌ی خودت را با چک‌لیست بررسی می‌کنی.

مسئله: از «کار می‌کند» تا «درست است»

Section titled “مسئله: از «کار می‌کند» تا «درست است»”

بیشتر Dockerfile ها و فایل‌های Compose «کار می‌کنند» ولی چیزهایی دارند که بعدها درد می‌شوند: latest، اجرا با root، بدون healthcheck، دیتابیسی که روی اینترنت باز است، یا build ای که با هر تغییر یک خط کد ۵ دقیقه طول می‌کشد. هر کدام را جدا یاد گرفتی؛ حالا یک فهرست واحد لازم است که سر هر پروژه نگاهش کنی و ابزارهایی که تکرارش را خودکار کنند.

خلبان‌های باتجربه هم قبل از پرواز چک‌لیست را مرور می‌کنند؛ نه چون بلد نیستند، چون در تکرار، آدم‌ها چیزهای ساده را فراموش می‌کنند. چک‌لیست حافظه‌ی تیم است. و چیزی که خودکار بررسی شود، هیچ‌وقت فراموش نمی‌شود.

چهار گروه: هرچه ساخت image است در Dockerfile، هرچه چند سرویس است در Compose، و دو مسئله‌ی مشترک امنیت و عملکرد روی همه‌ی آن‌ها اثر می‌گذارند.

مثال ۱: بررسی خودکار Dockerfile با hadolint

Section titled “مثال ۱: بررسی خودکار Dockerfile با hadolint”

hadolint یک linter برای Dockerfile است (مثل ESLint برای JS) و بر اساس قواعدی که در این دوره دیدی هشدار می‌دهد. یک Dockerfile پر از عادت بد:

bp/bad/Dockerfile
FROM python:latest
RUN apt-get update
RUN apt-get install -y curl
ADD . /app
WORKDIR /app
RUN pip install flask
CMD python app.py
Terminal window
docker 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) بنویس. نسخه‌ی اصلاح‌شده:

bp/good/Dockerfile
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
RUN useradd -r -u 10001 app
USER 10001
CMD ["python", "app.py"]
Terminal window
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 است؟

مثال ۲: یک چک‌کننده‌ی ساده برای Compose

Section titled “مثال ۲: یک چک‌کننده‌ی ساده برای Compose”

برای Compose ابزار استانداردِ همه‌جا نیست، اما docker compose config فایل را به شکل یکسان (JSON) بیرون می‌دهد و چند خط پایتون چک‌لیست ما را اجرا می‌کند:

bp/check_compose.py
import json
import subprocess
import 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 با چند اشتباه و یکی درست:

bp/bad.compose.yaml
name: lxbpbad
services:
web:
image: nginx
ports: ["8365:80"]
db:
image: redis:latest
ports: ["6390:6379"]
privileged: true
bp/good.compose.yaml
name: lxbpgood
services:
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"]
Terminal window
cd bp
echo "=== 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 که هر دو یک اپ را می‌سازند؛ یکی کل کد را اول کپی می‌کند، دیگری وابستگی‌ها را اول:

bp/cache/app/app.py
print("hello")
bp/cache/app/requirements.txt
flask==3.0.3
bp/cache/slow.Dockerfile
FROM python:3.12-slim
WORKDIR /app
COPY app/ .
RUN pip install --no-cache-dir -r requirements.txt
bp/cache/fast.Dockerfile
FROM python:3.12-slim
WORKDIR /app
COPY app/requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app/app.py .

هر کدام را می‌سازیم، کد را یک خط عوض می‌کنیم و دوباره می‌سازیم؛ می‌شماریم چند مرحله از کش آمد (CACHED):

Terminal window
cd bp/cache
for 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.py
done
docker rmi lx-bp-slow lx-bp-fast >/dev/null 2>&1
خروجی
slow: pip install دوباره اجرا شد (مرحله‌های CACHED: 2)
fast: pip install از کش آمد (مرحله‌های CACHED: 4)

در slow با تغییر کد، لایه‌ی COPY app/ باطل شد و pip install دوباره اجرا شد (دقیقه‌ها در پروژه‌ی واقعی). در fast فقط آخرین COPY دوباره انجام شد و pip از کش آمد. این اصل «کم‌تغییر پایین، پرتغییر بالا» است و بزرگ‌ترین صرفه‌جویی زمانی عمر روزمره‌ی تو.

مثال ۴: .dockerignore و اندازه‌ی context

Section titled “مثال ۴: .dockerignore و اندازه‌ی context”
Terminal window
mkdir -p bp/ctx && cd bp/ctx
printf 'FROM alpine\nCOPY . /ctx\n' > Dockerfile
echo hi > note.txt
head -c 30000000 /dev/zero > junk.bin
for 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"
done
docker 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 ثابت کن”
Terminal window
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/Dockerfile
head -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-pin
docker rmi lx-bp-pin >/dev/null
خروجی
FROM python@sha256:dddfd7e07f9d…
Python 3.12.15

با FROM image@sha256:… همیشه دقیقاً همان base ساخته می‌شود، حتی اگر tag فردا چیز دیگری شود (تکرارپذیری و امنیت زنجیره‌ی تأمین). بهای آن: باید digest را به‌روز کنی (ابزارهایی مثل Renovate/Dependabot این کار را خودکار می‌کنند) تا به‌روزرسانی امنیتی را از دست ندهی.

این چک‌لیست را برای پروژه‌ات تیک بزن (تیک‌ها فقط در همین مرورگر می‌مانند):

✓ چک‌لیست پروژه‌ی داکر

هر آیتم چک‌لیست جواب یک حالت خرابی واقعی است:

  • latest / بدون tag: دو build در دو زمان، دو image متفاوت و رفتار غیرقابل‌ردیابی.
  • root: هر باگ در برنامه، دست مهاجم را با اختیار root کانتینر باز می‌کند.
  • بدون healthcheck/restart: خرابی ساکت؛ کسی نمی‌فهمد تا مشتری شکایت کند.
  • پورت دیتابیس باز: ربات‌ها ظرف دقایق پیدایش می‌کنند.
  • بدون سقف لاگ/حافظه: یک کانتینر کل میزبان را از کار می‌اندازد.
  • بکاپ آزمایش‌نشده: وقتی لازم شود کار نمی‌کند.

اتوماسیون مهم‌تر از حفظ کردن است: چیزی که در CI بررسی شود (hadolint، Trivy، چک‌کننده‌ی Compose)، سلیقه‌ی افراد نیست، قانون تیم است. و چک‌لیست باید با تجربه‌ی تیم زنده بماند: هر حادثه‌ای که رخ داد، یک خط به آن اضافه کن.

ابزار چه چیزی را می‌سنجد دستور
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، شبکه‌ی داخلی

چک‌لیستی که فقط روز اول اجرا شود، با تغییر کد از بین می‌رود. راه‌حل: ابزار خودکار در CI.

۲) نادیده گرفتن همه‌ی هشدارها

Section titled “۲) نادیده گرفتن همه‌ی هشدارها”
Terminal window
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 کردن همه‌ی قاعده‌ها، لینتر ساکت می‌شود ولی مشکل‌ها برقرارند. راه‌حل: فقط استثنای آگاهانه با دلیل (کامنت) و محدود به همان خط.

اگر تیم نداند چرا USER لازم است، با اولین خطا آن را حذف می‌کند. راه‌حل: هر قاعده کنارش یک جمله دلیل (مثل «پشت پرده» همین درس).

۴) بزرگ‌نمایی کوچک کردن

Section titled “۴) بزرگ‌نمایی کوچک کردن”
Terminal window
printf 'FROM scratch\nCOPY note.txt /note.txt\n' > bp/ctx/scratch.Dockerfile
docker 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>&1
خروجی
exec: "cat": executable file not found in $PATH

scratch کوچک‌ترین است ولی چیزی برای اجرا (شل، cat) ندارد و دیباگ را سخت می‌کند. راه‌حل: کوچک بودن را با قابلیت نگهداری بسنج.

۵) بررسی فقط image، نه تنظیم اجرا

Section titled “۵) بررسی فقط image، نه تنظیم اجرا”

Dockerfile کامل است ولی docker run --privileged یا docker.sock در Compose همه‌چیز را می‌شکند. راه‌حل: چک‌لیست Compose/اجرا را جدا و خودکار بسنج (مثال ۲).

✎ تمرینآسان

hadolint را روی یک Dockerfile یک‌خطی FROM ubuntu اجرا کن و بگو کدام قاعده می‌گیرد.

دیدن جواب
Terminal window
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 یا هشدار را رفع کن تا هر دو تمیز شوند.

دیدن جواب
Terminal window
mkdir -p mine && cd mine
printf 'FROM python:3.12\nCOPY . .\nCMD python main.py\n' > Dockerfile
printf 'print("ok")\n' > main.py
printf 'name: lxmine\nservices:\n app:\n image: nginx\n' > compose.yaml
echo "--- قبل:"
docker run --rm -i hadolint/hadolint hadolint --no-color - < Dockerfile 2>&1 | sed -E 's/^-:/خط /' | cut -c1-110
python3 ../bp/check_compose.py compose.yaml | grep FAIL | head -4
echo "--- بعد از رفع:"
cat > Dockerfile <<'LXEOF'
FROM python:3.12-slim
WORKDIR /app
COPY main.py .
RUN useradd -r -u 10001 app
USER 10001
CMD ["python", "main.py"]
LXEOF
cat > compose.yaml <<'LXEOF'
name: lxmine
services:
app:
image: nginx:1.27-alpine
restart: unless-stopped
mem_limit: 128m
healthcheck:
test: ["CMD", "wget", "-qO-", "http://localhost/"]
LXEOF
docker 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 arguments
FAIL 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 COMPOSE
fail=0
docker 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 $fail
LXEOF
sh 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: 1
PASS همه‌ی بررسی‌ها
کد خروج good: 0
؟ آزمونک
  1. چرا در Dockerfile ابتدا requirements و بعد کد را COPY می‌کنیم؟

  2. hadolint چه کاری می‌کند؟

  3. کدام یک اشتباه Compose است؟

  4. FROM image@sha256:… چه مزیتی دارد؟

  5. اثر .dockerignore بر build؟

  6. مهم‌ترین چیز درباره‌ی چک‌لیست؟

  • چهار گروه چک‌لیست: 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