نشر Docker — الحاويات والبناء متعدد المراحل
Docker مثل حاوية شحن — فهو يُغلف التطبيقات وجميع تبعياتها في حاويات قياسية يمكن تفريغها وتشغيلها مباشرة في أي منفذ (خادم)، مما يُزيل مشكلة "يعمل على جهازي".
1. ما ستتعلمه
- Dockerfile متعدد المراحل: مرحلة البناء (تثبيت تبعيات UV) مقابل مرحلة التشغيل (تبسيط الصورة)
- تنسيق Docker Compose: مكدس كامل من FastAPI + PostgreSQL + Redis + Celery Worker
- متغيرات البيئة وإدارة الأسرار: ملفات
.envوأسرار Docker - فحص الصحة: أمر
HEALTHCHECKونقطة نهاية FastAPI/health - سيناريو Alice: يُطلق Charlie خدمة PriceTracker الكاملة بنقرة واحدة
docker compose up
2. القصة الحقيقية لـ Alice
(1) نقطة الألم: فشل النشر بسبب اختلافات البيئة
عندما نشر Charlie تطبيق PriceTracker — الذي كان قد إعداده محليًا — على خادم الإنتاج، واجه مشاكل مثل عدم تطابق إصدار Python، وغياب مكتبة libpq، وعدم تثبيت UV، واختلاف إعدادات اتصال PostgreSQL — مما أجبره على قضاء ساعتين في استكشاف الأخطاء يدويًا في كل نشر. وازداد الأمر سوءًا أن الخدمات الأربع — FastAPI و PostgreSQL و Redis و Celery — كان يجب بدؤها وإدارتها بشكل منفصل، وكان ترتيب بدء التشغيل معقدًا ومترابطًا.
(2) حل Docker Compose
يُعرّف Dockerfile حاويات التطبيق، بينما يُنسق Docker Compose جميع الخدمات وتبعياتها. أمر واحد docker compose up يُطلق مكدس الخدمة بالكامل، وتضمن الصورة تناسق البيئة.
(3) العائد
انخفض وقت النشر من عملية يدوية مدتها ساعتان إلى 30 ثانية بنقرة واحدة، وأصبحت بيئة التطوير المحلية مطابقة للإنتاج، لذا لن يضطر Charlie لقول "إنه يعمل على خادمي" مرة أخرى.
3. Dockerfile متعدد المراحل
(1) عملية البناء
flowchart LR
A[مرحلة البناء] -->|نسخ التبعيات المثبتة| B[مرحلة التشغيل]
subgraph Builder
A1[تثبيت تبعيات UV] --> A2[تجميع الحزم]
end
subgraph Runtime
B1[صورة Python خفيفة] --> B2[نسخ كود التطبيق]
B2 --> B3[نسخ التبعيات من البناء]
B3 --> B4[تشغيل uvicorn]
end
(1) ▶مثال: Dockerfile متعدد المراحل
# === المرحلة 1: البناء ===
FROM ghcr.io/astral-sh/uv:python3.12-bookworm-slim AS builder
WORKDIR /app
# نسخ ملفات التبعيات أولاً (طبقة التخزين المؤقت)
COPY pyproject.toml uv.lock ./
# تثبيت التبعيات في البيئة الافتراضية
RUN uv sync --frozen --no-dev --no-install-project
# نسخ كود التطبيق
COPY app/ app/
# === المرحلة 2: التشغيل ===
FROM python:3.12-slim-bookworm AS runtime
WORKDIR /app
# تثبيت تبعيات النظام للتشغيل
RUN apt-get update && \
apt-get install -y --no-install-recommends libpq5 && \
rm -rf /var/lib/apt/lists/*
# نسخ البيئة الافتراضية من مرحلة البناء
COPY --from=builder /app/.venv /app/.venv
# نسخ كود التطبيق
COPY app/ app/
# تعيين متغيرات البيئة
ENV PATH="/app/.venv/bin:$PATH" \
PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1
# فحص الصحة
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"
# تشغيل التطبيق
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "4"]
الناتج:
// التنفيذ ناجح
(2) مقارنة أحجام الصور
| الطريقة | حجم الصورة | الوصف |
|---|---|---|
| مرحلة واحدة (Python 3.12) | ~1.2 جيجابايت | يتضمن أدوات البناء والتخزين المؤقت |
| مرحلتين (Python: 3.12-slim) | ~200 ميجابايت | تبعيات التشغيل فقط |
| Alpine (python:3.12-alpine) | ~80 ميجابايت | أصغر لكن بها مشاكل توافق |
4. تنسيق Docker Compose
(1) بنية المكدس الكامل
flowchart TD
Nginx[Nginx Proxy عكسي] --> API1[FastAPI Worker 1]
Nginx --> API2[FastAPI Worker 2]
API1 --> PG[(PostgreSQL)]
API2 --> PG
API1 --> Redis[(Redis)]
API2 --> Redis
API1 --> Broker[Redis Broker]
API2 --> Broker
Broker --> CW1[Celery Worker 1]
Broker --> CW2[Celery Worker 2]
CW1 --> PG
CW2 --> PG
CW1 --> Redis
CW2 --> Redis
Flower[Flower Monitor] --> Broker
(1) ▶مثال: docker-compose.yml
version: "3.8"
services:
api:
build:
context: .
dockerfile: docker/Dockerfile
ports:
- "8000:8000"
environment:
- DATABASE_URL=postgresql+asyncpg://pricetracker:${DB_PASSWORD}@postgres:5432/pricetracker
- REDIS_URL=redis://redis:6379/0
- CELERY_BROKER_URL=redis://redis:6379/1
- SECRET_KEY=${SECRET_KEY}
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
healthcheck:
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"]
interval: 30s
timeout: 10s
retries: 3
postgres:
image: postgres:16-alpine
environment:
POSTGRES_USER: pricetracker
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_DB: pricetracker
volumes:
- postgres_data:/var/lib/postgresql/data
ports:
- "5432:5432"
healthcheck:
test: ["CMD-SHELL", "pg_isready -U pricetracker"]
interval: 10s
timeout: 5s
retries: 5
redis:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
- redis_data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
celery-worker:
build:
context: .
dockerfile: docker/Dockerfile
command: celery -A app.core.celery_app worker --loglevel=info --concurrency=4
environment:
- DATABASE_URL=postgresql+asyncpg://pricetracker:${DB_PASSWORD}@postgres:5432/pricetracker
- REDIS_URL=redis://redis:6379/0
- CELERY_BROKER_URL=redis://redis:6379/1
depends_on:
- redis
- postgres
flower:
build:
context: .
dockerfile: docker/Dockerfile
command: celery -A app.core.celery_app flower --port=5555
ports:
- "5555:5555"
depends_on:
- redis
volumes:
postgres_data:
redis_data:
الناتج:
CONTAINER ID IMAGE STATUS PORTS
abc123 nginx:latest Up 2 hours 0.0.0.0:80->80/tcp
(2) ▶مثال: ملف .env
# .env - يقرأه Docker Compose تلقائيًا
DB_PASSWORD=change-me-in-production
SECRET_KEY=your-very-long-random-secret-key-at-least-32-chars
CELERY_BROKER_URL=redis://redis:6379/1
الناتج:
// التنفيذ ناجح
5. فحوصات الصحة وترتيب الإقلاع
(1) depends_on + healthcheck
يضمن depends_on + condition: service_healthy في Docker Compose ترتيب بدء تشغيل الخدمات الصحيح: تصبح PostgreSQL و Redis جاهزتين أولاً، ثم يبدأ FastAPI.
| الخدمات | ترتيب البدء | فحوصات الصحة |
|---|---|---|
| PostgreSQL | 1 (الأول) | pg_isready -U pricetracker |
| Redis | 1 (الأول) | redis-cli ping |
| FastAPI | 2 (بعد جاهزية PG+Redis) | GET /health |
| Celery Worker | 3 (بعد جاهزية Redis و PostgreSQL) | نبض داخلي |
(1) ▶مثال: نقطة نهاية فحص صحة FastAPI
@app.get("/health")
async def health_check(db: AsyncSession = Depends(get_db), redis: Redis = Depends(get_redis)):
# فحص اتصال قاعدة البيانات
try:
await db.execute(select(1))
db_status = "healthy"
except Exception:
db_status = "unhealthy"
# فحص اتصال Redis
try:
await redis.ping()
redis_status = "healthy"
except Exception:
redis_status = "unhealthy"
overall = "healthy" if db_status == "healthy" and redis_status == "healthy" else "unhealthy"
return {
"status": overall,
"database": db_status,
"redis": redis_status,
}
الناتج:
# تم تعريف الدالة بنجاح
❓أسئلة شائعة
entrypoint.sh، شغّل alembic upgrade head أولاً قبل بدء uvicorn..env (أضفها إلى .gitignore). للإنتاج، استخدم Docker secrets أو K8s Secrets؛ لا تُصلبها برمجيًا ولا تُرسلها إلى Git.(2 x أنوية المعالج) + 1. لخادم بـ 4 أنوية، عيّن 9 Workers. لكن يجب أيضًا مراعاة حدود الذاكرة؛ كل Worker يستخدم 50-100 ميجابايت.docker compose logs api لعرض سجلات FastAPI، docker compose logs -f للمراقبة في الوقت الفعلي. في بيئات الإنتاج، استخدم ELK/Loki لتجميع السجلات.📖ملخص
- Dockerfile متعدد المراحل: تُثبت التبعيات خلال مرحلة Builder، ويُنسخ فقط الناتج خلال مرحلة Runtime، مما يُقلل حجم الصورة من 1.2 جيجابايت إلى 200 ميجابايت
- تنسيق المكدس الكامل مع Docker Compose: API + PostgreSQL + Redis + Celery Worker + Flower
- يضمن
depends_on+healthcheckتسلسل البدء التالي: تصبح قاعدة البيانات جاهزة أولاً، تليها API - ملف
.envلإدارة متغيرات البيئة؛ استخدم Docker secrets في بيئة الإنتاج - يتحقق فحص الصحة من حالة اتصال نقطة النهاية لقاعدة البيانات و Redis؛ يقوم أمر
HEALTHCHECKبالكشف التلقائي
📝تمارين
- سؤال أساسي (الصعوبة ⭐): اكتب Dockerfile بمرحلة واحدة لـ PriceTracker، ابنِ الصورة، شغّل الحاوية، وتحقق من إمكانية الوصول إلى نقطة النهاية
/health. تلميح:FROM python:3.12-slim+CMD ["uvicorn", ...] - تمرين متقدم (الصعوبة ⭐⭐): عدّل Dockerfile لاستخدام بناء متعدد المراحل، واكتب ملف
docker-compose.ymlيتضمن ثلاث خدمات: API و PostgreSQL و Redis، بحيث يمكن بدؤها بأمر واحد:docker compose up. تلميح:AS builder+COPY --from=builder - تحدٍ (الصعوبة ⭐⭐⭐): أكمل تنسيق Docker Compose — أضف خدمتي Celery Worker و Flower، أضف فحص صحة، عيّن كلمة مرور لملف
.env، وأضفentrypoint.shلتشغيلalembic upgrade headقبل البدء. تلميح:depends_on+condition: service_healthy+ سكريبت entrypoint
---|



