اجرای خودکار Docker Compose بعد از ریبوت سیستم
برای اجرای خودکار کانتینرها پس از ریبوت، از سیاست restart: always استفاده کنید. این راهنما تفاوت آن با on-failure و نحوه استفاده از systemd برای وابستگیهای خاص را توضیح میدهد.
پاسخ کوتاه
سرویسهای Docker Compose زمانی در هنگام بوت سیستم اجرا میشوند که دو شرط همزمان برقرار باشند. سرویس Docker daemon باید به عنوان یک سرویس سیستمی فعال (enable) شده باشد و هر سرویس در فایل تنظیمات باید دارای سیاست restart از نوع unless-stopped یا always باشد. کافی است restart: unless-stopped را به هر سرویس اضافه کنید و یک بار دستور docker compose up -d را اجرا نمایید تا کانتینرها پس از هر بار reboot بهطور خودکار بالا بیایند. برای موارد معمول، هیچ اقدام دیگری نیاز نیست.
شما تنها زمانی به یک unit در systemd نیاز دارید که ترتیب اجرا اهمیت داشته باشد: برای مثال، پشتهای (stack) که به یک دیسک mount شده، یک رابط VPN یا یک اشتراک شبکه وابسته است که در لحظه شروع به کار Docker daemon هنوز آماده نیست. این سناریو واقعی است و نیمه دوم این راهنما به آن میپردازد. اگر هنوز در حال یادگیری مفاهیم تعریف سرویسها و volumeها هستید، با اصول Docker Compose روی VPS شروع کنید و سپس به اینجا بازگردید.
تنظیم سیاست restart در compose.yaml
این سیاست برای هر سرویس در یک خط تعریف میشود. هیچ سوییچ سراسری برای آن وجود ندارد، بنابراین اگر سرویسی را فراموش کنید، پس از reboot خاموش میماند در حالی که بقیه 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 بازسازی (recreate) نشده است.
این رایجترین دلیل شکست است. سیاست restart روی container ذخیره میشود، نه در فایل YAML. ویرایش compose.yaml هیچ تغییری در container موجود ایجاد نمیکند. docker compose restart نیز کمکی نمیکند، زیرا همان شیء container را متوقف و شروع میکند بدون اینکه به پیکربندی آن دست بزند. فقط docker compose up -d فایل را با containerهای در حال اجرا مقایسه میکند، تغییر سیاست را تشخیص میدهد و آنها را بازسازی میکند.
برای containerای که نمیخواهید همین الان بازسازی کنید، سیاست را در لحظه تغییر دهید:
docker update --restart unless-stopped my-containerهمچنان فایل YAML را نیز ویرایش کنید. docker update container زنده را تغییر میدهد و اجرای بعدی docker compose up -d فایل را میخواند و مقدار قدیمی را بازمیگرداند.
عملکرد دقیق هر مقدار restart
Docker چهار مقدار را تعریف میکند که تفاوت آنها تنها هنگام reboot شدن ماشین یا restart شدن daemon مشخص میشود.
noمقدار پیشفرض است. کانتینر تحت هیچ شرایطی بهطور خودکار restart نمیشود.alwaysکانتینر را هر زمان که متوقف شود، restart میکند. اگر آن را بهصورت دستی متوقف کرده باشید، دفعه بعد که Docker daemon شروع به کار کند، کانتینر دوباره بالا میآید. این رفتار اغلب غیرمنتظره است: کانتینری که هفته گذشته عمداً متوقف کردهاید، پس از reboot دوباره در حال اجراست.unless-stoppedرفتاری مشابهalwaysدارد، با این تفاوت که کانتینری که بهصورت دستی متوقف شده باشد، پس از restart شدن daemon همچنان متوقف باقی میماند. این مقداری است که برای سرویسی که گاهی برای نگهداری (maintenance) متوقف میکنید، به آن نیاز دارید.on-failureکانتینر را تنها زمانی restart میکند که با کد خروج غیر صفر (non-zero exit code) خارج شود. میتوانید تعداد تلاشها را همانطور که درrestart: on-failure:3آمده، محدود کنید.
برای پشتهای (stack) که باید هر زمان سرور روشن است در دسترس باشد، unless-stopped گزینه پیشفرض مناسبی است. تنها زمانی always را انتخاب کنید که میخواهید کانتینر در برابر متوقف ماندن مقاوم باشد.
چرا restart: on-failure پس از reboot کار نمیکند
بسیاری از افراد on-failure را انتخاب میکنند چون محتاطانه به نظر میرسد، اما پس از اولین reboot متوجه میشوند که تمام کانتینرها متوقف شدهاند. دلیل این موضوع در تعریف آن نهفته است. on-failure تنها به یک مورد واکنش نشان میدهد: خروج پروسهٔ کانتینر با یک کد خطا.
یک reboot خطا محسوب نمیشود. هنگامی که میزبان خاموش میشود، systemd سرویس docker.service را متوقف میکند و daemon هر کانتینر را بهطور عمدی متوقف میسازد. کانتینر دچار خطا نشده است، بنابراین این سیاست دلیلی برای واکنش ندارد. هنگام بالا آمدن مجدد، daemon کانتینرهایی را که باید از سر بگیرد بررسی میکند و یک کانتینر با سیاست on-failure که بهطور تمیز متوقف شده است، جزو آنها نیست. کانتینر در وضعیت exited باقی میماند.
شما میتوانید این موضوع را مستقیماً مشاهده کنید. مقدار restart: on-failure را روی یک سرویس تنظیم کنید، docker compose up -d را اجرا کنید، سیستم را reboot کنید و سپس دستور زیر را بزنید:
docker compose ps -aسرویس با وضعیت Exited و حالتی شبیه به Exited (0) 2 minutes ago فهرست میشود. هیچچیز خراب نشده و هیچ خطایی در لاگ ثبت نمیشود؛ همین موضوع تشخیص مشکل را دشوار میکند. این سیاست دقیقاً همان کاری را انجام داد که برای آن تعریف شده است.
سیاست on-failure همچنان مفید است. این سیاست برای کانتینری مناسب است که یک وظیفه (job) را اجرا میکند و ممکن است crash کند؛ جایی که شما تعداد محدودی تلاش مجدد میخواهید و نمیخواهید در حلقهٔ restart بیفتید. این ابزار برای زنده نگهداشتن یک سرویس طولانیمدت در طول rebootها، انتخاب اشتباهی است.
سیاستهای راهاندازی مجدد تنها در صورتی کار میکنند که سرویس Docker در زمان بوت اجرا شود
سیاستهای راهاندازی مجدد (Restart policies) توسط Docker daemon اعمال میشوند. اگر daemon اجرا نشود، هیچچیز اعمال نخواهد شد. وضعیت آن را بررسی کنید:
systemctl is-enabled docker
systemctl is-enabled containerdهر دو دستور باید enabled را چاپ کنند. بستههای موجود در مخزن رسمی Docker این موارد را در زمان نصب فعال میکنند، بنابراین در یک سرور تازه، این مرحله معمولاً با موفقیت انجام میشود. اگر هر کدام از دستورات disabled را چاپ کردند، آن را اصلاح کنید:
sudo systemctl enable --now docker containerdدر اینجا یک تله وجود دارد که درک آن ضروری است. اوبونتو همچنین docker.socket را ارائه میدهد که daemon را در زمان نیاز، یعنی اولین باری که چیزی با Docker API ارتباط برقرار میکند، اجرا مینماید. کاربران ممکن است ببینند که docker.socket فعال است، تصور کنند که daemon تحت پوشش است و برای صرفهجویی در حافظه، docker.service را غیرفعال کنند. در زمان بوت، هیچکس API را فراخوانی نمیکند، بنابراین سوکت هرگز لمس نمیشود، daemon هرگز شروع به کار نمیکند و تا زمانی که اولین دستور docker خود را تایپ نکنید، هیچ containerای بالا نمیآید. فعالسازی از طریق سوکت (Socket activation) جایگزینی برای فعال بودن docker.service نیست.
چه زمانی استفاده از systemd unit گزینه بهتری است
سیاستهای restart هیچ درکی از ترتیببندی نسبت به سایر بخشهای سیستم ندارند. daemon شروع به کار میکند و کانتینرهای شما را در اولین فرصت بالا میآورد. اگر stack شما یک دایرکتوری را از یک volume جداگانه، یک NFS (network file system) share، یا یک دیسک رمزنگاریشده bind-mount میکند، ممکن است کانتینرها پیش از آنکه آن مسیر وجود داشته باشد، شروع به کار کنند. Docker با خوشحالی یک دایرکتوری خالی در نقطه mount ایجاد کرده و کانتینر را روی آن اجرا میکند، و دیتابیس شما بدون هیچ دادهای بالا میآید.
زمانی که هر یک از موارد زیر صادق باشد، یک systemd unit بنویسید. stack شما به یک mount، یک رابط VPN، یا یک unit دیگر نیاز دارد که ابتدا آماده شود. شما میخواهید systemctl stop myapp و systemctl start myapp همانطور کار کنند که برای سایر سرویسهای موجود در سیستم کار میکنند. یا میخواهید stack در هنگام shutdown بهجای آنکه همراه با daemon کشته شود، بهصورت تمیز متوقف گردد. اگر با systemd unitها آشنا نیستید، نوشتن یک systemd service و timer فرمت فایل را با جزئیات بیشتری پوشش میدهد.
نوشتن unit فایل systemd
پشته (stack) را در یک مسیر ثابت خارج از دایرکتوری home قرار دهید. /srv/myapp انتخاب مناسبی است، زیرا unitای که پیش از ورود هر کاربری به سیستم اجرا میشود، نباید به /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یک unit سالم وضعیت Active: active (exited) را نشان میدهد. در اولین نگاه ممکن است این وضعیت اشتباه به نظر برسد، اما کاملاً صحیح است: Type=oneshot به همراه RemainAfterExit=yes به این معناست که unit دستور خود را اجرا کرده، دستور به پایان رسیده و systemd وضعیت unit را فعال نگه میدارد تا ExecStop در زمان خاموش شدن سیستم اجرا شود.
هر خط در این فایل اهمیت خاص خود را دارد. Requires=docker.service باعث میشود unit بهجای تلاش برای اجرای docker compose روی یک سوکت غیرفعال، سریعاً شکست را اعلام کند. After= ترتیب اجرا را تعیین میکند، زیرا Requires= بهتنهایی این کار را انجام نمیدهد. RequiresMountsFor= باعث میشود systemd واحد mount مربوط به آن مسیر را فراخوانی کرده و منتظر آماده شدن آن بماند؛ این دقیقاً همان دلیلی است که باعث میشود از یک unit بهجای سیاست restart استفاده کنیم. TimeoutStartSec=0 از متوقف شدن job راهاندازی توسط systemd در زمانی که یک image حجیم در حال دانلود است، جلوگیری میکند.
نکتهای درباره ترکیب این دو مکانیزم: مستندات Docker توصیه میکند سیاستهای restart را با یک مدیر پردازش میزبان (host process manager) ترکیب نکنید. این هشدار مربوط به مدیر پردازشی است که مستقیماً بر پردازش container نظارت کرده و در حالی که daemon سعی در restart کردن آن دارد، آن را دوباره اجرا میکند. یک unit از نوع Type=oneshot بر هیچچیز نظارت نمیکند، بنابراین نگه داشتن restart: unless-stopped در فایل compose در کنار این unit مشکلی ایجاد نمیکند و دقیقاً همان چیزی است که به آن نیاز دارید. systemd ترتیب را در زمان بوت مدیریت میکند و daemon نیز containerای را که ممکن است ساعت 3 صبح کرش کند، مدیریت خواهد کرد.
وقتی چیزی که قصد دارید آن را زنده نگه دارید یک پردازش طولانیمدت ساده است و نه یک پشته، ظاهر unit متفاوت خواهد بود؛ زیرا در آن حالت 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 تأیید میکند که شما در حال بررسی ماشینی هستید که واقعاً reboot شده است. دستور docker compose ps که از دایرکتوری stack اجرا میشود، باید تمام سرویسها را به عنوان running با uptime نزدیک به uptime ماشین فهرست کند. سرویسی که وضعیت Exited را نشان میدهد، همان موردی است که باید بررسی شود.
اگر سرویسی بالا نیامد، لاگ daemon بازهٔ زمانی بوت را پوشش میدهد:
journalctl -u docker.service -b --no-pager | tail -50برای stackهایی که توسط یک unit مدیریت میشوند، journalctl -u myapp.service -b --no-pager خروجی دقیق docker compose از زمان بوت را نشان میدهد، که شامل مواردی مثل شکست در pull کردن image یا نبود فایل .env است. راهاندازی مجددی که برنامهریزی میکنید همان چیزی است که باید زیر نظر بگیرید، بنابراین اجازه دهید unit در مورد مواردی که نظارت نمیکنید به شما اطلاع دهد: یک خط OnFailure= که به یک سرور ntfy خودمیزبان اشاره دارد، stackای را که پس از بوت بالا نیامده است به یک اعلان push تبدیل میکند تا اینکه بخواهید روزها بعد متوجه آن شوید.
مواردی که بهطور پنهانی باعث اختلال در شروع خودکار میشوند
کانتینرهایی که با docker compose run ایجاد میشوند، هرگز سیاست restart را از فایل دریافت نمیکنند. Compose با آنها بهعنوان کانتینرهای یکبارمصرف رفتار میکند. اگر به نظر میرسد سرویسی سیاست خود را نادیده میگیرد، بررسی کنید که آیا بهجای up با run شروع شده است یا خیر.
یک مسیر نسبی در یک volume یا در ورودی env_file، نسبت به دایرکتوری فایل compose تفسیر میشود. این حالت از طریق shell شما و همچنین از طریق unitای که WorkingDirectory را تنظیم میکند، بهدرستی کار میکند. اما در unitای که فاقد این تنظیم باشد، عملیات با شکست مواجه میشود، زیرا دایرکتوری کاری در آن حالت / است.
Docker بدون دسترسی root (Rootless) یک مورد مجزا است. daemon بهعنوان یک سرویس کاربری اجرا میشود و سرویس کاربری با پایان یافتن آخرین نشست (session) آن کاربر، متوقف میگردد. آن را برای کاربر فعال کنید و اجازه دهید حتی زمانی که هیچکس وارد سیستم نشده است، به اجرا ادامه دهد:
systemctl --user enable docker
sudo loginctl enable-linger $USERبدون enable-linger، daemon در حالت rootless هنگام خروج شما از سیستم (logout) متوقف میشود و کانتینرها نیز همراه با آن از کار میافتند؛ این وضعیت دقیقاً مانند یک سیاست restart معیوب به نظر میرسد.
نکته آخر اینکه، بهروزرسانیهای امنیتی خودکار میتوانند سرور را در ساعت مشخصی reboot کنند؛ این کار تنها زمانی مفید است که stack شما بهطور خودکار دوباره بالا بیاید. تنظیم این مورد در یک ماشین جدید، جزو کارهای اولیه است که باید در ده دقیقه اول روی یک VPS جدید انجام شود.
FAQ
تفاوت بین restart: always و restart: unless-stopped چیست؟
هر دو گزینه، کانتینر را در صورتی که خودبهخود متوقف شود، دوباره اجرا میکنند. تفاوت آنها زمانی مشخص میشود که کانتینر را بهصورت دستی متوقف کنید. با استفاده از always، کانتینر در مرتبهٔ بعدی که Docker daemon شروع به کار کند، دوباره اجرا میشود؛ بنابراین یک reboot، توقف دستی شما را خنثی میکند. با استفاده از unless-stopped، دیمون به یاد میآورد که کانتینر عمداً متوقف شده است و آن را به حال خود رها میکند. از unless-stopped استفاده کنید، مگر اینکه مشخصاً بخواهید کانتینری داشته باشید که هرگز در حالت توقف باقی نماند.
من restart: unless-stopped را اضافه کردم اما کانتینر پس از reboot شروع نمیشود. چرا؟
این سیاست روی کانتینر اعمال میشود، نه در فایل؛ و کانتینری که از قبل وجود دارد با ویرایش فایل 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 unit نیاز دارم؟
معمولاً خیر. یک سیاست restart برای استکی که فقط به شبکه نیاز دارد (که شامل اکثر استکها میشود) کافی است. زمانی یک unit اضافه کنید که کانتینرها به چیزی وابسته باشند که هنگام شروع به کار Docker daemon هنوز آماده نیست؛ مانند دیسک خارجی، volume رمزنگاریشده، NFS share یا رابط VPN. این unit به شما امکان میدهد از طریق After= و RequiresMountsFor= ترتیب راهاندازی را کنترل کنید، کاری که سیاست restart قادر به انجام آن نیست.
چگونه یک استک را بهطور دائمی متوقف کنم تا پس از reboot دوباره اجرا نشود؟
با استفاده از unless-stopped، دستور docker compose stop کافی است، زیرا کانتینری که بهصورت دستی متوقف شده باشد، هنگام restart شدن دیمون دوباره اجرا نمیشود. با استفاده از always، یک توقف ساده کافی نیست و کانتینر پس از reboot بازمیگردد. یا دستور docker compose down را اجرا کنید که کانتینرها را حذف میکند، یا ابتدا سیاست را با docker update --restart no my-container تغییر دهید. اگر یک systemd unit مدیریت استک را بر عهده دارد، دستور sudo systemctl disable myapp.service را نیز اجرا کنید، در غیر این صورت unit آن را دوباره اجرا خواهد کرد.