تمرین هفته ۵

در این تمرین، مجموعه‌ای از مهارت‌های کلیدی هفته‌ی پنجم را در قالب یک پروژه‌ی واقعی و استاندارد پیاده‌سازی می‌کنید: ساخت ایمیج بهینه با رویکرد Multi-stage Build، انتشار ایمیج در رجیستری ابری (GitHub Container Registry - GHCR)، مدیریت Secretها و متغیرها با .env و در نهایت ارکستراسیون و راه‌اندازی Declarative سرویس‌ها (وب‌سرویس پایتون + پایگاه‌داده‌ی PostgreSQL) با Docker Compose.

در هفته‌ی چهارم وب‌سرویس را مستقیماً روی سیستم‌عامل میزبان با استفاده از systemd و venv مستقر کردید. در این هفته وارد دنیای مدرن Cloud-Native می‌شوید؛ برنامه‌ی خود را بسته‌بندی (Containerize) می‌کنید تا مستقل از بستر میزبان، روی هر سروری با یک دستور بالا بیاید.

پیش از شروع، درس‌های این هفته به‌ویژه ساخت Image با Dockerfile و بهینه‌سازی لایه‌ها، کار با Registryها و مدیریت سرویس‌های چندکانتینری با Docker Compose را مرور کنید.


مشخصات فنی و استانداردهای تمرین

پیاده‌سازی شما باید دقیقاً مطابق با مشخصات فنی زیر باشد:

مولفه / نیازمندیمقدار استاندارد الزامیتوضیحات
وب‌سرویس پایتونapp/app.pyمبتنی بر Flask، با اندپوینت‌های بررسی سلامت و اتصال به دیتابیس
ساختار DockerfileMulti-stage Buildتفکیک مرحله‌ی ساخت وابستگی‌ها (builder) از مرحله‌ی نهایی و سبک (runtime)
کاربر غیر روت (اختیاری)USER <username>اجرای پروسه‌ی کانتینر با کاربر غیر روت جهت ارتقای امنیت (اختیاری/بونس)
رجیستری انتشارghcr.io (GHCR)ایمیج باید در رجیستری گیت‌هاب منتشر و دسترسی آن عمومی (Public) باشد
آدرس ایمیج در Composeimage: ghcr.io/<username>/<image>:<tag>فایل Compose نباید ایمیج وب را به‌صورت محلی (build: .) بسازد؛ بلکه باید مستقیماً ایمیج GHCR را فراخوانی کند
پایگاه‌دادهpostgres:16-alpineسرویس دیتابیس در فایل Compose همراه با Named Volume ماندگار
بررسی سلامت دیتابیس (اختیاری)healthcheck با pg_isreadyاطمینان از بالا آمدن کامل دیتابیس پیش از استارت وب‌سرویس (اختیاری/بونس)
پورت‌های ارتباطیوب: 8080 (هاست) ◄ 5000 (کانتینر)
دیتابیس: فقط داخلی روی پورت 5432
پورت دیتابیس به دلایل امنیتی نباید روی هاست منتشر (ports) شود
مدیریت Secretها و متغیرها.env و .env.exampleمقادیر واقعی در .env (ignore شده در گیت) و نمونه‌ی تمپلیت در .env.example
شبکه‌ی ارتباطیCustom Bridge Networkارتباط دو کانتینر از طریق نام سرویس در شبکه‌ی اختصاصی Compose

گام ۱ — آماده‌سازی وب‌سرویس پایتون

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

ابتدا دایرکتوری کار تمرین را در ماشین لینوکسی خود بسازید:

1mkdir -p ~/week-5-lab/app
2cd ~/week-5-lab

۱. فایل نیازمندی‌ها (app/requirements.txt)

فایل app/requirements.txt را با محتویات زیر ایجاد کنید:

1Flask==3.1.3
2psycopg2-binary==2.9.13

۲. کد وب‌سرویس (app/app.py)

