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

داکرایز کردن اپ Python

توی این درس یاد می‌گیری یک اپ Python (یک API ساده با Flask) را داکرایز کنی و تصمیم‌های اصلی را بفهمی: image پایه‌ی slim یا alpine، نصب وابستگی‌ها با pip install --no-cache-dir -r requirements.txt و اثرش روی حجم، اجرا با gunicorn (سرور production) به‌جای سرور توسعه‌ی Flask، کاربر غیر root، و متغیرهای PYTHONUNBUFFERED و PYTHONDONTWRITEBYTECODE. در پایان، یک اپ Flask را کامل با Dockerfile اجرا و تست می‌کنی.

تشبیه: انتخاب وسیله‌ی نقلیه

Section titled “تشبیه: انتخاب وسیله‌ی نقلیه”

python:3.12 کامل مثل اتوبوس بزرگ است: همه‌چیز در آن هست ولی سنگین. python:3.12-slim مثل ون است: سبک‌تر و برای بیشتر کارها کافی. python:3.12-alpine مثل دوچرخه‌ی کارگو است: کوچک‌ترین، ولی بعضی بارها (کتابخانه‌های کامپایلی) سخت‌تر حمل می‌شوند. معمولاً slim انتخاب امن اول است.

slim بر پایه‌ی Debian و glibc است (سازگاری بیشتر)؛ alpine بر پایه‌ی musl و بسیار کوچک‌تر است ولی گاهی با کتابخانه‌های کامپایلی مشکل دارد.
app/app.py
from flask import Flask, jsonify
import os, sys
app = Flask(__name__)
app.json.ensure_ascii = False # نمایش درست متن فارسی در JSON
@app.get("/")
def home():
print("درخواست به / رسید", flush=False)
return jsonify(message="سلام از Flask در داکر")
@app.get("/health")
def health():
return jsonify(status="ok", user=os.getuid(), python=sys.version.split()[0])
app/requirements.txt
flask==3.0.3
gunicorn==22.0.0

فایل requirements.txt وابستگی‌ها را با نسخه‌ی دقیق (==) قفل می‌کند؛ معادل package-lock.json در دنیای Node (برای تکرارپذیری build).

app/Dockerfile
FROM python:3.12-slim
# لاگ‌ها فوراً در docker logs دیده شوند؛ فایل‌های .pyc ساخته نشوند
ENV PYTHONUNBUFFERED=1 \
PYTHONDONTWRITEBYTECODE=1
WORKDIR /app
# اول وابستگی‌ها (برای کش)، بدون کش pip داخل image
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
# کاربر غیر root
RUN useradd -m -u 1001 appuser
USER appuser
EXPOSE 8000
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "app:app"]
app/.dockerignore
__pycache__
*.pyc
.venv
.env
.git
  • ENV PYTHONUNBUFFERED=1: خروجی print بدون بافر مستقیم به stdout می‌رود، پس در docker logs فوراً دیده می‌شود.
  • ENV PYTHONDONTWRITEBYTECODE=1: Python فایل‌های .pyc نمی‌نویسد (image تمیزتر).
  • RUN pip install --no-cache-dir: pip فایل‌های دانلود‌شده را داخل image نگه نمی‌دارد (حجم کمتر).
  • CMD ["gunicorn", ...]: سرور production (فرم exec)، روی 0.0.0.0 تا از بیرون کانتینر در دسترس باشد. app:app یعنی «ماژول app، متغیر app».
Terminal window
docker build -t lx-py-app ./app 2>&1 | grep -E '^#[0-9]+ \[[0-9]/[0-9]\]' | sed -E 's/@sha256:[0-9a-f]+//' | awk '!s[$0]++'
docker run -d --name lx-py1 -p 8370:8000 lx-py-app >/dev/null; sleep 3
curl -s localhost:8370/
curl -s localhost:8370/health
docker logs lx-py1 2>&1 | grep -E "Listening|درخواست" | head -3
خروجی
#5 [1/6] FROM docker.io/library/python:3.12-slim
#6 [3/6] COPY requirements.txt .
#7 [2/6] WORKDIR /app
#8 [4/6] RUN pip install --no-cache-dir -r requirements.txt
#9 [5/6] COPY . .
#10 [6/6] RUN useradd -m -u 1001 appuser
{"message":"سلام از Flask در داکر"}
{"python":"3.12.15","status":"ok","user":1001}
[2026-10-03 11:06:32 +0000] [1] [INFO] Listening at: http://0.0.0.0:8000 (1)
درخواست به / رسید

API جواب داد و /health نشان می‌دهد پروسه با uid ۱۰۰۱ (کاربر appuser، غیر root) اجرا می‌شود و نسخه‌ی Python چیست. در لاگ‌ها خط «Listening at» از gunicorn و print برنامه را فوراً می‌بینیم (به‌خاطر PYTHONUNBUFFERED).

⚡ بررسی سریع

PYTHONUNBUFFERED=1 چه کمکی می‌کند؟

مثال ۴: slim در برابر alpine (اندازه‌ها)

