اکنون برنامه، پایگاه داده و کلید دسترسی داریم؛ برای اجرای دوبارهٔ آن باید معلوم باشد چه چیزی در بستهٔ برنامه است و چه چیزی هنگام اجرا تأمین می‌شود. این شب API را در تصویر Docker قرار می‌دهیم و تنظیمات اجرای آن را با Compose می‌نویسیم. نتیجهٔ مورد انتظار، کانتینری با کاربر غیرریشه و دادهٔ پایدار است که فقط از همین میزبان قابل دسترسی باشد.

کد، تنظیم و داده سه عمر متفاوت دارند#

تصویر یا image شامل مفسر، بسته‌ها و کد برنامه است. کانتینر یک اجرای آن تصویر با لایهٔ نوشتنی خودش است. تغییر تنظیمی مثل سطح گزارش نباید نیازمند ویرایش کد و ساخت دوبارهٔ تصویر باشد؛ آن را از محیط می‌گیریم. پایگاه داده هم نباید با جایگزینی کانتینر حذف شود؛ آن را روی volume، یعنی فضای ذخیرهٔ مستقل از لایهٔ کانتینر، قرار می‌دهیم.

برای این شب Docker Engine و افزونهٔ Compose نسخهٔ دوم باید از قبل روی میزبان آزمایشگاهی نصب و قابل استفاده باشند. نصب و پیکربندی موتور کانتینر موضوع این درس نیست. فرمان‌های docker version و docker compose version را بررسی کنید؛ نبود دسترسی به daemon با نبود ابزار خط فرمان فرق دارد. دریافت تصویر پایه و بسته‌ها هنگام ساخت به شبکه یا مخزن محلی نیاز دارد.

پوشهٔ ~/python-backend/night-11 را بسازید و main.py و security.py تکمیل‌شدهٔ شب دهم را در آن بگذارید. از یک محیط مجازی تازه، وابستگی زمان اجرا را نصب و ثبت کنید. تصویر پایهٔ مثال python:3.14-slim است؛ برچسب آن در آینده تغییر می‌کند. برای انتشار واقعی، نسخهٔ اصلاحی یا digest تأییدشده و روش ثابت‌کردن وابستگی‌ها را جدا مشخص کنید. این تمرین ادعای بازسازی بایت‌به‌بایت تصویر ندارد.

python3 -m venv .venv
source .venv/bin/activate
python -m pip install "fastapi[standard]"
python -m pip freeze > requirements.txt

این ثبت را در محیط اختصاصی وابستگی‌های زمان اجرا انجام دهید؛ اگر از محیط آزمون freeze بگیرید، pytest و ابزارهای اضافی هم وارد تصویر می‌شوند. فهرست نسخه‌ها بازسازی را قابل بررسی‌تر می‌کند ولی برای تضمین یکسان‌بودن بسته‌ها روی همهٔ معماری‌ها کافی نیست. در این دوره هدف روشن‌بودن مرز وابستگی و کد است و فایل به‌دست‌آمده را همراه پروژه نگه می‌داریم.

Dockerfile و زمینهٔ ساخت#

فایل Dockerfile زیر را بسازید. ابتدا فهرست وابستگی‌ها کپی می‌شود تا تغییر کد معمولاً نیاز به نصب دوبارهٔ همهٔ بسته‌ها نداشته باشد. PYTHONUNBUFFERED کمک می‌کند خروجی گزارش بی‌درنگ به سیستم ثبت کانتینر برسد. USER اجرای برنامه را از حساب ریشه خارج می‌کند و مالکیت /data اجازهٔ ساخت فایل SQLite را می‌دهد.

FROM python:3.14-slim

ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
WORKDIR /app

COPY requirements.txt .
RUN python -m pip install --no-cache-dir -r requirements.txt

RUN useradd --uid 10001 --create-home appuser \
    && mkdir -p /data \
    && chown appuser:appuser /data

COPY --chown=appuser:appuser main.py security.py ./
USER appuser
EXPOSE 8000
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

داخل کانتینر، 0.0.0.0 یعنی Uvicorn روی رابط‌های آن کانتینر گوش می‌دهد. این به‌تنهایی درگاه را روی میزبان عمومی نمی‌کند؛ انتشار درگاه با تنظیم اجرای Docker تعیین می‌شود. EXPOSE هم فقط مستندسازی درگاه تصویر است. شکل آرایه‌ای CMD پوستهٔ اضافه‌ای میان فرایند اصلی و سرور قرار نمی‌دهد و رسیدن سیگنال توقف به برنامه را ساده می‌کند.

فایل .dockerignore را بسازید تا داده و اعتبار خصوصی وارد زمینهٔ ساخت نشوند. حتی اگر COPY فعلی فقط فایل‌های مشخص را بردارد، حذف فایل‌های غیرلازم از زمینهٔ ارسال به موتور ساخت مفید است. فهرست زیر را برای همین پروژه نگه دارید؛ پوشهٔ گواهی شب بعد نیز از تصویر برنامه بیرون می‌ماند.

.venv
__pycache__
*.pyc
*.db
*.db-*
.env
.git
.pytest_cache
test_*.py
certs
docker build -t inventory-api:night-11 .
read -rsp 'API key: ' INVENTORY_API_KEY
echo
export INVENTORY_API_KEY
docker volume create inventory-data
docker run --rm --name inventory-api \
  -p 127.0.0.1:8000:8000 \
  -e INVENTORY_API_KEY -e LOG_LEVEL=INFO \
  -e DATABASE_PATH=/data/inventory.db \
  -v inventory-data:/data inventory-api:night-11