فایل app/app.py را بسازید. این برنامه مشخصات اتصال به پایگاه‌داده را از متغیرهای محیطی دریافت می‌کند:

  1import os
  2import time
  3import socket
  4import psycopg2
  5from psycopg2 import OperationalError
  6from flask import Flask, jsonify
  7
  8app = Flask(__name__)
  9
 10# Read database connection parameters from environment variables
 11DB_HOST = os.getenv("DB_HOST", "db")
 12DB_PORT = int(os.getenv("DB_PORT", "5432"))
 13DB_NAME = os.getenv("DB_NAME", "devops_db")
 14DB_USER = os.getenv("DB_USER", "postgres")
 15DB_PASSWORD = os.getenv("DB_PASSWORD", "postgres")
 16
 17def get_db_connection(max_retries=5, delay=2):
 18    """Establish connection to PostgreSQL with retry logic."""
 19    for attempt in range(max_retries):
 20        try:
 21            conn = psycopg2.connect(
 22                host=DB_HOST,
 23                port=DB_PORT,
 24                dbname=DB_NAME,
 25                user=DB_USER,
 26                password=DB_PASSWORD,
 27                connect_timeout=3
 28            )
 29            return conn
 30        except OperationalError:
 31            if attempt < max_retries - 1:
 32                time.sleep(delay)
 33            else:
 34                raise
 35
 36def init_db():
 37    """Create the visits counter table if it does not exist."""
 38    try:
 39        conn = get_db_connection()
 40        cur = conn.cursor()
 41        cur.execute("""
 42            CREATE TABLE IF NOT EXISTS visits (
 43                id SERIAL PRIMARY KEY,
 44                visited_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
 45            );
 46        """)
 47        conn.commit()
 48        cur.close()
 49        conn.close()
 50    except Exception as e:
 51        app.logger.error(f"Database initialization error: {e}")
 52
 53# Initialize table at application startup
 54try:
 55    init_db()
 56except Exception:
 57    pass
 58
 59@app.route("/health", methods=["GET"])
 60def health():
 61    """Health check endpoint to verify database connectivity."""
 62    try:
 63        conn = get_db_connection(max_retries=1)
 64        cur = conn.cursor()
 65        cur.execute("SELECT 1;")
 66        cur.close()
 67        conn.close()
 68        return jsonify({
 69            "status": "healthy",
 70            "database": "connected"
 71        }), 200
 72    except Exception as e:
 73        return jsonify({
 74            "status": "unhealthy",
 75            "database": f"disconnected: {str(e)}"
 76        }), 500
 77
 78@app.route("/", methods=["GET"])
 79def index():
 80    """Record a new visit and return total visit count."""
 81    try:
 82        conn = get_db_connection()
 83        cur = conn.cursor()
 84        cur.execute("""
 85            CREATE TABLE IF NOT EXISTS visits (
 86                id SERIAL PRIMARY KEY,
 87                visited_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
 88            );
 89        """)
 90        cur.execute("INSERT INTO visits DEFAULT VALUES;")
 91        conn.commit()
 92
 93        cur.execute("SELECT COUNT(*) FROM visits;")
 94        count = cur.fetchone()[0]
 95        cur.close()
 96        conn.close()
 97
 98        return jsonify({
 99            "service": "techstack-1405-python-app",
100            "hostname": socket.gethostname(),
101            "visits": count,
102            "database": "connected"
103        }), 200
104    except Exception as e:
105        return jsonify({
106            "service": "techstack-1405-python-app",
107            "error": str(e)
108        }), 500
109
110if __name__ == "__main__":
111    app.run(host="0.0.0.0", port=5000)

گام ۲ — ساخت Dockerfile چندمرحله‌ای (Multi-stage Build)

هدف این گام، نوشتن یک Dockerfile استاندارد با تکنیک Multi-stage Build است تا ایمیج نهایی فاقد کامپایلرها، پکیج‌های بیلد اضافه و فایل‌های کش pip باشد.

۱. ایجاد فایل .dockerignore

در ریشه‌ی پروژه (~/week-5-lab/.dockerignore) فایلی بسازید تا فایل‌های غیرضروری و حساس وارد Build Context نشوند:

1.git
2.gitignore
3.env
4__pycache__
5*.pyc
6*.pyo
7*.pyd
8.pytest_cache