Section titled “مثال ۴: slim در برابر alpine (اندازه‌ها)”

همان برنامه را با پایه‌ی alpine هم می‌سازیم:

Terminal window
sed 's/python:3.12-slim/python:3.12-alpine/; s/RUN useradd -m -u 1001 appuser/RUN adduser -D -u 1001 appuser/' app/Dockerfile > app/Dockerfile.alpine
docker build -q -f app/Dockerfile.alpine -t lx-py-alpine ./app >/dev/null 2>&1
docker images --format 'table {{.Repository}}\t{{.Size}}' | grep -E "REPOSITORY|lx-py-(app|alpine)"
خروجی
REPOSITORY SIZE
lx-py-alpine 97.8MB
lx-py-app 224MB

نسخه‌ی alpine چند برابر کوچک‌تر است. ولی alpine از musl استفاده می‌کند (به‌جای glibc) و بسیاری از بسته‌های Python که کد C دارند برایش wheel آماده ندارند، پس باید در زمان build کامپایل شوند (کند و نیاز به ابزار ساخت). برای یک اپ ساده‌ی Flask مشکلی نیست؛ برای numpy، pandas، psycopg2 و مانند آن slim مطمئن‌تر است. قاعده‌ی عملی: با slim شروع کن و فقط در صورت نیاز و آزمایش به alpine برو.

Terminal window
sed 's/pip install --no-cache-dir -r/pip install -r/' app/Dockerfile > app/Dockerfile.cache
docker build -q -f app/Dockerfile.cache -t lx-py-cache ./app >/dev/null 2>&1
docker images --format 'table {{.Repository}}\t{{.Size}}' | grep -E "REPOSITORY|lx-py-(app|cache)"
خروجی
REPOSITORY SIZE
lx-py-cache 227MB
lx-py-app 224MB

بدون --no-cache-dir، pip فایل‌های wheel دانلود‌شده را در ~/.cache/pip نگه می‌دارد و داخل لایه‌ی image ذخیره می‌شود (حجم اضافه بدون هیچ سودی). در image همیشه --no-cache-dir.

مثال ۶: سرور توسعه‌ی Flask و دام localhost

Section titled “مثال ۶: سرور توسعه‌ی Flask و دام localhost”
Terminal window
docker run -d --name lx-py2 -p 8371:5000 -v "$PWD/app":/app -w /app python:3.12-slim sh -c 'pip install -q flask==3.0.3 >/dev/null 2>&1 && flask --app app run --port 5000' >/dev/null
sleep 14
echo "پیش‌فرض flask run (فقط localhost داخل کانتینر):"
curl -s -m 5 -o /dev/null -w " HTTP %{http_code}\n" localhost:8371/ || echo " (کد خروج curl: $?)"
docker logs lx-py2 2>&1 | grep -E "WARNING|Running on" | head -3 | cut -c1-90
docker rm -f lx-py2 >/dev/null
خروجی
پیش‌فرض flask run (فقط localhost داخل کانتینر):
HTTP 000
(کد خروج curl: 56)
WARNING: This is a development server. Do not use it in a production deployment.
* Running on http://127.0.0.1:5000

flask run به‌صورت پیش‌فرض فقط روی 127.0.0.1 داخل کانتینر گوش می‌دهد، پس از بیرون در دسترس نیست (درس پورت‌ها)، و همین لاگ هشدار می‌دهد این سرور برای production نیست. راه‌حل: در production از gunicorn استفاده کن؛ در توسعه flask run --host 0.0.0.0.

بسته‌های Python معمولاً به شکل wheel (فایل از پیش‌ساخته) توزیع می‌شوند. برای کتابخانه‌های دارای کد C، wheel ها برای glibc (اغلب لینوکس‌ها، از جمله Debian و slim) ساخته می‌شوند. Alpine از musl استفاده می‌کند و اگر wheel مناسبی نباشد، pip بسته را از سورس کامپایل می‌کند که به compiler و هدرها نیاز دارد. این همان دلیل کندی build و بزرگ شدن image در alpine برای بعضی پروژه‌هاست.

سرور توسعه‌ی Flask تک‌نخی و بدون مدیریت مناسب خطا و بار است. gunicorn یک سرور WSGI است که چند worker می‌سازد، پروسه‌ها را مدیریت می‌کند و سیگنال‌ها (مثل SIGTERM) را درست می‌گیرد؛ به همین دلیل به‌عنوان CMD در فرم exec یک انتخاب استاندارد است.

انتخاب مزیت نکته
python:3.12 همه‌چیز دارد حجیم
python:3.12-slim سبک و سازگار انتخاب پیش‌فرض
python:3.12-alpine کوچک‌ترین musl؛ گاهی نیاز به کامپایل
تنظیم معنی
PYTHONUNBUFFERED=1 خروجی بدون بافر (لاگ فوری)
PYTHONDONTWRITEBYTECODE=1 نساختن .pyc
pip install --no-cache-dir بدون ذخیره‌ی کش pip در image
pip install -r requirements.txt نصب مطابق فایل
gunicorn --bind 0.0.0.0:8000 app:app سرور production
Terminal window
mkdir -p m1 && printf 'FROM python:3.12-slim\nCOPY requirements.txt .\nRUN pip install -r requirements.txt\n' > m1/Dockerfile
docker build -t lx-py-dev ./m1 2>&1 | grep -E 'ERROR' | head -1 | sed -E 's/ref [^ ]+/ref .../' | cut -c1-130
خروجی
#6 ERROR: failed to calculate checksum of ref ... "/requirements.txt": not found

