تخيل هذا السيناريو: إعداد Docker Compose يعمل بشكل مثالي على جهازك المحلي. تدفع الكود إلى CI، وفجأة تفشل جميع اختبارات التكامل مع خطأ Connection refused.
حاوية قاعدة البيانات "قيد التشغيل". حاوية API "سليمة". تبدأ عملية الاختبار. ثم لا تستطيع الاتصال بالخدمة التي تحتاجها.
يبدو هذا الفشل عشوائياً حتى تتذكر شيئاً واحداً: شبكات Docker المحلية وشبكات Docker في CI ليست نفس البيئة.
الإعداد المحلي يخدعك
على جهازك، قد تتصل بـ Postgres على localhost:5432.
داخل شبكة Compose، يجب أن تتصل حاوية أخرى عادةً بـ postgres:5432، حيث postgres هو اسم الخدمة.
في CI، قد يكون مشغل الاختبار:
- داخل شبكة Compose
- خارج شبكة Compose على المضيف
- داخل حاوية خدمة CI
- داخل منفذ Docker متداخل
تستخدم هذه الحالات الأربع أسماء مضيف مختلفة.
لهذا السبب يمكن أن يكون رابط الاتصال "صحيحاً" محلياً وخاطئاً في خط الأنابيب.
أولاً، حدد أين تعمل عملية الاختبار
قبل تغيير المنافذ، اسأل سؤالاً واحداً:
هل يعمل أمر الاختبار داخل خدمة Compose أم على مضيف CI؟
إذا كانت الاختبارات تعمل داخل Compose:
DATABASE_URL=postgres://user:pass@postgres:5432/appإذا كانت الاختبارات تعمل على مضيف CI وقامت Compose بنشر المنفذ:
DATABASE_URL=postgres://user:pass@127.0.0.1:5432/appإذا كانت الاختبارات تعمل في حاوية CI منفصلة، فقد لا يعمل أي منهما حتى يتم تكوين شبكة خدمة منصة CI.
يعتمد اسم المضيف الصحيح على مكان وجود مشغل الاختبار. تعمل أسماء الخدمات داخل شبكة Compose. تعمل منافذ localhost المنشورة من المضيف.
لا تثق في depends_on كجاهزية
يمكن لـ depends_on التحكم في ترتيب البدء. لكنه لا يضمن أن Postgres أو Redis أو تطبيقك جاهز لقبول الاتصالات.
النسخة السيئة الشائعة:
services:
api:
depends_on:
- postgresهذا يعني فقط أن حاوية postgres تبدأ قبل api. لا يعني أن الترحيلات تم تشغيلها. لا يعني أن TCP جاهز. لا يعني أن قاعدة البيانات قبلت المصادقة.
استخدم فحوصات الصحة أو سكريبت انتظار صريح.
services:
postgres:
image: postgres:16
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app"]
interval: 5s
timeout: 5s
retries: 12
api:
depends_on:
postgres:
condition: service_healthyهذا لا يحل كل مشكلة منصة CI، لكنه يزيل السباق الأكثر شيوعاً.
تحقق من فئات الفشل الأربعة
عندما أرى Connection refused، أعمل بهذا الترتيب.
فحص TCP ممل لكنه مفيد:
node -e "require('net').connect(5432, process.env.DB_HOST).on('connect', () => { console.log('ok'); process.exit(0) }).on('error', e => { console.error(e.message); process.exit(1) })"إذا فشل هذا، فإن مجموعة اختبار التطبيق الخاص بك ليست الشيء الذي يجب تصحيحه بعد.
استخدم روابط اتصال مختلفة لحدود مختلفة
نمط واحد نظيف هو جعل الحدود صريحة:
DATABASE_URL_INTERNAL=postgres://app:app@postgres:5432/app
DATABASE_URL_HOST=postgres://app:app@127.0.0.1:5432/appثم تختار مهمة CI الخاصة بك الرابط الصحيح بناءً على مكان تشغيل الأمر.
هذا أقل سحرية من محاولة جعل رابط واحد يعمل في كل مكان.
ابقِ الترحيلات منفصلة عن الجاهزية
يمكن أن تكون قاعدة البيانات سليمة قبل أن يكون المخطط جاهزاً.
إذا كان تطبيقك يحتاج إلى ترحيلات، اجعلها خطوة صريحة في خط الأنابيب:
docker compose up -d postgres
docker compose run --rm migrate
docker compose run --rm testأو قم بتشغيل الاختبارات داخل خدمة تنتظر كليهما:
- صحة قاعدة البيانات
- اكتمال الترحيلات
- تحميل البيانات الأولية
وإلا ستحصل على فئة أسوأ من الفشل: أخطاء اختبار متقطعة تبدو كأخطاء تطبيق لكنها في الحقيقة سباقات إعداد.
مخرجات التصحيح التي أريدها في كل فشل CI
لا تفرط في تسريب الأسرار. اطبع شكل البيئة.
المخرجات المفيدة:
- خدمات Docker Compose وحالتها
- سجلات الحاوية للتبعية
- المضيف والمنفذ المحلولين، مع إخفاء كلمة المرور
- أسماء الشبكات
- حالة فحص الصحة
- حالة الترحيل
مثال:
docker compose ps
docker compose logs --tail=80 postgres
docker network lsالهدف هو جعل الفشل التالي قابلاً للتشخيص في محاولة واحدة.
الدرس الإنتاجي
ألم شبكة CI هو معاينة لألم التكامل الإنتاجي.
إذا كانت اختباراتك تعتمد على الأمل، فمن المحتمل أن عمليات النشر الخاصة بك تفعل ذلك أيضاً. اجعل حدود الخدمة صريحة. أضف فحوصات الصحة. افصل الجاهزية عن الترحيلات. سجل الحقائق الصحيحة.
هذه هي الطريقة لتحويل "يعمل على جهازي" إلى شيء يمكن لخط الأنابيب إثباته.
