المرحلة 5: النشر والإطلاق
النشر في بيئة الإنتاج
لقد بنيت TaskFlow من الصفر -- النماذج والترحيلات ونقاط النهاية والمصادقة والاختبارات. حان الوقت الآن لتعبئته في حاوية جاهزة للإنتاج وإطلاقه.
بناء Docker متعدد المراحل
البناء متعدد المراحل يفصل بيئة البناء (حيث تثبت التبعيات وتجمّع) عن بيئة التشغيل (ما يعمل فعلياً في الإنتاج). هذا ينتج صوراً أصغر وأكثر أماناً.
# المرحلة 1: البناء -- تثبيت التبعيات
FROM python:3.13-slim AS builder
WORKDIR /app
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential libpq-dev \
&& rm -rf /var/lib/apt/lists/*
COPY requirements.txt .
RUN pip install --no-cache-dir --prefix=/install -r requirements.txt
# المرحلة 2: الإنتاج -- بيئة تشغيل خفيفة
FROM python:3.13-slim AS production
RUN apt-get update && apt-get install -y --no-install-recommends \
libpq5 curl \
&& rm -rf /var/lib/apt/lists/*
# مستخدم غير جذري للأمان
RUN groupadd -r taskflow && useradd -r -g taskflow taskflow
WORKDIR /app
# نسخ الحزم المثبتة فقط من مرحلة البناء
COPY /install /usr/local
COPY . .
RUN chown -R taskflow:taskflow /app
USER taskflow
HEALTHCHECK \
CMD curl -f http://localhost:8000/health || exit 1
EXPOSE 8000
CMD ["gunicorn", "app.main:app", "-w", "4", "-k", "uvicorn.workers.UvicornWorker", "--bind", "0.0.0.0:8000"]
المبادئ الأساسية:
| المبدأ | لماذا هو مهم |
|---|---|
| بناء متعدد المراحل | مرحلة البناء تُحذف؛ الصورة النهائية تحتوي فقط على تبعيات التشغيل |
| تثبيت إصدار الصورة الأساسية | python:3.13-slim بدلاً من python:latest يمنع الأعطال المفاجئة |
| مستخدم غير جذري | يحد من الضرر إذا تم اختراق الحاوية |
| HEALTHCHECK | المُنسقات (Compose، ECS، K8s) تعرف متى يكون التطبيق جاهزاً فعلاً |
| ترتيب الطبقات | COPY requirements.txt قبل COPY . لتُخزن طبقات التبعيات مؤقتاً |
Docker Compose للإنتاج
احتفظ بملفات منفصلة للتطوير والإنتاج. التطوير يستخدم إعادة التحميل التلقائي وإعدادات التصحيح؛ الإنتاج يستخدم حدود الموارد وسياسات إعادة التشغيل.
# docker-compose.prod.yml
services:
api:
build:
context: .
dockerfile: Dockerfile
target: production
ports:
- "8000:8000"
env_file: .env.production
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
deploy:
resources:
limits:
cpus: "1.0"
memory: 512M
restart: unless-stopped
networks:
- external
- internal
db:
image: postgres:18
volumes:
- pgdata:/var/lib/postgresql/data
environment:
POSTGRES_DB: taskflow
POSTGRES_USER: taskflow
POSTGRES_PASSWORD: ${DB_PASSWORD}
healthcheck:
test: ["CMD-SHELL", "pg_isready -U taskflow"]
interval: 10s
timeout: 5s
retries: 5
networks:
- internal
redis:
image: redis:8-alpine
command: redis-server --appendonly yes --maxmemory 128mb --maxmemory-policy allkeys-lru
volumes:
- redisdata:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
networks:
- internal
volumes:
pgdata:
redisdata:
networks:
external:
internal:
internal: true
ما الذي يُبعد قاعدة البيانات عن الإنترنت فعلاً
يغري المرء أن يقرأ internal: true على أنها «ما يجعل Postgres خاصاً». وهي ليست كذلك بمفردها — وسوء قراءتها هو كيف تنتهي قاعدة بيانات إلى الإنصات على عنوان IP عام. آليتان مستقلتان تقومان بالعمل في ذلك الملف، وأنت بحاجة إليهما معاً:
آليتان منفصلتان لتهديدين مختلفين
إذن: ports: تقرر من يصل إليك، وinternal: true تقرر إن كانت الحاويات تستطيع الوصول للخارج. وليست إحداهما بديلاً عن الأخرى. والعادة الجديرة بالتكوين أن تقرأ ملف Compose بطرح هذين السؤالين منفصلين على كل خدمة، بدل البحث عن عَلَم واحد معناه «آمن».
إدارة البيئة
لا تُضمّن الأسرار أبداً في الكود. استخدم ملفات .env للتطوير المحلي ومخازن الأسرار المناسبة للإنتاج.
# .env.development (يُرفع للمستودع كـ .env.example بقيم فارغة)
DATABASE_URL=postgresql+asyncpg://taskflow:localpass@localhost:5432/taskflow
REDIS_URL=redis://localhost:6379/0
SECRET_KEY=dev-only-not-for-production
ENVIRONMENT=development
# .env.production (لا يُرفع أبداً -- استخدم أسرار CI/CD)
DATABASE_URL=postgresql+asyncpg://taskflow:${DB_PASSWORD}@db:5432/taskflow
REDIS_URL=redis://redis:6379/0
SECRET_KEY=${SECRET_KEY}
ENVIRONMENT=production
مع Pydantic Settings، يتم التحقق من التكوين عند بدء التشغيل:
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
database_url: str
redis_url: str
secret_key: str
environment: str = "development"
api_v1_prefix: str = "/api/v1"
@property
def is_production(self) -> bool:
return self.environment == "production"
model_config = {"env_file": ".env"}
settings = Settings()
CI/CD مع GitHub Actions
خط أنابيب قياسي: اختبار، بناء، نشر -- يُشغل عند كل دفع إلى main وعند طلبات السحب.
# .github/workflows/ci.yml
name: CI/CD Pipeline
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:18
env:
POSTGRES_DB: taskflow_test
POSTGRES_USER: taskflow
POSTGRES_PASSWORD: testpassword
ports: ["5432:5432"]
options: >-
--health-cmd="pg_isready -U taskflow"
--health-interval=10s
--health-timeout=5s
--health-retries=5
redis:
image: redis:8-alpine
ports: ["6379:6379"]
options: >-
--health-cmd="redis-cli ping"
--health-interval=10s
--health-timeout=5s
--health-retries=5
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.13"
- run: pip install -r requirements.txt
- run: ruff check .
- run: pytest --cov=app tests/
env:
DATABASE_URL: postgresql+asyncpg://taskflow:testpassword@localhost:5432/taskflow_test
REDIS_URL: redis://localhost:6379/0
SECRET_KEY: test-secret-key
build:
needs: test
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: docker build -t taskflow-api:${{ github.sha }} .
قائمة فحص الإنتاج
قبل الإطلاق، تحقق من كل عنصر:
| الفئة | العنصر | الكيفية |
|---|---|---|
| الأمان | مستخدم Docker غير جذري | USER taskflow في Dockerfile |
| الأمان | ترويسات الأمان | Middleware: X-Content-Type-Options، X-Frame-Options، Strict-Transport-Security |
| الأمان | CORS مقيد | أصول مسموحة صريحة، لا تستخدم * أبداً في الإنتاج |
| الأداء | عمال Gunicorn | gunicorn -w 4 -k uvicorn.workers.UvicornWorker |
| الموثوقية | فحوصات الصحة | نقطة /health تُرجع 200 عندما يكون DB وRedis متاحين |
| الموثوقية | سياسة إعادة التشغيل | restart: unless-stopped في Compose |
| الإصدارات | بادئة API | جميع المسارات تحت /api/v1/ |
| الأسرار | لا مفاتيح مُضمنة | جميع الأسرار من متغيرات البيئة أو مدير الأسرار |
اختيار مكان للتشغيل
لأنك بنيت حاوية لا تطبيقاً مرتبطاً بمنصة بعينها، فهذا القرار قابل للتراجع — وهذا معظم سبب استخدام الحاويات أصلاً. أي مستضيف يشغّل صورة OCI سيشغّل TaskFlow.
وبدل جدول من المستويات والأسعار يصير خاطئاً خلال ربع سنة، إليك الأسئلة التي تحسم الأمر فعلاً، مرتبةً بحسب أثرها:
| السؤال | لماذا يحسم الجواب |
|---|---|
| من يدير قاعدة البيانات؟ | قاعدة Postgres مُدارة بنسخ احتياطي آلي واستعادة إلى نقطة زمنية هي أكبر فارق تشغيلي منفرد بين المستضيفين. وتشغيلها بنفسك في حاوية مقبول حتى أول عطل قرص. |
| ما خطتك في الاستعادة؟ | اسأل تحديداً: كيف أستعيد الحالة إلى الثالثة فجر أمس، وهل جرّبت ذلك؟ منصة بنشر بضغطة واحدة وبلا استعادة مُجرَّبة رهانٌ أسوأ من منصة أعقد لكن بنسخ احتياطي تدرّبت عليه. |
| هل تحتاج أكثر من منطقة جغرافية؟ | تعدد المناطق فارق حقيقي بين المستضيفين وتكلفة حقيقية أيضاً. معظم الواجهات لا تحتاجه، ومن يحتاجه يعرف سببه عادةً. |
| هل يفرض شيء شبكة VPC أو حدوداً تنظيمية؟ | هذا ما يدفع الفرق إلى AWS أو GCP أو Azure بغض النظر عن سهولة الاستخدام. وإن انطبق فهو يتقدم على كل اعتبار آخر. |
| كيف يبدو الخروج؟ | إن كان الانتقال يعني إعادة كتابة إعدادات النشر فقط فأنت بخير. أما إن كان يعني إعادة كتابة كود التطبيق فقد ربطت نفسك بالمستضيف. |
من المنصات الشائعة لهذا النوع من التطبيقات: Railway وRender وFly.io وAWS ECS/Fargate. ومستوياتها المجانية ومناطقها وعروض قواعد البيانات المُدارة تتغير بوتيرة تكفي لأن يصير أي رقم مكتوب هنا خاطئاً قبل أن تقرأه — وهذه الروابط هي مصدر الحقيقة. التسعير تحديداً شيء ينبغي أن تفحصه دائماً على صفحة المزوّد نفسه لا في درس تعليمي.
التالي: معمل عملي حيث ستُحوّل TaskFlow إلى حاوية وتُعد خط أنابيب CI/CD الكامل. :::
سجّل الدخول للتقييم