الوحدة 3 · بيئات محلية مع Docker Compose
ملفات env والـ Overrides ووضع Watch
20 دقراءة 4 داملأ الفراغات
الدرس في لمحة
انقل إعدادات notes-api إلى ملفات env وأسرار Compose، وافصل إعدادات التطوير وحدها في ملف override وفي profiles، واحصل على حلقة تعديل سريعة بـ docker compose watch.
لا فيديو هنا، بل الشيء الحقيقي: درس مكتوب وأمثلة قابلة للتشغيل واختبار وتدريب.
ملف Compose الخاص بـ notes-api يعمل الآن، وفيه ثلاث مشاكل تصطدم بها الفرق في الأسبوع الثاني. كلمة مرور قاعدة البيانات مكتوبة نصًّا صريحًا في ملف محفوظ في المستودع. وإعدادات التطوير وحدها، مثل منافذ التشخيص، مختلطة بما يحتاج إليه الـ CI. وكل تغيير في الكود يعني docker compose up --build وانتظار ثلاثين ثانية. هذا الدرس يصلح الثلاث.
نوعان من البيئة
لدى Compose آليتان منفصلتان تتعلّق كلتاهما بـ «env»، والخلط بينهما سبب معظم الالتباس في الإعدادات.
الاستبدال (interpolation) يملأ العناصر النائبة ${VAR} في ملف Compose نفسه. القيم تأتي من الـ shell لديك، ثم من ملف .env في مجلد المشروع:
POSTGRES_PASSWORD=change-me-locally
API_PORT=8080services:
api:
ports:
- "${API_PORT:-8080}:3000"
db:
environment:
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD in .env}الصيغة ${API_PORT:-8080} تستخدم 8080 قيمةً احتياطية إن لم يكن المتغيّر مضبوطًا. والصيغة ${POSTGRES_PASSWORD:?…} تجعل القيمة مطلوبة: إن كانت مفقودة، يتوقّف Compose برسالتك بدلًا من تشغيل Postgres بكلمة مرور فارغة.
بيئة الـ container هي ما تراه العملية في الداخل. وتأتي من environment: وenv_file: في الخدمة:
api:
env_file: .env.api
environment:
DATABASE_URL: postgres://notes:${POSTGRES_PASSWORD}@db:5432/notesملف .env الخاص بالمشروع لا يُمرَّر إلى الـ containers من تلقاء نفسه. إذا كان يجب أن تصل قيمة إلى التطبيق، فأشر إليها تحت environment: أو اذكر الملف تحت env_file:. وحين يأتي المتغيّر نفسه من المصدرين، تفوز environment:.
في الاستبدال، المتغيّر المضبوط في الـ shell يتغلّب على نظيره في .env، لذا فإن API_PORT=9090 docker compose up -d تجاوز سريع لمرة واحدة دون تعديل أي ملف. ولاستخدام ملف مختلف كليًا، مرّر --env-file .env.staging. أبقِ عدد هذه الملفات قليلًا؛ فثلاثة ملفات env متشابهة بفروق صغيرة كفيلة بأن تجعل الفريق يشخّص الإعدادات بدلًا من الكود.
احفظ في المستودع ملف .env.example بقيم وهمية، وأضف .env إلى .gitignore. ينسخ الزملاء الجدد ملف المثال ويملؤونه.
أسرار Compose
متغيّرات البيئة مرئية في docker inspect ولكل عملية داخل الـ container. لكلمات المرور، يستطيع Compose تركيب ملف بدلًا من ذلك:
services:
db:
image: postgres:17
environment:
POSTGRES_PASSWORD_FILE: /run/secrets/db_password
secrets:
- db_password
secrets:
db_password:
file: ./secrets/db_password.txtيظهر السرّ داخل الـ container في /run/secrets/db_password. تقرأ image Postgres المتغيّر POSTGRES_PASSWORD_FILE، وكثير من الـ images الرسمية تدعم عرف _FILE نفسه. أما في تطبيقك، فاقرأ الملف عند البدء. الـ Kubernetes Secrets المركّبة كملفات تعمل بالطريقة نفسها، فتنتقل العادة معك.
ملف أساسي مع override
يدمج Compose تلقائيًا compose.override.yaml فوق compose.yaml إن وُجد. استخدم هذا الفصل عن قصد:
compose.yaml: ما تحتاج إليه كل بيئة، بما فيها الـ CI.compose.override.yaml: وسائل الراحة للمطوّرين، مثل منافذ قاعدة البيانات المنشورة، وخيارات التشخيص، وقواعد المراقبة.
يستطيع الـ CI عندها التشغيل بالملف الأساسي وحده: docker compose -f compose.yaml up -d --wait. حين تمرّر -f، لا يُحمَّل الـ override تلقائيًا.
الـ Profiles تغطّي الخدمات الاختيارية. container pgAdmin لا يريده إلا بعض الناس يحصل على profiles: [tools]، ولا يبدأ إلا مع docker compose --profile tools up.
وضع Watch: حلقة تعديل سريعة
إعادة بناء الـ image مع كل تعديل بطيئة. وربط المشروع كاملًا بـ bind mount هشّ: node_modules على المضيف (المبني لـ macOS) يحجب نظيره في الـ container (المبني لـ Linux)، والوصول إلى الملفات عبر آلة Docker Desktop الافتراضية بطيء. Compose watch يحلّ المشكلتين بمزامنة الملفات التي تختارها فقط إلى الـ container الذي يعمل.
services:
api:
build:
context: .
target: build
command: ["npm", "run", "dev"]
develop:
watch:
- action: sync
path: ./src
target: /app/src
- action: rebuild
path: package.json
- action: rebuild
path: package-lock.jsonللتطوير، يبني الـ override مرحلة build (التي تحتوي على اعتماديات التطوير) ويشغّل npm run dev، وهو في notes-api tsx watch src/server.ts، أداة لتشغيل TypeScript تعيد التشغيل عند التغييرات. ثم:
docker compose up --watchعدّل src/routes.ts فيُنسخ الملف إلى /app/src خلال ثانية؛ ويعيد tsx تشغيل الخادم. غيّر package-lock.json فيعيد Compose بناء الـ image ويستبدل الـ container، لأن الاعتماديات الجديدة تحتاج إلى تثبيت حقيقي. وهناك إجراء ثالث، sync+restart، يزامن الملفات ويعيد تشغيل الـ container، ويناسب ملفات الإعدادات التي لا يقرؤها التطبيق إلا عند البدء. والأمر docker compose watch يفعل ما يفعله up --watch مع مخرجات أهدأ.
اكتمل القسم الثالث: بيئة محلية قابلة للتكرار، ببيانات دائمة، وإقلاع مرتّب، وإعدادات آمنة، وحلقة تعديل سريعة. في القسم التالي ستأخذ الـ image نفسها إلى Kubernetes، بدءًا بطريقة تفكير Kubernetes.
أهم النقاط
- ملف
.envالخاص بالمشروع يغذّي استبدال${VAR}داخلcompose.yaml؛ بينماenv_file:وenvironment:تضبطان المتغيّرات داخل الـ containers. إنهما آليتان مختلفتان. - الصيغة
${VAR:?message}تجعل Compose يفشل سريعًا برسالة واضحة حين تكون قيمة مطلوبة مفقودة. - الأمر
docker compose configيطبع الملف بعد الدمج والاستبدال الكاملين، وهو أسرع طريقة لتشخيص الإعدادات. - الملف
compose.override.yamlيُدمج تلقائيًا، لذا أبقِ الإعدادات المشتركة فيcompose.yamlوإعدادات التطوير وحدها في الـ override. - الخيار
develop.watchمعsyncينسخ الملفات المعدّلة إلى الـ container الذي يعمل، ومعrebuildيعيد بناء الـ image حين تتغيّر الاعتماديات.