۲. الزامات و الگوی مفهومی فایل Dockerfile

در مسیر ~/week-5-lab/Dockerfile یک فایل داکرفایل دومرحله‌ای طراحی کنید که نیازمندی‌های زیر را برآورده سازد:

  1. مرحله‌ی اول — ساخت و وابستگی‌ها (Builder Stage):
    • از یک ایمیج پایه‌ی رسمی و پایدار پایتون (مانند python:3.11-slim) با یک شناسه (مانند AS builder) استفاده کنید.
    • یک مسیر کاری (WORKDIR) مشخص کنید.
    • یک محیط مجازی (venv) بسازید و مسیر باینری‌های آن را در متغیر محیطی PATH قرار دهید تا نیازمندی‌ها درون آن نصب شوند.
    • فایل وابستگی‌ها (app/requirements.txt) را پیش از کدهای سورس کپی کرده و وابستگی‌ها را با سوئیچ --no-cache-dir نصب کنید تا از قابلیت Caching لایه‌ها نهایت استفاده را ببرید.
  2. مرحله‌ی دوم — زمان اجرای نهایی (Runtime Stage):
    • از یک ایمیج سبک و تمیز (مانند python:3.11-slim) به عنوان پایه‌ی مرحله‌ی نهایی استفاده کنید.
    • صرفاً پوشه‌ی محیط مجازی آماده را با دستور COPY --from=builder ... از مرحله‌ی بیلد به ایمیج نهایی منتقل کنید.
    • متغیر PATH را به مسیر محیط مجازی کپی‌شده تنظیم کنید تا دستور python مستقیماً پکیج‌های نصب‌شده را شناسایی کند.
    • کدهای برنامه (app/) را به کانتینر منتقل کنید.
    • پورت وب‌سرویس (5000) را با EXPOSE مشخص کنید و دستور اجرای وب‌سرویس (CMD) را به فرمت استاندارد Exec Form تعریف نمایید.

قالب مفهومی (Conceptual Skeleton):

 1# ==========================================
 2# Stage 1: Build & Dependencies (Builder)
 3# ==========================================
 4FROM python:3.11-slim AS builder
 5
 6WORKDIR /app
 7
 8# TODO: ساخت محیط مجازی و تنظیم PATH
 9# ...
10
11# TODO: کپی requirements.txt و نصب پکیج‌ها بدون کش
12# ...
13
14# ==========================================
15# Stage 2: Runtime Image (Lean & Minimal)
16# ==========================================
17FROM python:3.11-slim
18
19WORKDIR /app
20
21# TODO: انتقال پوشه venv از استیج builder و تنظیم مجدد PATH
22# COPY --from=builder ...
23# ENV PATH=...
24
25# TODO: کپی کدهای برنامه به مسیر کاری
26# COPY ...
27
28# TODO: مستندسازی پورت و دستور اجرای برنامه
29# EXPOSE ...
30# CMD [...]

بخش اختیاری — اجرای امن با کاربر غیر روت (Non-root User)

چالش اختیاری: امنیت بیشتر کانتینر

به‌طور پیش‌فرض، پردازه‌ی درون کانتینر با کاربر root اجرا می‌شود. در محیط‌های واقعی Production، توصیه می‌شود یک کاربر و گروه سیستمی بدون دسترسی روت بسازید و اجرای برنامه را با دستور USER به آن واگذار کنید:

1# ایجاد کاربر و گروه سیستمی و اعطای دسترسی به دایرکتوری کار
2RUN groupadd -r appuser && useradd -r -g appuser -d /app -s /sbin/nologin appuser \
3    && chown -R appuser:appuser /app
4
5USER appuser

پیاده‌سازی این بخش در تمرین اختیاری است.

۳. آزمایش ساخت ایمیج به‌صورت محلی و بررسی لایه‌ها

ایمیج را با یک تگ محلی بسازید و مشخصات آن را بررسی کنید:

1docker build -t techstack-1405-python-app:test .
2docker images techstack-1405-python-app:test

