اجرای خودکار Docker Compose پس از روشن شدن سیستم
برای اجرای دوباره سرویسهای Docker Compose پس از reboot، سیاستهای restart را تنظیم کنید؛ ببینید چرا on-failure پس از reboot کافی نیست و چه زمانی systemd لازم است.
پاسخ کوتاه
سرویسهای Docker Compose هنگام راهاندازی سیستم، فقط زمانی شروع میشوند که دو شرط همزمان برقرار باشد. daemon مربوط به Docker باید بهعنوان یک سرویس سیستم فعال شده باشد و هر سرویس در فایل باید دارای سیاست راهاندازی مجدد unless-stopped یا always باشد. restart: unless-stopped را به هر سرویس اضافه کنید، یکبار docker compose up -d را اجرا کنید تا کانتینرها پس از راهاندازی مجدد سیستم بهصورت خودکار دوباره اجرا شوند. برای حالت معمول، به کار دیگری نیاز نیست.
فقط زمانی به یک واحد systemd نیاز دارید که ترتیب راهاندازی اهمیت داشته باشد؛ برای مثال، زمانی که یک stack به دیسک mountشده، رابط VPN یا network share وابسته است و این منابع هنگام شروع daemon مربوط به Docker هنوز آماده نیستند. این حالت واقعی است و نیمه دوم این راهنما آن را پوشش میدهد. اگر هنوز با تعریف سرویسها و volumeها آشنا نیستید، از مبانی Docker Compose در یک VPS شروع کنید و سپس بازگردید.
تنظیم سیاست راهاندازی مجدد در compose.yaml
این سیاست برای هر سرویس در یک خط تنظیم میشود. کلید سراسری وجود ندارد؛ بنابراین سرویسی که تنظیم آن را فراموش کنید، پس از راهاندازی مجدد سیستم متوقف میماند، درحالیکه سایر اجزای stack اجرا میشوند.
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:سپس آن را اعمال کنید و سیاست را از container در حال اجرا بخوانید:
docker compose up -d
docker inspect -f '{{.HostConfig.RestartPolicy.Name}}' $(docker compose ps -q app)این دستور unless-stopped را چاپ میکند. اگر no چاپ شود، فایل ویرایش شده است، اما container هرگز دوباره ایجاد نشده است.
این رایجترین علت خرابی است. سیاست راهاندازی مجدد در container ذخیره میشود، نه در فایل YAML. ویرایش compose.yaml هیچ تغییری در container موجود ایجاد نمیکند. docker compose restart نیز کمکی نمیکند، زیرا همان شیء container را متوقف و دوباره اجرا میکند، بدون آنکه پیکربندی آن را تغییر دهد. فقط docker compose up -d فایل را با containerهای در حال اجرا مقایسه میکند، تغییر سیاست را تشخیص میدهد و containerها را دوباره ایجاد میکند.
برای containerی که فعلاً نمیخواهید دوباره ایجاد کنید، سیاست را درجا تغییر دهید:
docker update --restart unless-stopped my-containerفایل YAML را نیز ویرایش کنید. docker update container فعال را تغییر میدهد، اما اجرای بعدی docker compose up -d فایل را میخواند و مقدار قبلی را برمیگرداند.
هر مقدار راهاندازی مجدد عملاً چه کاری انجام میدهد
Docker چهار مقدار را تعریف میکند و تفاوت آنها فقط هنگام راهاندازی مجدد ماشین یا راهاندازی مجدد daemon مشخص میشود.
noمقدار پیشفرض است. container تحت هیچ شرایطی بهصورت خودکار راهاندازی مجدد نمیشود.alwaysهر زمان که container متوقف شود، آن را راهاندازی مجدد میکند. اگر آن را بهصورت دستی متوقف کرده باشید، دفعه بعد که Docker daemon شروع شود، container دوباره اجرا میشود. این وضعیت اغلب غیرمنتظره است: containerی که عمداً هفته گذشته متوقف کردهاید، پس از راهاندازی مجدد ماشین دوباره در حال اجراست.unless-stoppedمانندalwaysعمل میکند، با این تفاوت که containerی که بهصورت دستی متوقف شده است، پس از راهاندازی مجدد daemon نیز متوقف میماند. برای سرویسی که گاهی بهمنظور نگهداری متوقف میکنید، باید از این مقدار استفاده کنید.on-failureفقط زمانی container را راهاندازی مجدد میکند که با کد خروجی غیرصفر خارج شود. میتوانید تعداد تلاشها را مانندrestart: on-failure:3محدود کنید.
برای stackی که باید هر زمان server در حال اجراست، فعال باشد، unless-stopped مقدار پیشفرض مناسبی است. فقط زمانی always را انتخاب کنید که میخواهید container در برابر متوقف ماندن مقاومت کند.
چرا راهاندازی مجدد: 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 همچنان کاربرد دارد. این سیاست برای کانتینری مناسب است که یک job را اجرا میکند و ممکن است از کار بیفتد؛ در چنین حالتی تعداد تلاشهای مجدد محدود میخواهید و نباید حلقه راهاندازی مجدد ایجاد شود. اما برای زنده نگهداشتن یک سرویس بلندمدت پس از راهاندازی مجدد سیستم، ابزار مناسبی نیست.
سیاستهای راهاندازی مجدد فقط زمانی کار میکنند که سرویس Docker هنگام راهاندازی سیستم اجرا شود
سیاستهای راهاندازی مجدد را daemon مربوط به Docker اعمال میکند. اگر daemon اجرا نشود، هیچ چیزی این سیاستها را اعمال نمیکند. وضعیت آن را بررسی کنید:
systemctl is-enabled docker
systemctl is-enabled containerdهر دو دستور باید enabled را نمایش دهند. بستههای مخزن رسمی Docker این سرویسها را هنگام نصب فعال میکنند؛ بنابراین در یک سرور تازه، این بررسی معمولاً موفق است. اگر هرکدام disabled را نمایش داد، آن را اصلاح کنید:
sudo systemctl enable --now docker containerdدر اینجا نکتهای وجود دارد که باید آن را درک کنید. Ubuntu همچنین docker.socket را ارائه میکند که با نخستین درخواست به API مربوط به Docker، daemon را در صورت نیاز اجرا میکند. کاربران docker.socket را فعال میبینند، تصور میکنند daemon نیز تحت پوشش است، و برای صرفهجویی در حافظه docker.service را غیرفعال میکنند. هنگام راهاندازی سیستم، هیچ چیزی API را فراخوانی نمیکند؛ بنابراین socket هرگز مورد استفاده قرار نمیگیرد، daemon اجرا نمیشود و هیچ containerای تا زمانی که نخستین دستور docker خود را وارد نکنید، راهاندازی نمیشود. فعالسازی socket جایگزین فعال بودن docker.service نیست.
وقتی یک واحد systemd پاسخ بهتری است
سیاستهای راهاندازی مجدد، مفهومی برای تعیین ترتیب اجرا نسبت به بخشهای دیگر سیستم ندارند. daemon شروع میشود و کانتینرهای شما را در اولین فرصت راهاندازی میکند. اگر stack شما یک directory را از یک volume جداگانه، یک share از نوع NFS (network file system)، یا یک disk رمزنگاریشده bind-mount کند، ممکن است کانتینرها پیش از وجود آن path شروع شوند. Docker بدون مشکل یک directory خالی در mount point ایجاد میکند و کانتینر را با همان directory اجرا میکند؛ در نتیجه database شما بدون data راهاندازی میشود.
هرگاه یکی از شرایط زیر برقرار باشد، یک واحد systemd بنویسید. stack برای آمادهشدن به یک mount، یک interface مربوط به VPN، یا یک unit دیگر نیاز دارد. میخواهید systemctl stop myapp و systemctl start myapp مانند سایر serviceهای سیستم کار کنند. یا میخواهید هنگام shutdown، stack بهصورت cleanly متوقف شود، نه اینکه همزمان با daemon از بین برود. اگر واحدهای systemd برای شما جدید هستند، نوشتن یک service و timer برای systemd قالب فایل را با جزئیات بیشتری توضیح میدهد.
نوشتن واحد systemd
Stack را در مسیری ثابت و خارج از یک دایرکتوری home قرار دهید. /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 مربوط به آن مسیر را نیز وارد فرایند کند و منتظر آمادهشدن آن بماند. دلیل اصلی استفاده از واحد، بهجای یک سیاست restart، همین است. TimeoutStartSec=0 مانع میشود systemd در زمانی که یک image بزرگ هنوز در حال pull شدن است، job مربوط به start را خاتمه دهد.
نکتهای درباره ترکیب این دو سازوکار: مستندات Docker توصیه میکنند سیاستهای restart را با یک process manager میزبان ترکیب نکنید. این هشدار درباره process managerای است که خودِ فرایند container را نظارت میکند و همزمان با daemon، آن را restart میکند. یک واحد Type=oneshot بر چیزی نظارت نمیکند؛ بنابراین نگهداشتن restart: unless-stopped در فایل compose در کنار این واحد اشکالی ندارد و همین کاری است که باید انجام دهید. systemd ترتیب اجرا هنگام boot را مدیریت میکند و daemon نیز containerای را که ساعت 3 بامداد crash میکند، مدیریت خواهد کرد.
با یک راهاندازی مجدد واقعی بررسی کنید
هیچ چیزی جای آزمون واقعی را نمیگیرد. systemctl restart docker ترتیب mount را آزمایش نمیکند و اجرای docker compose down و سپس docker compose up -d، هیچ بخشی از فرایند boot را آزمایش نمیکند.
sudo rebootصبر کنید، دوباره متصل شوید و به این ترتیب بررسی کنید:
uptime
systemctl is-active docker
docker compose psuptime تأیید میکند که به ماشینی متصل هستید که واقعاً راهاندازی مجدد شده است. docker compose ps را از دایرکتوری stack اجرا کنید؛ این دستور باید همه سرویسها را با وضعیت running و uptime نزدیک به uptime ماشین نشان دهد. سرویسی که وضعیت Exited دارد، همان سرویسی است که باید بررسی شود.
اگر سرویسی راهاندازی نشد، گزارش daemon بازه زمانی boot را پوشش میدهد:
journalctl -u docker.service -b --no-pager | tail -50برای stack که با یک unit مدیریت میشود، journalctl -u myapp.service -b --no-pager خروجی دقیق docker compose را از زمان boot نشان میدهد؛ این خروجی شامل خطای pull کردن image یا نبودن فایل .env نیز میشود.
مواردی که بهطور نامحسوس راهاندازی خودکار را مختل میکنند
کانتینرهایی که با docker compose run ایجاد میشوند، هرگز سیاست راهاندازی مجدد را از فایل دریافت نمیکنند. Compose با آنها مانند کانتینرهای موقت رفتار میکند. اگر به نظر میرسد یک سرویس سیاست خود را نادیده میگیرد، بررسی کنید که آیا با run بهجای up راهاندازی شده است یا نه.
مسیر نسبی در یک volume یا در ورودی env_file نسبت به پوشه فایل Compose resolve میشود. این کار از shell شما و از unitای که WorkingDirectory را تنظیم میکند، درست انجام میشود. اما از unitای که این متغیر را تنظیم نکرده است، شکست میخورد؛ زیرا در آن حالت، دایرکتوری کاری / است.
Docker بدون root حالت جداگانهای دارد. daemon بهصورت یک user service اجرا میشود و user service پس از پایان آخرین session آن کاربر متوقف میشود. آن را برای کاربر فعال کنید و اجازه دهید بدون ورود هیچ کاربری به اجرا ادامه دهد:
systemctl --user enable docker
sudo loginctl enable-linger $USERبدون enable-linger، daemon بدون root هنگام خروج شما خاموش میشود و کانتینرها نیز متوقف میشوند. این وضعیت دقیقاً شبیه خراب بودن سیاست راهاندازی مجدد به نظر میرسد.
یک مورد دیگر باقی میماند. بهروزرسانیهای امنیتی خودکار میتوانند server را در ساعت مشخصی reboot کنند. این فقط زمانی مفید است که stack شما بهصورت خودکار دوباره بالا بیاید. تنظیم این مورد روی یک ماشین جدید، بخشی از کارهای ساعت اول در 10 دقیقه اول در یک VPS جدید است.
FAQ
تفاوت بین restart: always و restart: unless-stopped چیست؟
هر دو، کانتینر را هنگام توقف خودکار دوباره راهاندازی میکنند. تفاوت آنها پس از توقف دستی کانتینر مشخص میشود. با always، کانتینر هنگام راهاندازی بعدی daemon مربوط به Docker دوباره شروع میشود؛ بنابراین راهاندازی مجدد سیستم، توقف دستی شما را بیاثر میکند. با 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 بررسی کنید.
اگر از سیاستهای restart استفاده کنم، آیا همچنان به یک واحد systemd نیاز دارم؟
معمولاً خیر. برای پشتهای که فقط به شبکه نیاز دارد، سیاست restart کافی است و بیشتر پشتهها چنین هستند. زمانی واحد اضافه کنید که کانتینرها به چیزی وابسته باشند که هنگام راهاندازی daemon مربوط به Docker هنوز آماده نیست؛ مانند یک دیسک خارجی، یک volume رمزگذاریشده، یک share مربوط به NFS یا یک رابط VPN. واحد، ترتیب راهاندازی را از طریق After= و RequiresMountsFor= تعیین میکند؛ قابلیتی که یک سیاست restart نمیتواند بیان کند.
چگونه یک پشته را برای همیشه متوقف کنم تا در راهاندازی مجدد بعدی دوباره اجرا نشود؟
با unless-stopped، اجرای docker compose stop کافی است، زیرا کانتینری که بهصورت دستی متوقف شده است، هنگام راهاندازی مجدد daemon دوباره اجرا نمیشود. با always، توقف بهتنهایی کافی نیست و کانتینر پس از راهاندازی مجدد سیستم دوباره اجرا میشود. میتوانید docker compose down را اجرا کنید تا کانتینرها حذف شوند، یا ابتدا سیاست را با docker update --restart no my-container تغییر دهید. اگر یک واحد systemd پشته را مدیریت میکند، sudo systemctl disable myapp.service را نیز اجرا کنید؛ در غیر این صورت واحد دوباره آن را اجرا خواهد کرد.