تشغيل Docker Compose تلقائياً بعد إعادة التشغيل
تعرّف إلى سياسة restart الصحيحة في Docker Compose، ولماذا لا تصمد on-failure بعد إعادة التشغيل، ومتى تحتاج إلى وحدة systemd لترتيب الخدمات.
الإجابة المختصرة
تبدأ خدمات Docker Compose عند الإقلاع عندما يتحقق شرطان في الوقت نفسه. يجب تفعيل Docker daemon كخدمة systemd، ويجب أن تتضمن كل خدمة في الملف سياسة إعادة تشغيل بقيمة unless-stopped أو always. أضف restart: unless-stopped إلى كل خدمة، وشغّل docker compose up -d مرة واحدة، وستعود الحاويات إلى العمل تلقائياً بعد إعادة التشغيل. لا تحتاج إلى أي إعداد آخر في الحالة المعتادة.
تحتاج إلى وحدة systemd فقط عندما يكون ترتيب التشغيل مهماً: مثلاً، عندما تعتمد حزمة خدمات على قرص موصول، أو واجهة VPN، أو مشاركة شبكة لا تكون جاهزة عند بدء Docker daemon. هذه حالة فعلية، ويغطيها النصف الثاني من هذا الدليل. إذا كنت لا تزال تتعلم تعريفات الخدمات وvolumes، فابدأ بـأساسيات Docker Compose على VPS ثم عُد إلى هنا.
تعيين سياسة إعادة التشغيل في compose.yaml
تُحدَّد السياسة بسطر واحد لكل خدمة. لا يوجد مفتاح عام، لذلك ستبقى الخدمة التي تنسى إعدادها متوقفة بعد إعادة التشغيل، بينما تبدأ بقية مكوّنات الحزمة.
services:
app:
image: nginx:1.27
restart: unless-stopped
ports:
- "8080:80"
db:
image: postgres:16
restart: unless-stopped
environment:
POSTGRES_PASSWORD: change-me
volumes:
- dbdata:/var/lib/postgresql/data
volumes:
dbdata:طبّق الإعداد، ثم اقرأ السياسة من الحاوية قيد التشغيل:
docker compose up -d
docker inspect -f '{{.HostConfig.RestartPolicy.Name}}' $(docker compose ps -q app)يعرض ذلك unless-stopped. إذا عرض no، فهذا يعني أنّ الملف عُدِّل، لكن لم تتم إعادة إنشاء الحاوية.
هذا هو سبب الفشل الأكثر شيوعاً. تُخزَّن سياسة إعادة التشغيل في الحاوية، وليس في ملف YAML. تعديل compose.yaml لا يغيّر شيئاً في حاوية موجودة مسبقاً. ولا يفيد docker compose restart أيضاً، لأنّه يوقف كائن الحاوية نفسه ثم يشغّله من جديد من دون تعديل إعداداته. وحده docker compose up -d يقارن الملف بالحاويات قيد التشغيل، ويكتشف تغيّر السياسة، ثم يعيد إنشاء الحاويات.
بالنسبة إلى حاوية لا تريد إعادة إنشائها الآن، غيّر السياسة مباشرةً:
docker update --restart unless-stopped my-containerعدّل ملف YAML أيضاً. يغيّر docker update الحاوية قيد التشغيل، وستقرأ عملية docker compose up -d التالية الملف وتعيد القيمة القديمة.
ما الذي يفعله كل إعداد لإعادة التشغيل فعلياً
يعرّف Docker أربع قيم. ولا يظهر الفرق بينها إلا عند إعادة تشغيل الجهاز أو إعادة تشغيل daemon.
noهو الإعداد الافتراضي. لا يُعاد تشغيل الحاوية تلقائياً تحت أي ظرف.alwaysيعيد تشغيل الحاوية كلما توقفت. إذا أوقفتها يدوياً، فستعمل مجدداً عند بدء Docker daemon في المرة التالية. وهذا يسبب مفاجأة متكررة: حاوية أوقفتها عمداً الأسبوع الماضي تعمل مجدداً بعد إعادة التشغيل.unless-stoppedيعمل مثلalways، باستثناء أن الحاوية التي أوقفتها يدوياً تظل متوقفة بعد إعادة تشغيل daemon. هذه هي القيمة المناسبة لخدمة توقفها أحياناً لإجراء الصيانة.on-failureيعيد تشغيل الحاوية فقط عندما تنتهي برمز خروج غير صفري. ويمكنك تحديد الحد الأقصى لعدد المحاولات، كما فيrestart: on-failure:3.
بالنسبة إلى stack يجب أن يعمل ببساطة ما دام الخادم يعمل، فإن unless-stopped هو الإعداد الافتراضي المناسب. اختر always فقط عندما تريد حاوية تقاوم تركها متوقفة.
لماذا لا تستمر سياسة on-failure بعد إعادة التشغيل
يختار كثير من المستخدمين on-failure لأنها تبدو حذرة، ثم يكتشفون أن كل حاوية توقفت بعد أول إعادة تشغيل. السبب واضح في تعريف السياسة. تتفاعل on-failure مع أمر واحد فقط: خروج عملية الحاوية برمز خطأ.
إعادة التشغيل ليست خطأ. عندما يُغلق المضيف، يوقف systemd docker.service، ويوقف الـdaemon كل حاوية عمداً. لم تفشل الحاوية، لذلك لا يوجد ما تتفاعل معه السياسة. عند الإقلاع مجدداً، يفحص الـdaemon الحاويات التي يجب استئنافها، ولا تكون الحاوية on-failure التي أُوقفت بشكل سليم من بينها. وتبقى في حالة exited.
يمكنك التحقق من ذلك مباشرة. عيّن restart: on-failure لخدمة، وشغّل docker compose up -d، ثم أعد التشغيل وشغّل:
docker compose ps -aتظهر الخدمة بحالة Exited وبحالة مثل Exited (0) 2 minutes ago. لا يوجد عطل ولا يُسجَّل أي خطأ، وهذا ما يجعل تشخيص المشكلة صعباً. نفّذت السياسة ما تنص عليه تماماً.
لا تزال on-failure مفيدة. فهي مناسبة لحاوية تشغّل مهمة وقد تتعطل، عندما تريد عدداً محدوداً من محاولات إعادة التشغيل ومنع حلقة إعادة التشغيل. لكنها ليست الأداة المناسبة لإبقاء خدمة طويلة التشغيل قيد العمل بعد إعادة التشغيل.
لا تعمل سياسات إعادة التشغيل إلا إذا بدأت خدمة Docker عند الإقلاع
يفرض Docker daemon سياسات إعادة التشغيل. إذا لم يبدأ البرنامج الخفي، فلن يفرضها أي مكوّن. تحقّق من ذلك:
systemctl is-enabled docker
systemctl is-enabled containerdيجب أن يعرض كلا الأمرين enabled. تفعّل الحزم من مستودع Docker الرسمي هذه الإعدادات عند التثبيت، لذلك ينجح هذا الاختبار عادةً على خادم جديد. إذا عرض أيٌّ منهما disabled، فأصلح ذلك:
sudo systemctl enable --now docker containerdتوجد هنا نقطة مهمة يجب فهمها. يوفّر Ubuntu أيضاً docker.socket، الذي يبدأ البرنامج الخفي عند الطلب في المرة الأولى التي يتصل فيها شيء بواجهة Docker API. يرى المستخدمون أن docker.socket مفعّل، فيفترضون أن البرنامج الخفي سيبدأ تلقائياً، ثم يعطّلون docker.service لتوفير الذاكرة. عند الإقلاع، لا يستدعي أي شيء واجهة API، لذلك لا يُلمَس المقبس، ولا يبدأ البرنامج الخفي، ولا يبدأ أي container حتى تكتب أول أمر docker. لا يُعد تنشيط المقبس بديلاً عن تفعيل docker.service.
عندما تكون وحدة systemd هي الخيار الأفضل
لا تتضمن سياسات إعادة التشغيل أي مفهوم للترتيب بالنسبة إلى بقية النظام. تبدأ الخدمة، ثم تشغّل حاوياتك بمجرد أن تتمكن من ذلك. إذا كانت مكدستك تربط دليلاً عبر bind mount من وحدة تخزين منفصلة، أو مشاركة NFS (نظام ملفات شبكي)، أو قرص مشفّر، فقد تبدأ الحاويات قبل وجود هذا المسار. سينشئ Docker دليلاً فارغاً عند نقطة الربط ويبدأ الحاوية باستخدامه، فتبدأ قاعدة البيانات من دون بيانات.
اكتب وحدة systemd عند انطباق أي من الحالات التالية. تحتاج المكدسة إلى أن يصبح mount أو واجهة VPN أو وحدة أخرى جاهزاً أولاً. تريد أن يعمل systemctl stop myapp وsystemctl start myapp بالطريقة نفسها التي يعملان بها مع كل خدمة أخرى على الخادم. أو تريد إيقاف المكدسة بطريقة سليمة أثناء إيقاف التشغيل بدلاً من إنهائها مع الـdaemon. إذا كانت وحدات systemd جديدة عليك، يشرح كتابة خدمة ومؤقت systemd تنسيق الملف بمزيد من التفصيل.
كتابة وحدة systemd
ضع الحزمة في مسار ثابت خارج الدليل المنزلي. يُعد /srv/myapp خياراً جيداً، لأن الوحدة التي تعمل قبل تسجيل دخول أي مستخدم لا ينبغي أن تقرأ /home.
أنشئ /etc/systemd/system/myapp.service:
[Unit]
Description=myapp docker compose stack
Requires=docker.service
After=docker.service network-online.target
Wants=network-online.target
RequiresMountsFor=/srv/myapp/data
[Service]
Type=oneshot
RemainAfterExit=yes
WorkingDirectory=/srv/myapp
ExecStart=/usr/bin/docker compose up -d --remove-orphans
ExecStop=/usr/bin/docker compose down
TimeoutStartSec=0
[Install]
WantedBy=multi-user.targetفعّل الوحدة وشغّلها:
sudo systemctl daemon-reload
sudo systemctl enable --now myapp.service
systemctl status myapp.serviceتعرض الوحدة السليمة الحالة Active: active (exited). تبدو هذه الحالة خاطئة في المرة الأولى التي تراها فيها. لكنها صحيحة: يعني Type=oneshot مع RemainAfterExit=yes أن الوحدة نفّذت أمرها، وانتهى الأمر، وأن systemd يبقي الوحدة معلّمة على أنها نشطة لكي يُشغّل ExecStop عند إيقاف التشغيل.
لكل سطر غرضه. يعني Requires=docker.service أن الوحدة تفشل سريعاً بدلاً من تشغيل docker compose على socket متوقف. يحدد After= ترتيب التشغيل، لأن Requires= وحده لا يحدد ذلك. يجعل RequiresMountsFor= systemd يضمّن وحدة mount الخاصة بهذا المسار وينتظر جاهزيتها. وهذا هو السبب الأساسي لاستخدام وحدة بدلاً من سياسة إعادة التشغيل. يمنع TimeoutStartSec=0 systemd من إنهاء مهمة البدء أثناء سحب image كبيرة.
ملاحظة بشأن الجمع بين الآليتين. توصي وثائق Docker بعدم خلط سياسات إعادة التشغيل مع مدير عمليات يعمل على المضيف. يتعلق هذا التحذير بمدير عمليات يشرف على عملية الحاوية نفسها ويعيد تشغيلها بينما يحاول daemon فعل الشيء نفسه. لا تشرف وحدة Type=oneshot على أي شيء، لذلك لا مشكلة في إبقاء restart: unless-stopped في ملف compose إلى جانب هذه الوحدة، وهذا هو المطلوب. يتولى systemd ترتيب التشغيل عند الإقلاع، بينما يتولى daemon معالجة تعطل حاوية في الثالثة صباحاً.
تختلف الوحدة عندما يكون الشيء الذي تريد إبقاءه قيد التشغيل عملية عادية طويلة الأمد بدلاً من stack، لأن daemon لا يكون موجوداً خلفها عندئذ، ويتعين على Restart= الخاص بـsystemd أن يتولى الإشراف؛ يقدّم تشغيل dsh دون واجهة تفاعلية خلف systemd مثالاً عملياً لهذا النمط، بما في ذلك المستخدم المخصص وjournal.
تحقّق باستخدام إعادة تشغيل فعلية
لا بديل عن الاختبار الفعلي. لا يختبر systemctl restart docker ترتيب عمليات mount، بينما لا يختبر docker compose down المتبوع بـ docker compose up -d أي شيء يتعلق بالإقلاع.
sudo rebootانتظر، ثم أعد الاتصال، وأجرِ التحقق بالترتيب التالي:
uptime
systemctl is-active docker
docker compose psيؤكد uptime أنك تتحقق من جهاز أُعيد تشغيله فعلاً. أما docker compose ps، فعند تشغيله من مجلد stack، فيجب أن يعرض كل خدمة بالحالة running، مع مدة تشغيل قريبة من مدة تشغيل الجهاز. الخدمة التي تظهر بالحالة Exited هي التي يجب فحصها.
إذا لم تبدأ إحدى الخدمات، فسيسجل daemon log فترة الإقلاع:
journalctl -u docker.service -b --no-pager | tail -50بالنسبة إلى stack تديره وحدة، يعرض journalctl -u myapp.service -b --no-pager مخرجات docker compose الدقيقة منذ الإقلاع، بما في ذلك فشل سحب image أو فقدان ملف .env. إعادة التشغيل التي تجدولها هي التي تراقبها، لذلك دع الوحدة تخبرك بعمليات إعادة التشغيل التي لا تراقبها: يؤدي سطر OnFailure= يشير إلى خادم ntfy مستضاف ذاتياً إلى تحويل فشل stack في العودة إلى العمل إلى إشعار push، بدلاً من أن تكتشفه بعد مرور أيام.
الأمور التي تعطل التشغيل التلقائي دون أن تلاحظها
الحاويات التي تُنشأ باستخدام docker compose run لا تحصل مطلقاً على سياسة إعادة التشغيل من الملف. يتعامل Compose معها كحاويات تُشغَّل لمرة واحدة. إذا بدت إحدى الخدمات وكأنها تتجاهل سياستها، فتحقق مما إذا كانت قد بدأت باستخدام run بدلاً من up.
يُحلّ المسار النسبي في volume أو في إدخال env_file بالاستناد إلى دليل ملف Compose. يعمل ذلك من shell، ويعمل أيضاً من وحدة تضبط WorkingDirectory. لكنه يفشل من وحدة لا تضبطه، لأن دليل العمل يكون عندئذٍ /.
يُعد Docker الذي يعمل دون root حالة منفصلة. يعمل daemon كخدمة للمستخدم، وتتوقف خدمة المستخدم عند انتهاء آخر جلسة لذلك المستخدم. فعّلها للمستخدم واسمح لها بالاستمرار في العمل حتى عندما لا يكون أي مستخدم مسجلاً الدخول:
systemctl --user enable docker
sudo loginctl enable-linger $USERمن دون enable-linger، يتوقف daemon الذي يعمل دون root عند تسجيل الخروج، وتتوقف معه الحاويات. ويبدو ذلك تماماً كأنه عطل في سياسة إعادة التشغيل.
هناك أمر أخير. قد تعيد تحديثات الأمان التلقائية تشغيل الخادم في ساعة محددة. لا يكون ذلك مفيداً إلا إذا عادت مجموعة الخدمات إلى العمل تلقائياً. ويجب إعداد ذلك على الجهاز الجديد ضمن بقية أعمال الساعة الأولى في الدقائق العشر الأولى على VPS جديد.
FAQ
ما الفرق بين restart: always وrestart: unless-stopped؟
يعيد كلا الخيارين تشغيل الحاوية عندما تتوقف من تلقاء نفسها. ويختلفان بعد إيقاف الحاوية يدوياً. عند استخدام always، تبدأ الحاوية مرة أخرى في المرة التالية التي يبدأ فيها Docker daemon، ولذلك يلغي إعادة التشغيل إيقافك اليدوي. عند استخدام unless-stopped، يتذكر daemon أن الحاوية أُوقفت عمداً ويتركها متوقفة. استخدم unless-stopped ما لم تكن تريد تحديداً حاوية لا تبقى متوقفة.
أضفت restart: unless-stopped، لكن الحاوية لا تبدأ بعد إعادة التشغيل. لماذا؟
تُخزَّن السياسة في الحاوية، وليس في الملف، ولا يؤدي تعديل YAML إلى تحديث حاوية موجودة مسبقاً. شغّل docker compose up -d لكي يعيد Compose إنشاء الحاوية، ثم تحقّق باستخدام docker inspect -f '{{.HostConfig.RestartPolicy.Name}}' $(docker compose ps -q app). إذا أظهر ذلك no، فهذا يعني أن الحاوية أُنشئت قبل تعديلك. والسبب الشائع الآخر هو أن docker.service غير مفعّل، ويمكنك التحقق من ذلك باستخدام systemctl is-enabled docker.
هل أحتاج إلى وحدة systemd إذا كنت أستخدم سياسات إعادة التشغيل؟
عادةً لا. تكفي سياسة إعادة التشغيل لمكدس يحتاج إلى الشبكة فقط، وهذا ينطبق على معظم المكدسات. أضف وحدة عندما تعتمد الحاويات على مورد لا يكون جاهزاً عند بدء Docker daemon، مثل قرص خارجي، أو volume مشفّر، أو مشاركة NFS، أو واجهة VPN. تتيح لك الوحدة تحديد ترتيب البدء باستخدام After= وRequiresMountsFor=، وهو ما لا يمكن لسياسة إعادة التشغيل التعبير عنه.
كيف أوقف مكدساً نهائياً من دون أن يعود عند إعادة التشغيل التالية؟
مع unless-stopped، يكفي استخدام docker compose stop، لأن الحاوية التي أوقفتها يدوياً لا تُستأنف عند إعادة تشغيل daemon. أما مع always، فلا يكفي الإيقاف، وتعود الحاوية بعد إعادة التشغيل. شغّل docker compose down، الذي يزيل الحاويات، أو غيّر السياسة أولاً باستخدام docker update --restart no my-container. وإذا كانت وحدة systemd تدير المكدس، فشغّل sudo systemctl disable myapp.service أيضاً، وإلا فستبدأ الوحدة تشغيله مرة أخرى.