با دستور docker history، لایه‌های ساخته‌شده و میزان حجم هر لایه را مشاهده کنید:

1docker history techstack-1405-python-app:test

بررسی کنید که کدام لایه‌ها بیشترین فضا را اشغال کرده‌اند و چگونه انتقال تمیز فایل‌های محیط مجازی (/opt/venv) باعث شده تا ابزارهای بیلد در ایمیج نهایی قرار نگیرند. خروجی کوتاه دستورهای بالا را برای ثبت در 01-dockerfile.md یادداشت کنید.


گام ۳ — آشنایی با GHCR و انتشار ایمیج (Image Push)

رجیستری GHCR چیست و چرا از آن استفاده می‌کنیم؟

GitHub Container Registry (GHCR) با آدرس دامنه ghcr.io، سرویس میزبانی کانتینر اختصاصی گیت‌هاب است که با استاندارد OCI مطابقت دارد.

چرا به‌جای Docker Hub از GHCR استفاده می‌کنیم؟

  1. محدودیت‌ها و تحریم‌های داکر هاب برای کاربران ایران: همان‌طور که در درس سوم دیدید، سرویس داکر هاب به‌دلیل تحریم‌ها دسترسی مستقیم کاربران و سرورهای با IP ایران را مسدود کرده است (403 Forbidden یا خطای دسترسی جغرافیایی). ثبت اکانت، ورود و ارسال (docker push) به Docker Hub نیازمند استفاده‌ی مداوم از ابزارهای تغییر IP و پروکسی‌های اختصاصی است؛ در حالی که رجیستری GHCR به‌صورت مستقیم و پایدار از طریق اکانت گیت‌هاب در دسترس است و در بسیاری از محیط‌ها دردسرهای تحریم Docker Hub را ندارد.
  2. یکپارچگی مستقیم با گیت‌هاب: مخزن کدهای شما، دسترسی‌ها و پکیج‌های کانتینری همگی ذیل یک حساب کاربری قرار می‌گیرند و برای پایپ‌لاین‌های CI/CD (هفته‌های آینده) نیاز به اتصال به سرویس شخص ثالث مجزا ندارید.
  3. سقف درخواست بسیار بالاتر (Rate Limits): داکر هاب برای کاربران ناشناس محدودیت سخت‌گیرانه‌ی ۱۰۰ پول در ۶ ساعت اعمال می‌کند؛ در حالی که GHCR در تیم‌های DevOps پایداری و ظرفیت به مراتب بالاتری دارد.
  4. هم‌نامی سازمانی با مخزن پروژه: نام ایمیج مستقیماً شامل نام کاربری شما در گیت‌هاب می‌شود: ghcr.io/<your-github-username>/techstack-1405-python-app:1.0.0.

برای انتشار ایمیج در GHCR مراحل زیر را دنبال کنید:

۱. ساخت توکن دسترسی شخصی (Personal Access Token - PAT)

برای ورود به GHCR با خط فرمان داکر، نمی‌توانید از رمز عبور عادی گیت‌هاب استفاده کنید. باید یک Personal Access Token بسازید:

  1. در مرورگر وارد حساب کاربری GitHub خود شوید.
  2. به بخش Settings (پروفایل شخصی) بروید.
  3. در منوی سمت چپ به پایین صفحه اسکرول کرده و روی Developer settings کلیک کنید.
  4. روی Personal access tokens و سپس Tokens (classic) کلیک کنید.
  5. روی Generate new token ◄ Generate new token (classic) کلیک کنید.
  6. یک نام در بخش Note وارد کنید (مانند GHCR Token Week 5).
  7. تیک گزینه‌های زیر را فعال کنید:
    • write:packages (آپلود و انتشار پکیج‌ها در GitHub Package Registry)
    • read:packages (دانلود و دریافت پکیج‌ها)
    • delete:packages (اختیاری - برای مدیریت و حذف پکیج‌های قدیمی)
  8. روی دکمه‌ی سبز رنگ Generate token کلیک کرده و رشته‌ی توکن ایجادشده (که با ghp_ آغاز می‌شود) را در جایی امن کپی کنید.
هشدار امنیتی توکن‌ها

