Docker Compose ما بعد بيئة التطوير
ليس Docker Compose حكراً على التطوير. فمع الممارسات الصحيحة، يصبح أداة قوية لتنسيق التطبيقات في بيئة الإنتاج.
بالنسبة لتطبيق مستضاف على خادم واحد (VPS أو جهاز مخصص)، يقدّم Compose توازناً ممتازاً: تُوصَف البنية التحتية كاملةً في بضعة ملفات خاضعة لإدارة الإصدارات، ويتم النشر بأمرين فقط، ولا توجد طبقة تحكّم يجب صيانتها ولا عنقود (cluster) تجب مراقبته. يصبح Kubernetes أو Docker Swarm مناسبين عندما تحتاج إلى توزيع الحمل على عدة أجهزة أو ضمان توافرية عالية على مستوى المضيف، وهذا ليس ما تحتاجه أغلب تطبيقات الأعمال.
يعرض هذا المقال تنظيماً للملفات، وإعدادات إنتاج مشروحة، وإدارة السجلات والأسرار والبيانات، ثم إجراء للنشر وأكثر الأخطاء شيوعاً.
بنية الملفات الموصى بها
├── docker-compose.yml # Base configuration
├── docker-compose.prod.yml # Production overrides
├── docker-compose.dev.yml # Development overrides
├── .env.production # Environment variables
└── nginx/
└── default.conf # Nginx configuration
المبدأ هو التجاوز (override): يصف الملف الأساسي ما هو مشترك بين جميع البيئات (الخدمات وشبكاتها ووحدات التخزين الخاصة بها)، ولا يضيف ملف كل بيئة إلا اختلافاته. في التطوير نركّب الشيفرة المصدرية كوحدة تخزين ونفعّل Xdebug؛ وفي الإنتاج نستخدم الصورة المبنية، ونحدّ من الموارد، ونفعّل إعادة التشغيل التلقائي.
يدمج Compose الملفات بالترتيب الذي تُمرَّر به عبر الخيار -f: القيم البسيطة في الملف الأخير هي التي تسري، بينما تُجمَع القوائم مثل ports أو volumes. ولتجنّب تكرار الخيارات في كل أمر، يمكن تعريف المتغير COMPOSE_FILE على الخادم:
# Explicit file merge
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d
# Or once and for all in the server environment
export COMPOSE_FILE=docker-compose.yml:docker-compose.prod.yml
docker compose up -d
# Check the final configuration after merging
docker compose config
يعرض الأمر docker compose config الإعدادات المطبّقة فعلياً، بما فيها المتغيرات بعد استبدال قيمها. إنه أول ما يجب تنفيذه عندما لا تتصرف خدمة ما كما هو متوقع.
إعدادات الإنتاج
services:
app:
build:
context: .
target: production
restart: always
deploy:
resources:
limits:
memory: 512M
cpus: '0.5'
healthcheck:
test: ["CMD", "php-fpm-healthcheck"]
interval: 30s
timeout: 5s
retries: 3
nginx:
image: nginx:alpine
restart: always
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
depends_on:
app:
condition: service_healthy
redis:
image: redis:7-alpine
restart: always
command: redis-server --maxmemory 256mb --maxmemory-policy allkeys-lru
لكل كتلة في هذه الإعدادات دور محدد:
build.target: production: يبني مرحلة الإنتاج فقط من Dockerfile متعدد المراحل، دون أدوات التطوير. راجع مقال البناء متعدد المراحل في Docker.restart: always: تُعاد تشغيل الحاوية بعد أي تعطّل وعند إعادة تشغيل خدمة Docker. يتصرف البديلunless-stoppedبالطريقة نفسها، إلا أن الحاوية التي أُوقفت يدوياً تبقى متوقفة بعد إعادة تشغيل الخادم.deploy.resources.limits: مع Docker Compose v2 تُطبَّق هذه الحدود حتى خارج Swarm. الحاوية التي تتجاوز حدّ الذاكرة المخصص لها يُنهيها النواة (OOM) بدلاً من أن تُسقط الخادم بأكمله.healthcheck:php-fpm-healthcheckسكربت صغير مفتوح المصدر يُثبَّت داخل الصورة؛ يستعلم صفحة حالة PHP-FPM، والتي يجب إذن تفعيلها عبرpm.status_pathفي إعدادات الـ pool.depends_onمعcondition: service_healthy: لا يبدأ Nginx إلا بعد أن يُعلَن PHP-FPM سليماً، مما يتجنّب أخطاء 502 عند الإقلاع.- Redis محدود بـ 256 ميغابايت مع سياسة
allkeys-lru: عند بلوغ الحد، تُحذف المفاتيح الأقل استخداماً مؤخراً. هذا مناسب لذاكرة cache، لا لتخزين جلسات يجب ألا تختفي.
متغيرات البيئة والأسرار
يجب ألا يُرفع الملف .env.production إلى المستودع أبداً: فهو يعيش على الخادم فقط، بصلاحيات مقيّدة (chmod 600). ويتيح Compose أيضاً جعل متغير ما إلزامياً، بحيث يؤدي نسيانه إلى فشل النشر بدلاً من تشغيل تطبيق بإعدادات خاطئة:
services:
app:
env_file: .env.production
environment:
APP_ENV: prod
DATABASE_URL: ${DATABASE_URL:?DATABASE_URL must be set}
بالنسبة للبيانات الأكثر حساسية (كلمات مرور قواعد البيانات، مفاتيح API)، فضّل أسرار Compose (secrets)، التي تُركَّب كملفات داخل /run/secrets/ بدلاً من كشفها في بيئة العملية. هذا الموضوع مفصّل في مقال تأمين حاويات Docker.
إدارة السجلات
اضبط مشغّل تسجيل (logging driver) مركزياً لتسهيل المراقبة:
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
من دون هذه الخيارات، يحتفظ المشغّل json-file بالسجلات إلى ما لا نهاية، وينتهي الأمر بحاوية كثيرة الكتابة إلى ملء القرص. هنا تحتفظ كل حاوية بثلاثة ملفات كحد أقصى، حجم كل منها 10 ميغابايت. وبدلاً من تكرار هذه الكتلة في كل خدمة، استخدم امتداد YAML ومرساة (anchor):
x-logging: &default-logging
driver: json-file
options:
max-size: "10m"
max-file: "3"
services:
app:
logging: *default-logging
nginx:
logging: *default-logging
يمكنك أيضاً تعريف هذه القيم الافتراضية للمضيف كله في /etc/docker/daemon.json، ثم إعادة تشغيل Docker. ولا تنطبق إلا على الحاويات المُنشأة بعد التغيير:
{
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3"
}
}
أما للتجميع (Loki أو Elasticsearch أو خدمة SaaS)، فاجعل التطبيق يكتب على المخرج القياسي ودع وكيلاً يجمع سجلات الحاويات: هذا أمتن من الكتابة في ملفات داخل الحاوية.
وحدات التخزين والبيانات الدائمة
الحاوية قابلة للاستبدال، أما بياناتها فلا. كل ما يجب أن يصمد بعد docker compose down (قاعدة البيانات، الملفات التي يرفعها المستخدمون) يجب أن يعيش في وحدة تخزين مسمّاة (named volume) أو في مجلد واضح التحديد على المضيف. انتبه: الأمر docker compose down -v يحذف وحدات التخزين المسمّاة الخاصة بالمشروع، وبالتالي البيانات.
وحدة التخزين ليست نسخة احتياطية. بالنسبة لقاعدة البيانات، نفّذ تصديراً منطقياً منتظماً بدلاً من نسخ الملفات الخام لمحرّك قيد التشغيل:
# MySQL export from the container, compressed on the host
docker compose exec -T db sh -c 'mysqldump -u root -p"$MYSQL_ROOT_PASSWORD" --single-transaction app' \
| gzip > backup-$(date +%F).sql.gz
تذكّر بعد ذلك نسخ هذه الملفات إلى خارج الخادم واختبار استعادتها بانتظام.
نشر إصدار جديد
مع صور تُبنى في CI وتُنشر في سجلّ (registry)، يقتصر التحديث على جلب الصور الجديدة وإعادة إنشاء الحاويات التي تغيّرت:
docker compose pull
docker compose up -d --remove-orphans --wait
docker image prune -f
ينتظر الخيار --wait أن تكون الخدمات قيد التشغيل وسليمة قبل أن يعيد التحكم، مما يسمح لسكربت النشر بالفشل بشكل نظيف إذا لم ينجح أحد فحوص السلامة. ويحذف --remove-orphans حاويات الخدمات المحذوفة من الملف. ضع في اعتبارك أن Compose يعيد إنشاء الحاوية بإيقاف القديمة قبل تشغيل الجديدة، لذا يحدث انقطاع قصير. إذا لم يكن ذلك مقبولاً، فأنت بحاجة إلى وكيل عكسي قادر على التبديل بين نسختين، أو إلى أداة تنسيق (orchestrator).
أخطاء شائعة
- الوسم
latestفي الإنتاج: يستحيل معرفة الإصدار الذي يعمل أو العودة إلى إصدار سابق. استخدم وسماً لكل إصدار أو لكل commit. - منافذ منشورة على جميع الواجهات:
"3306:3306"يكشف قاعدة البيانات على الإنترنت، وكثيراً ما تتجاوز قواعد Docker جدار الحماية UFW. لا تنشر أي منفذ للخدمات الداخلية، أو اربطها بـ127.0.0.1. - فحص سلامة غائب أو متساهل جداً: لا فائدة من
condition: service_healthyإذا كان الاختبار يتحقق فقط من وجود العملية. - بناء الصور على خادم الإنتاج: يستهلك ذلك موارده ويجعل عمليات النشر غير قابلة لإعادة الإنتاج. ابنِ في CI، وانشر صوراً.
النقاط الأساسية
- عرّف دائماً
restart: alwaysمن أجل المرونة - حدّ من الموارد باستخدام
deploy.resources - استخدم فحوص السلامة (healthchecks) من أجل التوافرية العالية
- افصل وحدات تخزين البيانات الدائمة
- لا تكشف المنافذ الداخلية أبداً دون داعٍ
- دوّر السجلات وانسخ البيانات احتياطياً خارج الخادم
عندما لا يعود Compose كافياً
يبقى Compose محدوداً بمضيف واحد: لا توزيع تلقائي بين عدة أجهزة، ولا تحديث تدريجي دون انقطاع، ولا استئناف إذا تعطّل الخادم نفسه. إذا كان تطبيقك يحتاج إلى هذه الضمانات، فتلك إشارة للانتقال إلى أداة تنسيق. وحتى ذلك الحين، يبقى خادم بموارد مناسبة يديره Compose حلاً بسيطاً وواضحاً وموثوقاً.