{"id":"l-dk-3-4","courseId":"docker-kubernetes","lang":"ar","kind":"lesson","summary":"انقل إعدادات notes-api إلى ملفات env وأسرار Compose، وافصل إعدادات التطوير وحدها في ملف override وفي profiles، واحصل على حلقة تعديل سريعة بـ <code dir=\"ltr\">docker compose watch</code>.","html":"<p>ملف Compose الخاص بـ notes-api يعمل الآن، وفيه ثلاث مشاكل تصطدم بها الفرق في الأسبوع الثاني. كلمة مرور قاعدة البيانات مكتوبة نصًّا صريحًا في ملف محفوظ في المستودع. وإعدادات التطوير وحدها، مثل منافذ التشخيص، مختلطة بما يحتاج إليه الـ CI. وكل تغيير في الكود يعني <code dir=\"ltr\">docker compose up --build</code> وانتظار ثلاثين ثانية. هذا الدرس يصلح الثلاث.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">نوعان من البيئة</h2>\n<p>لدى Compose آليتان منفصلتان تتعلّق كلتاهما بـ «env»، والخلط بينهما سبب معظم الالتباس في الإعدادات.</p>\n<p><strong>الاستبدال</strong> (interpolation) يملأ العناصر النائبة <code dir=\"ltr\">${VAR}</code> <em>في ملف Compose نفسه</em>. القيم تأتي من الـ shell لديك، ثم من ملف <code dir=\"ltr\">.env</code> في مجلد المشروع:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"ini\" data-index=\"0\"><div class=\"code-head\"><span class=\"code-title\">.env</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"نسخ الكود\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>نسخ</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">POSTGRES_PASSWORD</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">=change-me-locally</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">API_PORT</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">=8080</span></span></code></pre></div>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"yaml\" data-index=\"1\"><div class=\"code-head\"><span class=\"code-title\">compose.yaml</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"نسخ الكود\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>نسخ</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">services</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">  api</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">    ports</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">      - </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"${API_PORT:-8080}:3000\"</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">  db</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">    environment</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">      POSTGRES_PASSWORD</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD in .env}</span></span></code></pre></div>\n<p>الصيغة <code dir=\"ltr\">${API_PORT:-8080}</code> تستخدم 8080 قيمةً احتياطية إن لم يكن المتغيّر مضبوطًا. والصيغة <code dir=\"ltr\">${POSTGRES_PASSWORD:?…}</code> تجعل القيمة مطلوبة: إن كانت مفقودة، يتوقّف Compose برسالتك بدلًا من تشغيل Postgres بكلمة مرور فارغة.</p>\n<p><strong>بيئة الـ container</strong> هي ما تراه العملية في الداخل. وتأتي من <code dir=\"ltr\">environment:</code> و<code dir=\"ltr\">env_file:</code> في الخدمة:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"yaml\" data-index=\"2\"><div class=\"code-head\"><span class=\"code-title\">YAML</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"نسخ الكود\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>نسخ</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">  api</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">    env_file</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">.env.api</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">    environment</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">      DATABASE_URL</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">postgres://notes:${POSTGRES_PASSWORD}@db:5432/notes</span></span></code></pre></div>\n<p>ملف <code dir=\"ltr\">.env</code> الخاص بالمشروع <em>لا</em> يُمرَّر إلى الـ containers من تلقاء نفسه. إذا كان يجب أن تصل قيمة إلى التطبيق، فأشر إليها تحت <code dir=\"ltr\">environment:</code> أو اذكر الملف تحت <code dir=\"ltr\">env_file:</code>. وحين يأتي المتغيّر نفسه من المصدرين، تفوز <code dir=\"ltr\">environment:</code>.</p>\n<p>في الاستبدال، المتغيّر المضبوط في الـ shell يتغلّب على نظيره في <code dir=\"ltr\">.env</code>، لذا فإن <code dir=\"ltr\">API_PORT=9090 docker compose up -d</code> تجاوز سريع لمرة واحدة دون تعديل أي ملف. ولاستخدام ملف مختلف كليًا، مرّر <code dir=\"ltr\">--env-file .env.staging</code>. أبقِ عدد هذه الملفات قليلًا؛ فثلاثة ملفات env متشابهة بفروق صغيرة كفيلة بأن تجعل الفريق يشخّص الإعدادات بدلًا من الكود.</p>\n<p>احفظ في المستودع ملف <code dir=\"ltr\">.env.example</code> بقيم وهمية، وأضف <code dir=\"ltr\">.env</code> إلى <code dir=\"ltr\">.gitignore</code>. ينسخ الزملاء الجدد ملف المثال ويملؤونه.</p>\n<aside class=\"callout callout-tip\" role=\"note\"><p class=\"callout-title\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><path d=\"M15 14c.2-1 .7-1.7 1.5-2.5 1-.9 1.5-2.2 1.5-3.5A6 6 0 0 0 6 8c0 1 .2 2.2 1.5 3.5.7.7 1.3 1.5 1.5 2.5\"/><path d=\"M9 18h6\"/><path d=\"M10 22h4\"/></svg><span><span class=\"callout-kind\">نصيحة</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>انظر إلى ما يراه Compose</span></p>\n<p>الأمر <code dir=\"ltr\">docker compose config</code> يطبع الإعدادات النهائية بعد دمج الملفات واستبدال المتغيّرات. حين لا تكون قيمة ما كما تتوقّع، شغّله قبل أي شيء آخر. عشر ثوانٍ معه وفّرت عليّ ساعات من التخمين.</p>\n</aside>\n<h2 id=\"sec-2\" tabindex=\"-1\">أسرار Compose</h2>\n<p>متغيّرات البيئة مرئية في <code dir=\"ltr\">docker inspect</code> ولكل عملية داخل الـ container. لكلمات المرور، يستطيع Compose تركيب ملف بدلًا من ذلك:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"yaml\" data-index=\"3\"><div class=\"code-head\"><span class=\"code-title\">YAML</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"نسخ الكود\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>نسخ</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">services</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">  db</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">    image</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">postgres:17</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">    environment</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">      POSTGRES_PASSWORD_FILE</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">/run/secrets/db_password</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">    secrets</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">      - </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">db_password</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">secrets</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">  db_password</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">    file</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">./secrets/db_password.txt</span></span></code></pre></div>\n<p>يظهر السرّ داخل الـ container في <code dir=\"ltr\">/run/secrets/db_password</code>. تقرأ image ‏Postgres المتغيّر <code dir=\"ltr\">POSTGRES_PASSWORD_FILE</code>، وكثير من الـ images الرسمية تدعم عرف <code dir=\"ltr\">_FILE</code> نفسه. أما في تطبيقك، فاقرأ الملف عند البدء. الـ Kubernetes Secrets المركّبة كملفات تعمل بالطريقة نفسها، فتنتقل العادة معك.</p>\n<h2 id=\"sec-3\" tabindex=\"-1\">ملف أساسي مع override</h2>\n<p>يدمج Compose تلقائيًا <code dir=\"ltr\">compose.override.yaml</code> فوق <code dir=\"ltr\">compose.yaml</code> إن وُجد. استخدم هذا الفصل عن قصد:</p>\n<ul>\n<li><code dir=\"ltr\">compose.yaml</code>: ما تحتاج إليه كل بيئة، بما فيها الـ CI.</li>\n<li><code dir=\"ltr\">compose.override.yaml</code>: وسائل الراحة للمطوّرين، مثل منافذ قاعدة البيانات المنشورة، وخيارات التشخيص، وقواعد المراقبة.</li>\n</ul>\n<p>يستطيع الـ CI عندها التشغيل بالملف الأساسي وحده: <code dir=\"ltr\">docker compose -f compose.yaml up -d --wait</code>. حين تمرّر <code dir=\"ltr\">-f</code>، لا يُحمَّل الـ override تلقائيًا.</p>\n<p><strong>الـ Profiles</strong> تغطّي الخدمات الاختيارية. container ‏pgAdmin لا يريده إلا بعض الناس يحصل على <code dir=\"ltr\">profiles: [tools]</code>، ولا يبدأ إلا مع <code dir=\"ltr\">docker compose --profile tools up</code>.</p>\n<h2 id=\"sec-4\" tabindex=\"-1\">وضع Watch: حلقة تعديل سريعة</h2>\n<p>إعادة بناء الـ image مع كل تعديل بطيئة. وربط المشروع كاملًا بـ bind mount هشّ: ‏<code dir=\"ltr\">node_modules</code> على المضيف (المبني لـ macOS) يحجب نظيره في الـ container (المبني لـ Linux)، والوصول إلى الملفات عبر آلة Docker Desktop الافتراضية بطيء. <strong>Compose watch</strong> يحلّ المشكلتين بمزامنة الملفات التي تختارها فقط إلى الـ container الذي يعمل.</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"yaml\" data-index=\"4\"><div class=\"code-head\"><span class=\"code-title\">compose.override.yaml</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"نسخ الكود\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>نسخ</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">services</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">  api</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">    build</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">      context</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">.</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">      target</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">build</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">    command</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: [</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"npm\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"run\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"dev\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">]</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">    develop</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">      watch</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">        - </span><span style=\"color:#116329;--shiki-dark:#7EE787\">action</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">sync</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">          path</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">./src</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">          target</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">/app/src</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">        - </span><span style=\"color:#116329;--shiki-dark:#7EE787\">action</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">rebuild</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">          path</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">package.json</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">        - </span><span style=\"color:#116329;--shiki-dark:#7EE787\">action</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">rebuild</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">          path</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">package-lock.json</span></span></code></pre></div>\n<p>للتطوير، يبني الـ override مرحلة <code dir=\"ltr\">build</code> (التي تحتوي على اعتماديات التطوير) ويشغّل <code dir=\"ltr\">npm run dev</code>، وهو في notes-api ‏<code dir=\"ltr\">tsx watch src/server.ts</code>، أداة لتشغيل TypeScript تعيد التشغيل عند التغييرات. ثم:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"bash\" data-index=\"5\"><div class=\"code-head\"><span class=\"code-title\">Terminal</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"نسخ الكود\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>نسخ</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">docker</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> compose</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> up</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> --watch</span></span></code></pre></div>\n<p>عدّل <code dir=\"ltr\">src/routes.ts</code> فيُنسخ الملف إلى <code dir=\"ltr\">/app/src</code> خلال ثانية؛ ويعيد <code dir=\"ltr\">tsx</code> تشغيل الخادم. غيّر <code dir=\"ltr\">package-lock.json</code> فيعيد Compose بناء الـ image ويستبدل الـ container، لأن الاعتماديات الجديدة تحتاج إلى تثبيت حقيقي. وهناك إجراء ثالث، <code dir=\"ltr\">sync+restart</code>، يزامن الملفات ويعيد تشغيل الـ container، ويناسب ملفات الإعدادات التي لا يقرؤها التطبيق إلا عند البدء. والأمر <code dir=\"ltr\">docker compose watch</code> يفعل ما يفعله <code dir=\"ltr\">up --watch</code> مع مخرجات أهدأ.</p>\n<aside class=\"callout callout-mistake\" role=\"note\"><p class=\"callout-title\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><path d=\"m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3\"/><path d=\"M12 9v4\"/><path d=\"M12 17h.01\"/></svg><span><span class=\"callout-kind\">خطأ شائع</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>شحن إعدادات التطوير</span></p>\n<p>الـ override يشغّل مرحلة <code dir=\"ltr\">build</code> مع npm في موقع PID 1 ومراقب للملفات. هذا جيد على حاسوبك وخاطئ في أي مكان آخر. أبقِ إعدادات التطوير وحدها في <code dir=\"ltr\">compose.override.yaml</code>، وتأكّد من أن الـ CI وأي نشر على خادم يستخدمان <code dir=\"ltr\">-f compose.yaml</code> صراحة، حتى تكون الـ image بشكلها الإنتاجي هي التي تُختبر.</p>\n</aside>\n<p>اكتمل القسم الثالث: بيئة محلية قابلة للتكرار، ببيانات دائمة، وإقلاع مرتّب، وإعدادات آمنة، وحلقة تعديل سريعة. في القسم التالي ستأخذ الـ image نفسها إلى Kubernetes، بدءًا بطريقة تفكير Kubernetes.</p>\n","toc":[{"id":"sec-1","text":"نوعان من البيئة","level":2},{"id":"sec-2","text":"أسرار Compose","level":2},{"id":"sec-3","text":"ملف أساسي مع override","level":2},{"id":"sec-4","text":"وضع Watch: حلقة تعديل سريعة","level":2}],"takeaways":["ملف <code dir=\"ltr\">.env</code> الخاص بالمشروع يغذّي استبدال <code dir=\"ltr\">${VAR}</code> داخل <code dir=\"ltr\">compose.yaml</code>؛ بينما <code dir=\"ltr\">env_file:</code> و<code dir=\"ltr\">environment:</code> تضبطان المتغيّرات داخل الـ containers. إنهما آليتان مختلفتان.","الصيغة <code dir=\"ltr\">${VAR:?message}</code> تجعل Compose يفشل سريعًا برسالة واضحة حين تكون قيمة مطلوبة مفقودة.","الأمر <code dir=\"ltr\">docker compose config</code> يطبع الملف بعد الدمج والاستبدال الكاملين، وهو أسرع طريقة لتشخيص الإعدادات.","الملف <code dir=\"ltr\">compose.override.yaml</code> يُدمج تلقائيًا، لذا أبقِ الإعدادات المشتركة في <code dir=\"ltr\">compose.yaml</code> وإعدادات التطوير وحدها في الـ override.","الخيار <code dir=\"ltr\">develop.watch</code> مع <code dir=\"ltr\">sync</code> ينسخ الملفات المعدّلة إلى الـ container الذي يعمل، ومع <code dir=\"ltr\">rebuild</code> يعيد بناء الـ image حين تتغيّر الاعتماديات."],"further":[{"title":"Set environment variables in Compose","url":"https://docs.docker.com/compose/how-tos/environment-variables/set-environment-variables/"},{"title":"Use Compose Watch","url":"https://docs.docker.com/compose/how-tos/file-watch/"},{"title":"Merge Compose files","url":"https://docs.docker.com/compose/how-tos/multiple-compose-files/merge/"},{"title":"Secrets in Compose","url":"https://docs.docker.com/compose/how-tos/use-secrets/"}],"quiz":[{"q":"<p>ملف <code dir=\"ltr\">.env</code> في مشروعك يحتوي على <code dir=\"ltr\">LOG_LEVEL=debug</code>. خدمة <code dir=\"ltr\">api</code> ليس فيها مدخلات <code dir=\"ltr\">environment:</code> ولا <code dir=\"ltr\">env_file:</code>. ما قيمة <code dir=\"ltr\">LOG_LEVEL</code> داخل الـ container؟</p>","options":[{"text":"‏<code dir=\"ltr\">debug</code>، لأن Compose يحمّل <code dir=\"ltr\">.env</code> في كل container.","why":"ملف <code dir=\"ltr\">.env</code> الخاص بالمشروع مخصّص لاستبدال القيم في ملف Compose نفسه. لا يُحقن في الـ containers ما لم تشر إليه."},{"text":"غير مضبوطة، ما لم تمرّرها الخدمة عبر <code dir=\"ltr\">environment:</code> أو <code dir=\"ltr\">env_file:</code>.","why":"صحيح. أضف <code dir=\"ltr\">LOG_LEVEL: ${LOG_LEVEL}</code> تحت <code dir=\"ltr\">environment:</code>، أو <code dir=\"ltr\">env_file: .env</code>، لتمريرها."},{"text":"‏<code dir=\"ltr\">debug</code>، لكن بعد <code dir=\"ltr\">docker compose restart</code> فقط.","why":"إعادة التشغيل لا تغيّر طريقة استخدام <code dir=\"ltr\">.env</code>. يبقى مخصّصًا للاستبدال فقط."},{"text":"خطأ، لأن <code dir=\"ltr\">.env</code> يجب أن يكون اسمه <code dir=\"ltr\">api.env</code>.","why":"الاسم <code dir=\"ltr\">.env</code> هو الاسم المتعارف عليه الذي يقرؤه Compose للاستبدال. لا يلزم تسمية لكل خدمة."}],"answer":1},{"q":"<p>الملف <code dir=\"ltr\">compose.yaml</code> يحتوي على <code dir=\"ltr\">POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set it in .env}</code> والمتغيّر غير معرّف في أي مكان. ماذا يحدث عند <code dir=\"ltr\">docker compose up</code>؟</p>","options":[{"text":"يبدأ Postgres بكلمة مرور فارغة.","why":"هذا ما كانت ستفعله <code dir=\"ltr\">${POSTGRES_PASSWORD}</code> العادية، ولهذا بالضبط وُجدت الصيغة <code dir=\"ltr\">:?</code>."},{"text":"يطلب منك Compose كتابة كلمة المرور.","why":"لا يطلب Compose إدخال المتغيّرات أبدًا."},{"text":"يصبح النصّ الحرفي <code dir=\"ltr\">${POSTGRES_PASSWORD:?set it in .env}</code> هو كلمة المرور.","why":"يستبدل Compose التعبير؛ ولا يمرّره حرفيًا."},{"text":"يرفض Compose البدء ويطبع <code dir=\"ltr\">set it in .env</code>.","why":"صحيح. ‏<code dir=\"ltr\">:?</code> تجعل المتغيّر مطلوبًا وتستخدم نصّك رسالةً للخطأ."}],"answer":3},{"q":"<p>تعدّل <code dir=\"ltr\">src/routes.ts</code>. مع قواعد المراقبة أدناه، ماذا يفعل Compose؟</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"yaml\" data-index=\"0\"><div class=\"code-head\"><span class=\"code-title\">YAML</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"نسخ الكود\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>نسخ</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">develop</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">  watch</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    - </span><span style=\"color:#116329;--shiki-dark:#7EE787\">action</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">sync</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">      path</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">./src</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">      target</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">/app/src</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    - </span><span style=\"color:#116329;--shiki-dark:#7EE787\">action</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">rebuild</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">      path</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">package-lock.json</span></span></code></pre></div>\n","options":[{"text":"يعيد بناء الـ image وإنشاء الـ container.","why":"لا يؤدّي إلى إعادة البناء إلا التغيير في <code dir=\"ltr\">package-lock.json</code>. والملف داخل <code dir=\"ltr\">./src</code> يطابق قاعدة الـ sync."},{"text":"لا شيء حتى تشغّل <code dir=\"ltr\">docker compose up</code> من جديد.","why":"وضع المراقبة يتفاعل مع تغييرات الملفات أثناء عمله؛ ولا تعيد تشغيل <code dir=\"ltr\">up</code>."},{"text":"ينسخ الملف المتغيّر إلى <code dir=\"ltr\">/app/src</code> في الـ container الذي يعمل؛ ويلتقطه خادم التطوير في الداخل.","why":"صحيح. ‏<code dir=\"ltr\">sync</code> يحدّث الملفات في مكانها خلال ثوانٍ، دون إعادة بناء."},{"text":"يعيد تشغيل قاعدة البيانات، لأن الـ API يعتمد عليها.","why":"قواعد المراقبة لا تعمل إلا على الخدمة المعرّفة فيها."}],"answer":2},{"q":"<p>تريد خدمة <code dir=\"ltr\">pgadmin</code> متاحة للتشخيص المحلي، لكن يجب ألا تبدأ في الـ CI أو عند الزملاء الذين لا يحتاجون إليها. ماذا تستخدم؟</p>","options":[{"text":"‏<code dir=\"ltr\">profiles: [tools]</code> على الخدمة، وتُشغَّل بـ <code dir=\"ltr\">docker compose --profile tools up</code>.","why":"صحيح. الخدمات التي لها profile لا تبدأ إلا حين يُفعَّل ذلك الـ profile."},{"text":"تعليق يطلب من الناس تشغيلها يدويًا.","why":"التعليقات لا تغيّر السلوك؛ سيحصل الجميع على pgadmin مع كل <code dir=\"ltr\">up</code>."},{"text":"‏<code dir=\"ltr\">deploy: replicas: 0</code>","why":"إنه التفاف يُخفي النية ويحتاج إلى تعديل لتشغيله. الـ profiles موجودة لهذا الغرض."},{"text":"فرع Git منفصل أُضيف إليه pgadmin.","why":"الفروع المخصّصة للإعدادات تتقادم بسرعة. الـ profiles تُبقي كل شيء في ملف واحد."}],"answer":0}],"exercise":{"kind":"fill","language":"yaml","template":"# compose.yaml\nservices:\n  db:\n    environment:\n      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD{{0}}set POSTGRES_PASSWORD in .env}\n\n# compose.override.yaml\nservices:\n  api:\n    develop:\n      watch:\n        - action: {{1}}\n          path: ./src\n          target: /app/src\n        - action: {{2}}\n          path: package-lock.json\n\n# start the stack and keep watching:\n# docker compose up {{3}}","blanks":[{"answers":[":?"]},{"answers":["sync"]},{"answers":["rebuild"]},{"answers":["--watch","-w"]}],"title":"قيم مطلوبة وحلقة تعديل سريعة","prompt":"<p>املأ الفراغات حتى يرفض Compose البدء حين يكون <code dir=\"ltr\">POSTGRES_PASSWORD</code> مفقودًا، وتُنسخ تعديلات الكود إلى container الـ API الذي يعمل، وتعيد تغييرات الـ lockfile بناء الـ image، وتبدأ البيئة في وضع المراقبة.</p>\n","hints":["عامل الاستبدال الذي يعني «مطلوب، مع رسالة خطأ» من حرفين: نقطتان وعلامة استفهام.","إجراء مراقبة واحد ينسخ الملفات إلى الـ container؛ وآخر يعيد بناء الـ image.","الأمر <code dir=\"ltr\">docker compose up</code> يأخذ خيارًا يفعّل وضع المراقبة."],"explanation":"<div class=\"code-block\" dir=\"ltr\" data-lang=\"yaml\" data-index=\"0\"><div class=\"code-head\"><span class=\"code-title\">YAML</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"نسخ الكود\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>نسخ</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">POSTGRES_PASSWORD</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD in .env}</span></span>\n<span class=\"line\"><span style=\"color:#6E7781;--shiki-dark:#8B949E\"># …</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">- </span><span style=\"color:#116329;--shiki-dark:#7EE787\">action</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">sync</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">  path</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">./src</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">  target</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">/app/src</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">- </span><span style=\"color:#116329;--shiki-dark:#7EE787\">action</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">rebuild</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">  path</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">package-lock.json</span></span></code></pre></div>\n<p>ثم <code dir=\"ltr\">docker compose up --watch</code>. ‏<code dir=\"ltr\">:?</code> تحوّل قيمةً كانت ستبقى فارغة دون أن ينتبه أحد إلى خطأ واضح عند البدء. و<code dir=\"ltr\">sync</code> ينسخ ملفات الكود المعدّلة إلى الـ container الذي يعمل خلال ثانية تقريبًا، بينما يحتاج تغيير الاعتماديات إلى <code dir=\"ltr\">rebuild</code> لأن الحزم الجديدة يجب أن تُثبَّت داخل الـ image.</p>\n","checkLabels":{}},"words":680,"readMinutes":4}