هرگز مقدار توکن PAT را در مخزن گیت کامیت نکنید یا آن را در فایل‌های متنی داخل ریپازیتوری ذخیره نسازید. توکن‌ها معادل رمز عبور شما هستند.

۲. لاگین در GHCR با دستور docker login

روی ماشین لینوکس خود، دستور docker login را برای رجیستری ghcr.io اجرا کنید:

1docker login ghcr.io

داکر به‌صورت تعاملی (Interactive) نام کاربری و رمز عبور را از شما درخواست می‌کند:

  1. Username: نام کاربری حساب کاربری GitHub خود را وارد کنید (با حروف کوچک انگلیسی).
  2. Password: مقدار Personal Access Token (PAT) که در مرحله‌ی قبل کپی کردید را Paste کنید (رمز عبور اصلی اکانت کار نخواهد کرد و در ترمینال کاراکترهای آن نمایش داده نمی‌شوند).
نکته در مورد نام کاربری در GHCR

در مراحل لاگین و نام‌گذاری ایمیج‌ها، حتماً <your-github-username> را با حروف کوچک انگلیسی بنویسید؛ زیرا استاندارد رجیستری‌های کانتینر نسبت به حروف بزرگ حساس است.

در صورت موفقیت، پیام Login Succeeded در ترمینال نمایش داده می‌شود.

۳. تگ‌گذاری و ارسال ایمیج (Tag & Push)

ایمیج محلی را مطابق الگوی GHCR برچسب بزنید و به سرورهای گیت‌هاب ارسال کنید:

1# برچسب‌گذاری ایمیج (استفاده از نسخه‌بندی معنایی مانند 1.0.0)
2docker tag techstack-1405-python-app:test ghcr.io/<your-github-username>/techstack-1405-python-app:1.0.0
3
4# ارسال لایه‌ها به رجیستری GHCR
5docker push ghcr.io/<your-github-username>/techstack-1405-python-app:1.0.0

۴. الزامی: عمومی‌سازی دسترسی پکیج (Change Package Visibility to Public)

به‌صورت پیش‌فرض، ایمیج‌هایی که برای نخستین بار به GHCR ارسال می‌شوند ممکن است در وضعیت Private قرار گیرند. اگر ایمیج Private بماند، تیم داوری یا همکاران نمی‌توانند بدون لاگین به اکانت شما آن را Pull کنند.

برای عمومی کردن ایمیج:

  1. در وب‌سایت گیت‌هاب به صفحه اصلی پروفایل خود بروید: https://github.com/<your-github-username>
  2. به زبانه‌ی Packages بروید.
  3. پکیج تازه‌تولیدشده‌ی techstack-1405-python-app را انتخاب کنید.
  4. در سمت راست، روی Package settings کلیک کنید.
  5. به انتهای صفحه بخش Danger Zone بروید و روی دکمه‌ی Change visibility کلیک کنید.
  6. گزینه‌ی Public را انتخاب کرده و با تایپ نام پکیج، تغییر را تأیید نمایید.

برای آزمودن عمومی بودن، می‌توانید از طریق یک نشست یا شل بدون لاگین امتحان کنید:

1docker pull ghcr.io/<your-github-username>/techstack-1405-python-app:1.0.0

گام ۴ — مدیریت متغیرهای محیطی با .env و .env.example

یکی از اصول بنیادین متدولوژی Twelve-Factor App (به‌ویژه اصل سوم: III. Config)، تفکیک کامل پیکربندی‌ها و Secretها از کد برنامه است:

  • فایل .env: حاوی مقادیر واقعی محیط جاری شماست و هرگز نباید کامیت شود.
  • فایل .env.example: یک فایل الگو شامل نام تمام متغیرها و مقادیر پیش‌فرض یا ساختگی است که در مخزن گیت کامیت می‌شود تا دیگران بدانند چه متغیرهایی برای راه‌اندازی پروژه لازم است.

در مسیر ~/week-5-lab/.env.example فایل زیر را بسازید:

