3.3مثال مسلسل : تطبيق وب + قاعدة بيانات
الأساسي في 30 ثانية
- ثلاث خدمات، وشبكة واحدة : الواجهة api (بناء محلي)، والقاعدة bdd (postgres + مجلد)، وadminer (وحدة تحكم التطوير) — فالواجهة تصل bdd:5432 بالاسم، دون IP ثابت.
- رابط الاتصال يسافر في متغير : DATABASE_URL تقرؤه الواجهة — ولا مضيف ثابت في الكود، فالإعداد يتغير دون مساس الصورة.
- depends_on يرتب، وإعادة المحاولة توثق : فالقاعدة تبدأ أبطأ — والواجهة تعيد محاولة اتصالها عند بدء التشغيل، وإلا فالإطلاق الأول يتحطم.
حالة مصغرة مسلسلة — إدير يربط نموذج الفريق
إدير، ديفوبس مبتدئ في الجزائر، عليه تقديم نموذج API + قاعدة يطلقه كل واحد بأمر واحد : فمحاولته الأولى تضع IP القاعدة ثابتا وتتحطم مرة من اثنتين عند بدء التشغيل. يمر إلى الربط بالأسماء (تصل الواجهة bdd:5432)، والرابط في متغير DATABASE_URL، والمجلد الدائم — ويضيف حلقة إعادة محاولة جهة الواجهة. النتيجة : up -d --build، والواجهة ترد على :3000، و adminer على :8081، والقاعدة لا تُوصَل إلا من الداخل ؛ ثم down فـ up : فالجدول المُنشَأ أمس ما زال موجودا.
هذه الحالة نفسها تعود في الدرس والمخطط والنص الجاهز وتحدي 48 ساعة.
1. ثلاث خدمات وشبكة واحدة وأسماء — لا عناوين IP
التطبيق الحقيقي هو الواجهة api (صورتكم، وبناء محلي من ./api)، والقاعدة bdd (postgres + مجلد مسمى) وadminer (وحدة تحكم وب خفيفة لإدارة SQL، منشورة في التطوير). يربطها كومبوز على الشبكة نفسها : فتصل الواجهة bdd:5432 باسم الخدمة — فالعناوين IP تتغير عند كل إعادة إنشاء، والأسماء تبقى. ولا IP ثابت أبدا في الكود : إنها القاعدة التي تجعل الستاك محمولا.
وتقرأ الواجهة DATABASE_URL في متغير (المضيف = bdd، والقاعدة، والمعرفات) : فيسافر الإعداد، ويبقى الكود متماثلا بين التطوير والإنتاج. وننشر الواجهة (:3000) و adminer (:8081، والتطوير فقط) ؛ أما القاعدة فلا تُنشَر أبدا — فلا تُوصَل إلا من الداخل. سطح معروض أدنى : وحده ما يجب أن يخرج يخرج.
2. الترتيب لا يكفي : إعادة المحاولة واجبة، والديمومة مثبتة
يطلق depends_on القاعدة قبل الواجهة، لكن ليس توفرها : فيحتاج postgres ثواني عدة لقبول الاتصالات. ودون حلقة إعادة محاولة عند بدء التشغيل، تتحطم الواجهة عند الإطلاق الأول ثم « تنجح » في الثاني — العرض الكلاسيكي. فالفرق بين عرض وستاك ينجو من إعادة التشغيل هو إعادة المحاولة هذه مع مهلة جهة التطبيق (وفحص الصحة ينظم زيادة على ذلك، المستوى 4.2).
والبرهان على تماسك كل شيء : down ثم up — فيجب أن ينجو الجدول المُنشَأ عبر adminer، لأنه يعيش في المجلد، لا في الحاوية. ويجعل إدير منه اختبار استلامه المنهجي : فما لا ينجو من إعادة التشغيل ليس دائما، نقطة. فقاعدة دائمة، وغير منشورة أبدا، وإعادة محاولة عند بدء التشغيل : ثلاثي الستاك السليم.
🔎 مثال ملموس — ستاك API + Postgres + Adminer، مربوط ودائم
يكتب إدير كومبوز النموذج : متغيرات ومجلد وشبكة، و adminer اختياري في التطوير.
services:
api:
build: ./api
ports: ["3000:3000"]
environment:
DATABASE_URL: ${DATABASE_URL}
depends_on: [bdd]
networks: [appnet]
bdd:
image: postgres:16.4
environment:
POSTGRES_USER: ${POSTGRES_USER:-app}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB:-appdb}
volumes: [pgdata:/var/lib/postgresql/data]
networks: [appnet]
adminer:
image: adminer:4.8
profiles: [dev]
ports: ["8081:8080"]
networks: [appnet]
volumes:
pgdata:
networks:
appnet:
# الإطلاق : docker compose --profile dev up -d --build
القراءة : الواجهة و adminer منشوران، والقاعدة ليست كذلك — فلا تُوصَل إلا من الداخل.
الدرس : افتحوا :3000 (الواجهة) و :8081 (adminer) ؛ ويثبت down/up نجاة الجدول.
مفردات القسم
- DATABASE_URL
- رابط الاتصال الكامل الممرر في متغير : المضيف = اسم الخدمة، ولا IP ثابت أبدا.
مثال بسيط: واجهة إدير تقرأ ${DATABASE_URL} المشيرة إلى bdd.
ما ليس كذلك: الـ IP الموجود أمس في inspect، منسوخ في الكود.
رد فعل: المضيف = اسم الخدمة، والإعداد = متغير.
- إعادة المحاولة (Retry)
- حلقة إعادة الاتصال عند بدء التشغيل : تعيد الواجهة المحاولة حتى تقبل القاعدة.
مثال بسيط: إعادة المحاولة التي تشفي تحطم بدء التشغيل الأول.
ما ليس كذلك: اتصال وحيد يفشل إذا كانت القاعدة بطيئة.
رد فعل: كل اتصال عند بدء التشغيل = إعادة محاولة مع مهلة.
- Adminer
- وحدة تحكم وب خفيفة لإدارة SQL، منشورة في التطوير لرؤية القاعدة وملئها.
مثال بسيط: ينشئ إدير جدول الاختبار على :8081.
ما ليس كذلك: القاعدة منشورة في الإنتاج « للتنقيح ».
رد فعل: وحدة التحكم في التطوير فقط، ولا في الإنتاج أبدا.
- بناء محلي (Build local)
- خدمة مبنية من مجلد (./api) بدل صورة بعيدة : كودكم مُحوَى.
مثال بسيط: صورة الواجهة المبنية عند up --build.
ما ليس كذلك: كود مركب يدويا في حاوية nginx.
رد فعل: --build بعد كل تغيير كود.
- السطح المعروض
- مجموع المنافذ المنشورة : نقلله — وحده الضروري يخرج.
مثال بسيط: الواجهة + adminer (التطوير) منشوران، و bdd داخلية.
ما ليس كذلك: postgres منشور على المضيف « تحسبا ».
رد فعل: كل منفذ منشور يجب أن يبرر وجوده.
في التطبيق — تركيب التطبيق المصغر المربوط والدائم
- أنشئوا ./api (Dockerfile + خادم يقرأ DATABASE_URL مع إعادة محاولة عند بدء التشغيل).
- اكتبوا كومبوز أعلاه (الرابط في متغير، والقاعدة غير منشورة) واطلقوا up -d --build.
- تحققوا من الواجهة (:3000) و adminer (:8081، والملف الشخصي dev)، وأنشئوا جدولا.
- أعيدوا تشغيل كل شيء (down ثم up) : فيجب أن ينجو الجدول — وإلا فالمجلد مركب خطأ.
نص جاهز للقول — تسليم النموذج للفريق (30 ثانية)
« أمر واحد : up -d --build مع الملف الشخصي dev. ترد الواجهة على 3000، ووحدة تحكم القاعدة على 8081، ولا تُوصَل القاعدة إلا من الداخل. والرابط يسافر في متغير، وإعادة المحاولة تمتص بدء التشغيل، ولا يفقد down/up شيئا — مُختبَر. »
يُقال عند تسليم النموذج ؛ واعرضوا down/up والجدول سليما.
نموذج نسخ-لصق — API + قاعدة + وحدة تحكم (صالح)
services:
api:
build: ./api
ports:
- "${PORT:-3000}:3000"
environment:
DATABASE_URL: ${DATABASE_URL}
depends_on:
- bdd
networks:
- appnet
bdd:
image: postgres:16.4
environment:
POSTGRES_USER: ${POSTGRES_USER:-app}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB:-appdb}
volumes:
- pgdata:/var/lib/postgresql/data
networks:
- appnet
adminer:
image: adminer:4.8
profiles:
- dev
ports:
- "8081:8080"
networks:
- appnet
volumes:
pgdata:
networks:
appnet:
# .env محلي (غير مُلتزَم به) : DATABASE_URL و POSTGRES_PASSWORD
مستقل
الربط بالأسماء + الرابط في متغير ونموذجكم يطلق عند كل عميل.
مقاولة صغيرة في الجزائر
يسلم إدير النموذج بأمر واحد : الواجهة على 3000، ووحدة التحكم على 8081، والقاعدة دائمة.
فريق منتج
توحيد DATABASE_URL + إعادة المحاولة عند بدء التشغيل لكل خدمات الفريق.
مبتدئ
هذا الأسبوع : واجهة + قاعدة مربوطتان بالأسماء، وجدول مُنشَأ، و down/up يثبت الديمومة.
✓ ممارسات جيدة
- صِلوا بأسماء الخدمات، واضبطوا بمتغيرات البيئة.
- أضيفوا إعادة محاولة اتصال عند بدء تشغيل كل خدمة زبونة للقاعدة.
- أبقوا وحدة تحكم الإدارة في التطوير فقط (الملف الشخصي dev)، وسطحا أدنى في الإنتاج.
✗ أخطاء شائعة
- IP ثابت في الكود : فيتغير عند كل إعادة إنشاء.
- الاعتقاد بأن depends_on = جاهز : فدون إعادة محاولة، يتحطم بدء التشغيل الأول.
- نشر القاعدة في الإنتاج : سطح هجوم موسع دون فائدة.
النقاط الأساسية
- شبكة واحدة وأسماء ورابط في متغير : bdd:5432 عبر DATABASE_URL، ولا IP أبدا.
- قاعدة دائمة (مجلد مسمى) وغير منشورة أبدا : داخلية فقط.
- إعادة المحاولة واجبة عند بدء التشغيل : فـ depends_on يرتب، وإعادة المحاولة تنتظر حقا.
- يثبت DOWN/UP الديمومة : فيجب أن ينجو الجدول من إعادة التشغيل.
تحدي 48 ساعة — طبّق غدا
ركبوا واجهة + قاعدة مربوطتين بالأسماء مع إعادة محاولة، وأثبتوا الديمومة بـ down/up أمام زميل.
- خدماتي تتصل بالأسماء عبر متغيرات.
- قاعدتي دائمة وغير منشورة.
- واجهتي تعيد محاولة اتصالها عند بدء التشغيل.
أسئلة شائعة
تتحطم الواجهة عند بدء التشغيل الأول ثم تنجح : طبيعي ؟
نعم، إذا لم تُعِد المحاولة : فتبدأ القاعدة أبطأ من الواجهة (و depends_on يرتب دون انتظار). أضيفوا حلقة إعادة محاولة مع مهلة عند بدء تشغيل الواجهة — فيختفي التحطم. وتكملة المستوى 4 : فحص صحة جهة القاعدة + شرط بدء التشغيل جهة الواجهة للجدولة النظيفة.
أين نضع كلمة السر والرابط حقا ؟
في التطوير : .env غير مُلتزَم به بجانب كومبوز (موثق بـ .env.example وهمي). وفي الإنتاج : سر مُدار (ملف مركب، أو CI، أو منسق). ولا مكشوف أبدا في كومبوز المُلتزَم به، ولا في الصورة. وتبقى القاعدة غير منشورة : فلا يعبر السر إلا الشبكة الداخلية لكومبوز.
يجب أن تصل الواجهة postgres على شبكة كومبوز. أي رابط سليم ؟