COPY فایل را پیدا نکرد. راه‌حل: pip freeze > requirements.txt (یا ساخت دستی) و در context بگذار.

flask (بدون ==) در هر build آخرین نسخه را می‌گیرد و تکرارپذیری را از بین می‌برد. راه‌حل: نسخه‌ی دقیق، یا ابزارهایی مثل pip-tools.

۳) اجرای سرور توسعه در production

Section titled “۳) اجرای سرور توسعه در production”

مثال ۶: هشدار می‌دهد و فقط روی localhost است. راه‌حل: gunicorn.

بدون PYTHONUNBUFFERED، print ها گاهی دیر در docker logs ظاهر می‌شوند. راه‌حل: ENV PYTHONUNBUFFERED=1.

۵) COPY . . قبل از نصب وابستگی‌ها

Section titled “۵) COPY . . قبل از نصب وابستگی‌ها”

هر تغییر کد، pip install را دوباره اجرا می‌کند. راه‌حل: اول COPY requirements.txt، pip install، بعد COPY . . (درس لایه‌ها و کش).

✎ تمرینآسان

بدون ساختن image، با یک دستور docker run --rm نسخه‌ی Python داخل python:3.12-slim را چاپ کن.

دیدن جواب
Terminal window
docker run --rm python:3.12-slim python --version
خروجی
Python 3.12.15
✎ تمرینمتوسط

تمرین اصلی: یک اپ Flask را داکرایز کن. اپ app را با docker build -t lx-py-app ./app بساز، روی پورت 8372 اجرا کن و / را بخوان.

دیدن جواب
Terminal window
docker build -q -t lx-py-app ./app >/dev/null 2>&1
docker run -d --name lx-py3 -p 8372:8000 lx-py-app >/dev/null; sleep 3
curl -s localhost:8372/
docker rm -f lx-py3 >/dev/null
خروجی
{"message":"سلام از Flask در داکر"}
✎ تمرینسخت

اندازه‌ی image ساخته‌شده با slim و alpine را در یک جدول کنار هم بگذار و توضیح بده چرا alpine با وجود کوچک‌تر بودن همیشه بهترین انتخاب نیست.

دیدن جواب
Terminal window
docker images --format 'table {{.Repository}}\t{{.Size}}' | grep -E "REPOSITORY|lx-py-(app|alpine)"
خروجی
REPOSITORY SIZE
lx-py-app 224MB
lx-py-alpine 97.8MB

alpine کوچک‌تر است، ولی به‌جای glibc از musl استفاده می‌کند. بسته‌های دارای کد C (مثل numpy و psycopg2) ممکن است wheel آماده برای musl نداشته باشند و باید با ابزار ساخت کامپایل شوند (build کند و image بزرگ‌تر). slim معمولاً سازگارتر و پیش‌بینی‌پذیرتر است.

؟ آزمونک
  1. pip install --no-cache-dir در Dockerfile چه سودی دارد؟

  2. چرا در production به‌جای flask run از gunicorn استفاده می‌کنیم؟

  3. flask run در کانتینر از بیرون در دسترس نیست چون…

  4. alpine چرا گاهی مشکل‌ساز است؟

  5. PYTHONUNBUFFERED=1 برای چیست؟

  • ساختار: FROM python:3.12-slim ← ENV PYTHONUNBUFFERED=1 PYTHONDONTWRITEBYTECODE=1 ← COPY requirements.txt ← pip install --no-cache-dir -r ← COPY . . ← کاربر غیر root ← CMD ["gunicorn", ...].
  • slim انتخاب پیش‌فرض؛ alpine کوچک‌تر ولی با musl (گاهی نیاز به کامپایل).
  • نسخه‌ی وابستگی‌ها را با == قفل کن.
  • gunicorn برای production؛ برنامه روی 0.0.0.0 گوش بدهد.
  • .dockerignore: __pycache__، .venv، .env، .git.
برگه‌ی تقلب این درس
دستورکاری که می‌کند
pip install --no-cache-dir -r requirements.txtنصب بدون کش pip
pip freeze > requirements.txtساخت فایل وابستگی‌ها
ENV PYTHONUNBUFFERED=1لاگ بدون بافر
ENV PYTHONDONTWRITEBYTECODE=1نساختن .pyc
gunicorn --bind 0.0.0.0:8000 app:appسرور production
flask --app app run --host 0.0.0.0سرور توسعه که از بیرون در دسترس است
docker build -t py-app .ساخت image
docker run -d -p 8000:8000 py-appاجرا با پورت