1# تنظیمات پایگاه‌داده PostgreSQL
2POSTGRES_DB=devops_db
3POSTGRES_USER=postgres
4POSTGRES_PASSWORD=change_this_to_a_secure_password
5POSTGRES_PORT=5432
6
7# تنظیمات پورت وب‌سرویس روی سیستم میزبان
8APP_PORT=8080

سپس برای اجرای محلی خودتان، یک کپی از آن به نام .env ایجاد کرده و پسورد دلخواه خود را تعیین کنید:

1cp .env.example .env

گام ۵ — تدوین compose.yaml و راه‌اندازی یکپارچه‌ی سرویس‌ها

اکنون در ریشه‌ی پروژه، فایل ~/week-5-lab/compose.yaml را طراحی کنید. این فایل زیرساخت دو کانتینری پروژه (دیتابیس PostgreSQL و وب‌سرویس پایتون) را به‌صورت توصیفی مدیریت می‌کند.

قانون مهم در مورد مرجع ایمیج وب‌سرویس

در فایل compose.yaml برای سرویس وب، نباید از کلید build: . استفاده کنید! سرویس وب باید مستقیماً ایمیج عمومی شما در GHCR را فراخوانی کند:

1image: ghcr.io/<your-github-username>/techstack-1405-python-app:1.0.0

این کار تضمین می‌کند که ایمیج روی رجیستری ابری با موفقیت پابلیش شده، قابل دانلود عمومی است و بدون نیاز به کدهای سورس محلی روی سرور اجرا می‌شود.

الزامات پیاده‌سازی compose.yaml:

  1. سرویس دیتابیس (db):
    • استفاده از ایمیج سبک و پایدار postgres:16-alpine.
    • دریافت متغیرهای نام دیتابیس، کاربر و رمز عبور از فایل .env (مانند ${POSTGRES_DB}).
    • اتصال داده‌های پایگاه‌داده (/var/lib/postgresql/data) به یک Named Volume جهت جلوگیری از حذف اطلاعات با خاموش شدن کانتینر.
    • اتصال به شبکه‌ی اختصاصی کانتینرها.
    • نکته امنیتی: به هیچ وجه پورت 5432 را به هاست نگاشت نکنید (ports)؛ ارتباط دیتابیس فقط باید در شبکه‌ی داخلی کانتینرها برقرار باشد.
  2. سرویس وب (web):
    • فراخوانی ایمیج ساخته‌شده از GHCR (ghcr.io/...).
    • اتصال پورت هاست (از متغیر ${APP_PORT}) به پورت کانتینر (5000).
    • تنظیم متغیرهای محیطی اتصال به پایگاه‌داده (DB_HOST برابر با نام سرویس دیتابیس یعنی db، DB_PORT برابر با 5432 و سایر مشخصات از .env).
    • تعریف وابستگی به دیتابیس (depends_on: - db).
    • اتصال به همان شبکه‌ی اختصاصی کانتینرها.
  3. تعریف سراسری منابع:
    • بخش‌های volumes: و networks: در انتهای فایل تعریف شوند.

قالب مفهومی (Conceptual Skeleton):

 1services:
 2  db:
 3    image: postgres:16-alpine
 4    restart: unless-stopped
 5    environment:
 6      # TODO: تنظیم متغیرهای POSTGRES_DB, POSTGRES_USER, POSTGRES_PASSWORD با ارجاع به .env
 7      POSTGRES_DB: ${POSTGRES_DB}
 8      # ...
 9    volumes:
10      # TODO: اتصال Named Volume به مسیر دیتای پستگرس
11      - <named_volume>:/var/lib/postgresql/data
12    networks:
13      - <custom_network>
14
15  web:
16    # الزامی: استفاده از ایمیج پابلیک GHCR (از build: . استفاده نکنید)
17    image: ghcr.io/<your-github-username>/techstack-1405-python-app:1.0.0
18    restart: unless-stopped
19    ports:
20      # TODO: مپ کردن پورت هاست به پورت 5000 کانتینر
21      - "${APP_PORT}:5000"
22    environment:
23      # TODO: آدرس‌دهی نام سرویس دیتابیس (db) و ارسال مقادیر اتصال
24      DB_HOST: db
25      # ...
26    depends_on:
27      - db
28    networks:
29      - <custom_network>
30
31volumes:
32  # TODO: تعریف Named Volume برای ماندگاری اطلاعات
33  <named_volume>:
34
35networks:
36  # TODO: تعریف شبکه‌ی اختصاصی Bridge
37  <custom_network>:

