ثمانية أخطاء شائعة في Docker عند نشر التطبيقات في الإنتاج، وكيف تسببت في تعطل سيرفراتي، والحلول العملية التي تعلمتها بعد ليالٍ من Debugging المجنون.
كنت أظن أنني أتقن Docker بعد أن شغلت بضعة حاويات على جهازي المحلي، لكنني اكتشفت الحقيقة المرة عندما انتقلت إلى الإنتاج: Docker ليس مجرد أداة لتشغيل حاويات، بل هو نظام معقد يتفاعل مع الذاكرة والـ I/O والـ Network بطرق لا تتوقعها. في أول أسبوع من إطلاق خدمتي الجديدة، تعطل السيرفر ثلاث مرات بسبب خطأ بسيط في إعداد الـ Memory Limits، وكل مرة كان الـ Load Average يقفز إلى ٥٠+ قبل أن ينهار النظام بالكامل. المشكلة لم تكن في الكود، بل في فهمي الخاطئ لكيفية عمل Docker خلف الكواليس.
في هذا المقال، سأشارككم الأخطاء التي ارتكبتها في الإنتاج، وكيف تسببت في كوارث حقيقية، والحلول التي تعلمتها بالطريقة الصعبة. لن نتحدث عن الأساسيات، بل عن التفاصيل القذرة التي لا تجدها في الوثائق الرسمية: كيف يتصرف Docker عندما ينفذ الذاكرة؟ لماذا تتسرب الـ Network Connections؟ وكيف يمكن أن يؤدي خطأ بسيط في الـ Logging إلى إيقاف الخدمة بالكامل؟
عندما قمت بتشغيل أول حاوية في الإنتاج، لم أضع أي حدود للذاكرة، ظناً مني أن النظام سيوزع الموارد بشكل ذكي. لكن الحقيقة هي أن Linux لا يعرف أن هذه العملية جزء من حاوية Docker، بل يراها كأي عملية أخرى. وعندما بدأت الحاوية تستهلك ذاكرة أكثر مما يتحمله السيرفر، تدخل الـ OOM Killer (Out Of Memory Killer) وقتل العملية بشكل عشوائي، مما تسبب في انهيار الخدمة بالكامل. المشكلة الأكبر أن Docker لا يعيد تشغيل الحاوية تلقائياً في هذه الحالة، بل يتركها في حالة Exit(137).
الحل؟ يجب دائماً تحديد حدود للذاكرة باستخدام `--memory` و `--memory-swap`. لكن حتى هذا ليس كافياً، لأنك تحتاج أيضاً إلى ضبط الـ `oom_score_adj` لمنع الـ OOM Killer من قتل العمليات الحرجة. مثلاً، إذا كان لديك خدمة تعتمد على قاعدة بيانات داخل الحاوية، يمكنك تعديل الـ OOM Score لتقليل احتمالية قتلها:
# تشغيل حاوية مع حدود ذاكرة وضبط OOM Score
sudo docker run -d \
--name my_service \
--memory="512m" \
--memory-swap="1g" \
--oom-score-adj="-500" \
my_image:latestلكن حتى مع هذه الإعدادات، هناك مشكلة أخرى: إذا كانت الحاوية تستهلك ذاكرة قريبة من الحد المحدد، قد تبدأ في الـ Swapping، مما يؤدي إلى تدهور الأداء بشكل كبير. الحل الأمثل هو مراقبة استهلاك الذاكرة باستخدام أدوات مثل `docker stats` أو `cAdvisor`، وضبط الحدود بناءً على البيانات الحقيقية، وليس على التخمينات.
في بيئة التطوير، كنت أستخدم دائماً `latest` tag لسهولة التحديث، لكنني لم أدرك خطورة ذلك في الإنتاج. عندما قمت بسحب صورة جديدة باستخدام `docker pull my_image:latest`، لم أكن أعلم أن Docker لا يضمن أن هذه الصورة هي نفسها التي استخدمتها في التطوير. في إحدى المرات، تم تحديث الصورة الأساسية (Base Image) بشكل غير متوقع، مما أدى إلى فشل الخدمة بسبب تغيير في تبعيات النظام. المشكلة الأكبر أن Docker لا يحتفظ بسجل للتغييرات في `latest` tag، مما يجعل الـ Debugging شبه مستحيل.
الحل؟ يجب دائماً استخدام إصدارات محددة في الإنتاج، مثل `my_image:v1.2.3`. لكن حتى هذا ليس كافياً، لأنك تحتاج أيضاً إلى التحقق من سلامة الصورة باستخدام الـ `docker image inspect` والتأكد من أن الـ Digest متطابق مع ما استخدمته في التطوير. بالإضافة إلى ذلك، يمكنك استخدام أدوات مثل `Trivy` لفحص الصور بحثاً عن ثغرات أمنية قبل نشرها:
# فحص صورة Docker بحثاً عن ثغرات
trivy image my_image:v1.2.3لكن حتى مع هذه الإجراءات، هناك مشكلة أخرى: إذا كنت تعتمد على صور خارجية (مثل `nginx` أو `postgres`)، فقد تتغير هذه الصور دون إشعار. الحل هو استخدام صور محددة بإصدار ثابت، مثل `nginx:1.25.3-alpine` بدلاً من `nginx:latest`.
عندما قمت بنشر خدمتي على عدة نودات في بيئة Kubernetes، واجهت مشكلة غريبة: بعض الطلبات كانت تستغرق وقتاً أطول بكثير من المتوقع، رغم أن الـ Load Balancer كان يوزع الحمل بشكل متساوٍ. بعد ساعات من الـ Debugging، اكتشفت أن المشكلة كانت في الـ Network Latency بين النودات. Docker يستخدم افتراضياً `bridge` network، وهو غير مناسب للبيئات متعددة النودات لأنه يضيف طبقة إضافية من الـ Overhead. المشكلة الأكبر أن Docker لا يعرض هذه المعلومات بشكل واضح، مما يجعل الـ Troubleshooting صعباً للغاية.
الحل؟ يجب استخدام `overlay` network في البيئات متعددة النودات، لأنه مصمم لتقليل الـ Latency بين الحاويات على نودات مختلفة. لكن حتى هذا ليس كافياً، لأنك تحتاج أيضاً إلى ضبط الـ MTU (Maximum Transmission Unit) ليتناسب مع بيئتك. مثلاً، إذا كنت تستخدم AWS، يمكنك ضبط الـ MTU إلى 1500:
# إنشاء overlay network مع ضبط MTU
sudo docker network create \
--driver=overlay \
--opt com.docker.network.driver.mtu=1500 \
my_overlay_networkلكن حتى مع هذه الإعدادات، هناك مشكلة أخرى: إذا كانت الحاويات تتواصل عبر خدمات خارجية (مثل قواعد البيانات)، فقد يكون الـ Latency خارج نطاق سيطرتك. الحل هو استخدام أدوات مثل `tcptraceroute` لقياس الـ Latency بين النودات، وضبط الـ Timeouts في تطبيقك بناءً على البيانات الحقيقية.
في إحدى الليالي، تلقيت تنبيهاً من نظام المراقبة بأن أحد السيرفرات توقف عن الاستجابة. عندما دخلت إلى الـ Dashboard، وجدت أن القرص الصلب ممتلئ بالكامل. بعد التحقيق، اكتشفت أن المشكلة كانت في الـ Logging: كانت الحاويات تنتج كميات هائلة من السجلات، ولم أكن أتحكم في حجمها أو دورانها. Docker يخزن السجلات افتراضياً في ملفات على القرص، وإذا لم يتم إدارتها بشكل صحيح، يمكن أن تملأ القرص بالكامل وتوقف الخدمة.
الحل؟ يجب دائماً تحديد حدود للـ Logging باستخدام `--log-opt max-size` و `--log-opt max-file`. مثلاً، يمكنك تحديد الحد الأقصى لحجم السجل بـ 10 ميجابايت، والاحتفاظ بثلاثة ملفات فقط:
# تشغيل حاوية مع ضبط حدود Logging
sudo docker run -d \
--name my_service \
--log-driver=json-file \
--log-opt max-size=10m \
--log-opt max-file=3 \
my_image:latestلكن حتى مع هذه الإعدادات، هناك مشكلة أخرى: إذا كنت تستخدم خدمات خارجية للـ Logging (مثل ELK أو Datadog)، فقد تضيف هذه الخدمات حملاً إضافياً على الشبكة. الحل هو استخدام الـ `logrotate` لإدارة السجلات المحلية، وتوجيه السجلات الحرجة فقط إلى الخدمات الخارجية. بالإضافة إلى ذلك، يمكنك استخدام أدوات مثل `Fluentd` لفلترة السجلات قبل إرسالها، مما يقلل من حجم البيانات المرسلة.
عندما قمت بتوسيع خدمتي لتشغيلها على عدة نودات، واجهت مشكلة في مزامنة البيانات بين الحاويات. كنت أستخدم الـ Volumes المحلية لتخزين البيانات، لكن هذه الطريقة لا تعمل في البيئات الموزعة لأن كل نودة لديها نسختها الخاصة من البيانات. المشكلة الأكبر أن Docker لا يدعم مزامنة البيانات بين النودات بشكل تلقائي، مما يعني أنك بحاجة إلى حل خارجي.
الحل؟ يجب استخدام حلول تخزين موزعة مثل `NFS` أو `Ceph` أو خدمات السحابة مثل `AWS EFS`. مثلاً، يمكنك استخدام `NFS` لمشاركة مجلد بين عدة نودات:
# تركيب NFS على النودات
sudo apt-get install -y nfs-common
# إنشاء مجلد مشترك
sudo mkdir -p /mnt/nfs_share
sudo mount -t nfs <NFS_SERVER_IP>:/path/to/share /mnt/nfs_share
# تشغيل حاوية مع استخدام المجلد المشترك
sudo docker run -d \
--name my_service \
-v /mnt/nfs_share:/data \
my_image:latestلكن حتى مع هذه الحلول، هناك مشكلة أخرى: إذا كانت البيانات حساسة، قد تحتاج إلى تشفيرها قبل تخزينها. الحل هو استخدام أدوات مثل `Vault` لإدارة المفاتيح، وتشفير البيانات قبل كتابتها على القرص. بالإضافة إلى ذلك، يجب مراقبة أداء الـ Volumes الموزعة باستمرار، لأن أي تأخير في الوصول إلى البيانات يمكن أن يؤثر على أداء الخدمة بالكامل.
في إحدى المرات، توقف أحد الخدمات عن العمل دون أن يرسل أي تنبيهات. عندما دخلت إلى الـ Dashboard، وجدت أن الحاوية كانت لا تزال تعمل، لكن التطبيق داخلها كان معطلاً. المشكلة كانت في الـ Health Checks: كنت أستخدم افتراضياً الـ `docker ps` للتحقق من حالة الحاوية، لكن هذا لا يضمن أن التطبيق داخلها يعمل بشكل صحيح. Docker يدعم الـ Health Checks المخصصة، لكن الكثير من المطورين يتجاهلونها لأنها تتطلب جهداً إضافياً.
الحل؟ يجب دائماً تحديد الـ Health Checks في الـ Dockerfile باستخدام `HEALTHCHECK`. مثلاً، يمكنك التحقق من أن خدمة الويب تستجيب على منفذ معين:
FROM nginx:alpine
# إضافة Health Check
HEALTHCHECK --interval=30s --timeout=3s \
CMD curl -f http://localhost/ || exit 1لكن حتى مع هذه الإعدادات، هناك مشكلة أخرى: إذا كان التطبيق يعتمد على خدمات خارجية (مثل قواعد البيانات)، فقد يفشل الـ Health Check حتى لو كان التطبيق نفسه يعمل بشكل صحيح. الحل هو تصميم الـ Health Checks لتكون بسيطة وسريعة، وعدم الاعتماد عليها للتحقق من الخدمات الخارجية. بدلاً من ذلك، يمكنك استخدام أدوات مثل `Prometheus` لمراقبة الخدمات الخارجية بشكل منفصل.
إذا كنت تريد نشر Docker في الإنتاج دون بكاء، فاتبع هذه القواعد الذهبية: أولاً، دائماً حدد حدوداً للذاكرة والـ CPU، ولا تعتمد على النظام ليقوم بذلك نيابةً عنك. ثانياً، لا تستخدم أبداً `latest` tag في الإنتاج، بل استخدم إصدارات محددة وقم بفحص الصور قبل نشرها. ثالثاً، استخدم `overlay` network في البيئات متعددة النودات، وقم بضبط الـ MTU ليتناسب مع بيئتك. رابعاً، تحكم في الـ Logging باستخدام `--log-opt`، ولا تدع السجلات تملأ القرص. خامساً، استخدم حلول تخزين موزعة مثل `NFS` أو `EFS` بدلاً من الـ Volumes المحلية. وأخيراً، دائماً استخدم الـ Health Checks المخصصة، ولا تعتمد على حالة الحاوية فقط.
Docker أداة قوية، لكنها ليست سحرية. إذا لم تفهم كيف تعمل خلف الكواليس، ستجد نفسك تقضي ليالٍ طويلة في محاولة إصلاح أخطاء كان من الممكن تجنبها بسهولة. تعلمت هذه الدروس بالطريقة الصعبة، وآمل أن تساعدك على تجنب نفس الأخطاء.