نام inventory-data را برای این آزمایشگاه تازه انتخاب کنید؛ اگر volume قدیمی با همین نام دارید، پیش از استفاده منشأ و مالکیت آن را بررسی کنید. روی volume تازه، مالکیت دایرکتوری تصویر به ساخت فضای قابل نوشتن کمک می‌کند. پوشهٔ میزبان یا volume قدیمی ممکن است مالکیت متفاوتی داشته باشد؛ خطای مجوز را با اجرای دائمی برنامه به‌عنوان root پنهان نکنید.

از فرمان بلند به Compose#

در ترمینال دوم سلامت را بخوانید و با docker exec inventory-api id کاربر فرایند را بررسی کنید؛ شناسهٔ مورد انتظار 10001 است. با کلید صحیح یک رکورد ثبت کنید. سپس با Ctrl+C کانتینر را متوقف کنید؛ --rm همان کانتینر را حذف می‌کند ولی volume باقی می‌ماند. اجرای دوباره با همان volume باید رکورد قبلی را برگرداند.

اکنون به‌جای تکرار فرمان بلند، فایل compose.yaml بنویسید. تنظیم‌های این فایل به همان کد شب دهم مربوط‌اند و مسیر /health واقعاً جدول پایگاه را می‌خواند. کلید از محیط پوسته می‌آید؛ عبارت الزام‌آور Compose نبودن یا خالی‌بودن آن را پیش از اجرا گزارش می‌کند. مقدار واقعی کلید را داخل فایل ننویسید.

services:
  api:
    build: .
    ports:
      - "127.0.0.1:8000:8000"
    environment:
      DATABASE_PATH: /data/inventory.db
      LOG_LEVEL: INFO
      INVENTORY_API_KEY: ${INVENTORY_API_KEY:?INVENTORY_API_KEY must be set}
    volumes:
      - inventory-data:/data
    restart: unless-stopped
    healthcheck:
      test:
        - CMD
        - python
        - -c
        - "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/health', timeout=2)"
      interval: 10s
      timeout: 3s
      retries: 3
      start_period: 5s

volumes:
  inventory-data:
docker compose config --quiet
docker compose up --build --detach
docker compose ps
curl http://127.0.0.1:8000/health
docker compose logs api

برای پیکربندی دارای راز از config --quiet استفاده می‌کنیم تا نسخهٔ جایگزینی‌شدهٔ متغیرها چاپ نشود. پس از چند بررسی، وضعیت باید healthy شود. healthcheck فرمانی را داخل کانتینر اجرا می‌کند؛ اگر وابستگی پایگاه مشکل داشته باشد، مسیر سلامت 503 می‌دهد و probe ناموفق می‌شود. سالم بودن probe به معنی درست بودن همهٔ مسیرها و همهٔ قراردادهای امنیتی نیست؛ آزمون‌های شب دهم همچنان لازم‌اند.

سیاست restart: unless-stopped به خروج فرایند اصلی مربوط است. نتیجهٔ unhealthy به‌تنهایی کانتینر را راه‌اندازی دوباره نمی‌کند. interval فاصلهٔ بررسی، timeout مهلت هر بررسی، retries شمار شکست‌های لازم و start_period فرصت آغاز برنامه را تعیین می‌کند. اگر وضعیت ناسالم ماند، ابتدا logs و دسترسی به فایل پایگاه را بررسی کنید، نه اینکه بی‌دلیل تعداد تلاش‌ها را بالا ببرید.

تمرین ماندگاری و گزارش#

volume فرمان docker run با نام ثابت بالا با volume پروژه‌ای Compose الزاماً یکی نیست؛ Compose معمولاً نام پروژه را به نام منبع اضافه می‌کند. برای آزمون Compose، یک رکورد تازه ثبت کنید و نام volume همان پروژه را با docker volume ls و برچسب‌های آن در docker volume inspect بررسی کنید. نتیجهٔ موفق آزمایش قبلی را به وجود داده در volume دیگری نسبت ندهید.

docker compose down
docker compose up --detach

پس از این توقف و آغاز، رکورد Compose باید باقی باشد. گزینهٔ --volumes را به down اضافه نکنید؛ آن گزینه volume اعلام‌شده و دادهٔ داخلش را هم حذف می‌کند. برای تمرین تکمیلی منبع، جدولی به نام metadata با شمارندهٔ starts بسازید و هنگام آغاز برنامه مقدارش را افزایش دهید؛ با هر اجرای تازه، مقدار قبلی باید از همان فایل خوانده شود. این شمارنده فقط تمرین ذخیره‌سازی است و جای معیار قابل اعتماد تعداد راه‌اندازی‌ها را نمی‌گیرد.

برای خودآزمایی توضیح دهید چرا کلید در تصویر نیست، چرا SQLite روی volume است و چرا وضعیت ناسالم لزوماً به restart منجر نمی‌شود. در پایان، API باید فقط از loopback میزبان در دسترس باشد؛ در شب بعد همین درگاه مستقیم را هم حذف و Nginx را تنها ورودی شبکه می‌کنیم.

منابع#