بخش اختیاری — سلامت‌سنجی دیتابیس (Healthcheck)

چالش اختیاری: راه‌اندازی مطمئن با Healthcheck

در داکر کامپوز، استفاده‌ی ساده از depends_on: - db فقط ترتیب استارت کانتینر را تعیین می‌کند، اما تضمین نمی‌کند که پروسه‌ی PostgreSQL در لحظه‌ی شروع وب‌سرویس کاملاً بالا آمده و آماده‌ی پذیرش اتصال باشد.

برای پیاده‌سازی حرفه‌ای این سازوکار، می‌توانید یک healthcheck روی کانتینر دیتابیس با باینری pg_isready تعریف کنید و در سرویس وب با شرط condition: service_healthy به آن وابسته شوید:

 1  db:
 2    # ...
 3    healthcheck:
 4      test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
 5      interval: 5s
 6      timeout: 5s
 7      retries: 5
 8
 9  web:
10    # ...
11    depends_on:
12      db:
13        condition: service_healthy
چرا $$ به جای $ در دستور test استفاده شده؟

در فایلهای Compose، نماد $ برای جایگزینی متغیرهای سیستم میزبان (Compose Variable Interpolation) به کار می‌رود. با نوشتن $$ به داکر کامپوز اعلام می‌کنید که متغیر را در سطح هاست Expand نکند ($$ به $ خروجی داده می‌شود) تا شلِ داخل خود کانتینر در زمان اجرای دستور، مقدار متغیر محیطی داخل کانتینر را بخواند.

استفاده از این مکانیزم در تمرین اختیاری است و باعث پایداری و خودترمیمی بیشتر سرویس‌های شما در محیط‌های عملیاتی می‌شود.


گام ۶ — تست، راه‌اندازی و اعتبارسنجی پایداری داده‌ها

۱. حذف ایمیج‌های محلی (تضمین Pull واقعی از GHCR)

برای اینکه مطمئن شوید فایل compose.yaml واقعاً ایمیج را از رجیستری ابری GHCR دانلود می‌کند و به کدهای محلی یا ایمیج کش‌شده روی سیستمتان وابسته نیست، تگ‌های محلی ساخته‌شده را از سیستم خود پاک کنید:

1# حذف تگ‌های محلی ایمیج
2docker rmi ghcr.io/<your-github-username>/techstack-1405-python-app:1.0.0
3docker rmi techstack-1405-python-app:test

اکنون هنگام اجرای داکر کامپوز، لاگ دانلود لایه‌ها (Pulling from ...) را به وضوح در خروجی مشاهده خواهید کرد.

۲. راه‌اندازی سرویس‌ها با Docker Compose

1# راه‌اندازی در پس‌زمینه (دانلود ایمیج‌ها و ایجاد کانتینرها)
2docker compose up -d

۳. بررسی وضعیت و لاگ‌ها

1docker compose ps
2docker compose logs -f web

وضعیت سرویس دیتابیس و وب باید فعال (Up یا healthy) باشد.

۴. تست اندپوینت سلامت

در ترمینال، صحت ارتباط با پایگاه‌داده را با curl بیازمایید:

1curl -i http://localhost:8080/health

خروجی باید کد وضعیت 200 OK و پاسخ JSON زیر باشد:

1{"database":"connected","status":"healthy"}

۵. تست ثبت و شمارش داده‌ها

چند بار اندپوینت اصلی را فراخوانی کنید:

1curl http://localhost:8080/
2curl http://localhost:8080/
3curl http://localhost:8080/

با هر درخواست، مقدار کلید visits باید یک واحد افزایش یابد (مثلاً "visits": 3).

۶. آزمون ماندگاری داده‌ها (Data Persistence Test)

اکنون کانتینرها را به‌طور کامل متوقف کرده و حذف نمایید:

1docker compose down

توجه داشته باشید که دستور بالا کانتینرها را حذف می‌کند اما Named Volume حاوی اطلاعات دیتابیس حفظ می‌شود. دوباره کانتینرها را استارت کنید:

1docker compose up -d

مجدداً اندپوینت را صدا بزنید:

1curl http://localhost:8080/

مقدار visits باید ادامه‌ی ارقام قبلی (مثلاً 4) باشد و از ۱ شروع نشود. این آزمایش تأیید می‌کند که والیوم داکر به درستی کار می‌کند.


گام ۷ — ساختار تحویل تمرین و ارسال Pull Request

مستندات و فایل‌های تمرین را در فورک مخزن دوره‌ی تمرین‌ها روی شاخه‌ی week-5 و درون ساختار پوشه‌ی زیر قرار دهید:

 1<your-github-username>/
 2└── 5/
 3    ├── 01-dockerfile.md
 4    ├── 02-ghcr-publish.md
 5    ├── 03-compose-setup.md
 6    └── assets/
 7        ├── Dockerfile
 8        ├── compose.yaml
 9        ├── .env.example
10        ├── .dockerignore
11        └── app/
12            ├── app.py
13            └── requirements.txt

شرح محتوای فایل‌های Markdown (کوتاه و متنی):

نیازی به گزارش‌نویسی طولانی، ارائه‌ی لاگ‌های مفصل یا اسکرین‌شات نیست؛ کافی است خروجی‌های متنی کوتاه زیر را در فایل‌ها قرار دهید:

  1. 01-dockerfile.md:
    • توضیح کوتاه (چند خط) درباره‌ی دو مرحله‌ی Dockerfile.
    • خروجی متنی دستورهای docker images و docker history برای ایمیج ساخته‌شده.
  2. 02-ghcr-publish.md:
    • آدرس لینک عمومی پکیج شما در GHCR (مثلاً https://github.com/<username>/techstack-1405-python-app/pkgs/container/techstack-1405-python-app).
    • خروجی چند خطی دستور docker push.
  3. 03-compose-setup.md:
    • خروجی متنی دستور docker compose ps.
    • خروجی دستورهای curl برای تست اندپوینت‌های /health و /.
    • خروجی تست ماندگاری بازدیدها پس از اجرای مجدد کانتینرها.

چک‌لیست نهایی تحویل تمرین

پیش از باز کردن Pull Request، موارد زیر را بررسی و تأیید کنید:

  • فایل Dockerfile از تکنیک Multi-stage Build استفاده کرده و حجم آن بهینه شده است.
  • فایل .dockerignore در کنار Dockerfile قرار گرفته و مانع ارسال فایل‌های غیرضروری به کانتینر شده است.
  • (اختیاری) اجرای پروسه‌ی کانتینر با کاربر غیر روت (USER) در فایل Dockerfile پیاده‌سازی شده است.
  • ایمیج به رجیستری GHCR با تگ مشخص (مانند 1.0.0) پوش شده و تنظیم دسترسی آن روی Public قرار دارد.
  • در فایل compose.yaml از image: ghcr.io/... استفاده شده و کلید build: وجود ندارد.
  • پایگاه‌داده PostgreSQL به یک Named Volume متصل است و پورت آن مستقیماً به هاست باز نشده است.
  • فایل .env.example حاوی متغیرهای نمونه در پوشه assets/ قرار داده شده و فایل واقعی .env در گیت کامیت نشده است.
  • (اختیاری) سلامت‌سنجی (healthcheck) دیتابیس در فایل Compose کانفیگ شده و سرویس وب با شرط condition: service_healthy به آن وابسته است.
  • آزمون ماندگاری داده‌ها (شمارش بازدیدها پس از docker compose down) با موفقیت انجام شده است.
  • ساختار دایرکتوری در ریپازیتوری تمرین‌ها دقیقا در مسیر <your-github-username>/5/ ایجاد شده است.
  • شاخه‌ی گیت week-5 آماده و Pull Request ارسال شده است.