آموزش سرور

آموزش ساخت سرویس systemd؛ اجرای خودکار و دائمی هر برنامه روی سرور لینوکس

تهیه: کلودیفای (نگارش با کمک هوش مصنوعی) · انتشار: · زمان مطالعه: ۷ دقیقه

root@server: ~$ systemctl daemon-reload$ systemctl enable --now myappCreated symlink /etc/systemd/system/mult$ systemctl status myapp --no-pager | he* myapp.service - My Python App Loaded: loaded (/etc/systemd/system/msystemdآموزش سرورآموزش ساخت سرویسsystemd؛ اجرای خودکار و دائمیهر برنامه روی سرور لینوکسمجلهٔ کلودیفایclodify.net/blog
پاسخ کوتاه

برای اینکه برنامه‌ای روی سرور همیشه اجرا بماند، پس از بوت خودکار شروع شود و در صورت خطا دوباره راه بیفتد، یک فایل سرویس در /etc/systemd/system بسازید. بخش‌های اصلی آن ExecStart (دستور اجرا)، WorkingDirectory، User و Restart=on-failure است. سپس با systemctl daemon-reload، systemctl enable --now و journalctl -u سرویس را فعال و لاگ آن را بررسی کنید. این روش از nohup و screen بسیار مطمئن‌تر است.

چرا nohup و screen کافی نیستند؟

بسیاری از کاربران برای اجرای دائمی یک اسکریپت پایتون، ربات یا برنامهٔ Node.js از nohup یا اجرای برنامه داخل screen استفاده می‌کنند. این روش‌ها برای آزمایش سریع خوب‌اند، اما برای سرویس واقعی سه ضعف جدی دارند: اگر برنامه به‌دلیل خطا متوقف شود کسی دوباره اجرایش نمی‌کند، پس از ری‌استارت سرور برنامه دیگر بالا نمی‌آید، و لاگ‌ها پراکنده و بدون مدیریت هستند.

systemd مدیر سرویس پیش‌فرض تقریباً همهٔ توزیع‌های امروزی لینوکس است. همان ابزاری که Nginx، SSH و دیتابیس را اجرا می‌کند، می‌تواند برنامهٔ شما را هم مدیریت کند: اجرا هنگام بوت، اجرای دوباره پس از خطا، اجرا با کاربر محدود، ثبت لاگ متمرکز و حتی محدود کردن رم و CPU.

چرخهٔ زندگی یک سرویس systemd
شروعExecStartstdout/stderrبوت سرورmulti-user.targetsystemdخواندن myapp.serviceبرنامهٔ شمابا کاربر myappjournaldثبت خروجی و خطاشروعExecStartstdout/stderrبوت سرورmulti-user.targetsystemdخواندن myapp.serviceبرنامهٔ شمابا کاربر myappjournaldثبت خروجی و خطا
شکل ۱ systemd برنامه را هنگام بوت اجرا می‌کند، خروجی را در journald ثبت می‌کند و در صورت خطا دوباره راهش می‌اندازد.

ساختار یک فایل سرویس (Unit File)

فایل‌های سرویس سفارشی در /etc/systemd/system/ قرار می‌گیرند و پسوند .service دارند. فرض کنید یک برنامهٔ پایتونی در /opt/myapp با محیط مجازی venv دارید. فایل زیر را بسازید:

sudo nano /etc/systemd/system/myapp.service
[Unit]
Description=My Python App
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=myapp
Group=myapp
WorkingDirectory=/opt/myapp
EnvironmentFile=/opt/myapp/.env
ExecStart=/opt/myapp/venv/bin/python main.py
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target

این فایل سه بخش دارد:

  • [Unit]: توضیح سرویس و ترتیب اجرا. After=network-online.target یعنی سرویس پس از آماده شدن شبکه اجرا شود؛ برای برنامه‌هایی که به اینترنت یا دیتابیس وصل می‌شوند مهم است.
  • [Service]: نحوهٔ اجرا. ExecStart باید مسیر کامل فایل اجرایی را داشته باشد؛ systemd مثل ترمینال شما PATH کامل ندارد. Restart=on-failure یعنی اگر برنامه با خطا خارج شد، پس از RestartSec ثانیه دوباره اجرا شود.
  • [Install]: مشخص می‌کند سرویس هنگام فعال‌سازی در کدام مرحلهٔ بوت قرار بگیرد. multi-user.target حالت عادی سرور است.

اجرا با کاربر جداگانه، نه root

اجرای برنامه با کاربر root یعنی اگر برنامه آسیب‌پذیری داشته باشد، مهاجم کل سرور را در اختیار می‌گیرد. برای هر برنامه یک کاربر سیستمی بدون امکان ورود بسازید و مالکیت پوشه را به او بدهید:

sudo useradd --system --home /opt/myapp --shell /usr/sbin/nologin myapp
sudo chown -R myapp:myapp /opt/myapp

رمزها و توکن‌ها را در فایل .env بگذارید و دسترسی‌اش را محدود کنید (chmod 600). EnvironmentFile محتوای آن را به‌صورت متغیر محیطی به برنامه می‌دهد؛ هر خط به شکل KEY=value.

فعال‌سازی و مدیریت سرویس

هر بار که فایل سرویس را می‌سازید یا تغییر می‌دهید، systemd باید آن را دوباره بخواند:

sudo systemctl daemon-reload
sudo systemctl enable --now myapp     # فعال برای بوت + اجرای فوری
sudo systemctl status myapp

دستورهای روزمره:

sudo systemctl restart myapp   # ری‌استارت پس از به‌روزرسانی کد
sudo systemctl stop myapp      # توقف
sudo systemctl disable myapp   # غیرفعال برای بوت
systemctl is-active myapp      # فقط وضعیت (برای اسکریپت‌ها)
systemctl list-units --type=service --state=failed   # سرویس‌های خراب
root@server: ~
$ systemctl daemon-reload$ systemctl enable --now myappCreated symlink /etc/systemd/system/multi-user.target.wants/myapp.service ...$ systemctl status myapp --no-pager | head -n 3* myapp.service - My Python App   Loaded: loaded (/etc/systemd/system/myapp.service; enabled)   Active: active (running) since ...$ 
شکل ۲ enable --now سرویس را هم برای بوت فعال می‌کند و هم بلافاصله اجرا می‌کند.

خواندن لاگ‌ها با journalctl

هر چیزی که برنامه در خروجی استاندارد یا خطا چاپ کند، به‌طور خودکار در journald ذخیره می‌شود. دیگر نیازی به فایل لاگ جداگانه ندارید:

journalctl -u myapp -n 100 --no-pager   # صد خط آخر
journalctl -u myapp -f                  # دنبال کردن زنده
journalctl -u myapp --since "1 hour ago"
journalctl -u myapp -p err              # فقط خطاها

نکته برای برنامه‌های پایتونی: پایتون خروجی را بافر می‌کند و ممکن است لاگ‌ها با تأخیر ظاهر شوند. با افزودن Environment=PYTHONUNBUFFERED=1 به بخش Service این مشکل حل می‌شود.

انتخاب Type مناسب برای سرویس

مقدار Type به systemd می‌گوید چطور بفهمد برنامه «آماده» شده است. انتخاب اشتباه آن یکی از دلایل رفتارهای عجیب سرویس‌هاست:

  • simple: پیش‌فرض و مناسب بیشتر برنامه‌های امروزی؛ systemd فرض می‌کند برنامه به محض اجرای ExecStart آماده است و برنامه در پیش‌زمینه می‌ماند. ربات‌ها، برنامه‌های پایتون، Node.js و Gunicorn در این دسته‌اند.
  • exec: شبیه simple، با این تفاوت که systemd منتظر می‌ماند فایل اجرایی واقعاً اجرا شود؛ اگر مسیر اشتباه باشد، خطا همان لحظه گزارش می‌شود.
  • forking: برای برنامه‌های قدیمی که خودشان به پس‌زمینه می‌روند (daemonize) و فرایند اصلی‌شان خارج می‌شود. اگر برنامه‌ای را که به پس‌زمینه می‌رود با simple اجرا کنید، systemd گمان می‌کند برنامه بسته شده و آن را دوباره اجرا می‌کند.
  • oneshot: برای کارهایی که یک بار اجرا و تمام می‌شوند، مثل اسکریپت بکاپ یا تنظیم اولیه. معمولاً همراه تایمر به کار می‌رود.
  • notify: برنامه خودش به systemd پیام «آماده‌ام» می‌دهد؛ برخی سرویس‌های حرفه‌ای از این حالت پشتیبانی می‌کنند.

قاعدهٔ عملی: اگر برنامه را در ترمینال اجرا می‌کنید و ترمینال تا پایان کار برنامه مشغول می‌ماند، Type=simple درست است. اگر بلافاصله به خط فرمان برمی‌گردید و برنامه در پس‌زمینه ادامه می‌دهد، یا گزینهٔ «اجرا در پیش‌زمینه» برنامه را فعال کنید یا از forking استفاده کنید.

روند پیشنهادی به‌روزرسانی برنامه

وقتی کد جدید را روی سرور می‌برید، این ترتیب ساده را رعایت کنید تا قطعی کوتاه و قابل‌کنترل باشد: ابتدا کد و وابستگی‌ها را در پوشهٔ برنامه به‌روز کنید، سپس سرویس را ری‌استارت کنید و بلافاصله لاگ را چند ثانیه دنبال کنید تا از سالم بودن اجرا مطمئن شوید. اگر خطا دیدید، نسخهٔ قبلی را برگردانید و دوباره ری‌استارت کنید. نگه داشتن کد در گیت و برچسب زدن نسخه‌ها، این برگشت را به یک دستور تبدیل می‌کند.

گزینه‌های پیشرفته و کاربردی

محدود کردن منابع: برای جلوگیری از اینکه یک برنامهٔ معیوب کل سرور را از کار بیندازد:

MemoryMax=512M
CPUQuota=50%

جلوگیری از حلقهٔ ری‌استارت بی‌پایان: اگر برنامه بلافاصله پس از اجرا خطا بدهد، systemd پس از چند تلاش پشت‌سرهم آن را رها می‌کند. این رفتار با StartLimitIntervalSec و StartLimitBurst در بخش Unit قابل تنظیم است. اگر سرویس در وضعیت failed ماند، پس از رفع مشکل با systemctl reset-failed myapp شمارنده را صفر کنید.

سخت‌سازی امنیتی: چند گزینهٔ ساده دسترسی برنامه را به سیستم محدود می‌کند:

NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=full
ProtectHome=true

با systemd-analyze security myapp می‌توانید ارزیابی امنیتی سرویس را ببینید و گزینه‌های بیشتر را بشناسید.

برنامه‌های Node.js: همین الگو کار می‌کند؛ فقط ExecStart را به مسیر کامل node تغییر دهید (/usr/bin/node /opt/app/server.js). مسیر را با which node پیدا کنید. اگر از nvm استفاده می‌کنید، مسیر در پوشهٔ خانهٔ کاربر است و باید دقیقاً همان را بنویسید.

تایمرهای systemd به‌جای cron

systemd می‌تواند کارهای زمان‌بندی‌شده را هم اجرا کند. برای این کار یک سرویس با Type=oneshot و یک فایل .timer هم‌نام می‌سازید:

# /etc/systemd/system/backup.timer
[Unit]
Description=Nightly backup

[Timer]
OnCalendar=*-*-* 03:30:00
Persistent=true

[Install]
WantedBy=timers.target

سپس sudo systemctl enable --now backup.timer و با systemctl list-timers زمان اجرای بعدی را ببینید. Persistent=true باعث می‌شود اگر سرور در زمان مقرر خاموش بود، کار پس از روشن شدن اجرا شود.

nohup/screen یا systemd؟
nohup و screen
  • پس از خطا دوباره اجرا نمی‌شود
  • پس از ری‌استارت سرور اجرا نمی‌شود
  • لاگ پراکنده یا گم‌شده
  • معمولاً با root اجرا می‌شود
در برابر
سرویس systemd
  • Restart خودکار پس از خطا
  • اجرای خودکار هنگام بوت
  • لاگ متمرکز در journalctl
  • کاربر محدود و سقف منابع
شکل ۳ برای هر برنامه‌ای که باید دائماً روشن بماند، سرویس systemd انتخاب درست است.

عیب‌یابی رایج

  • status=203/EXEC: مسیر ExecStart اشتباه است یا فایل مجوز اجرا ندارد. مسیر کامل را بررسی کنید.
  • status=200/CHDIR: پوشهٔ WorkingDirectory وجود ندارد یا کاربر سرویس به آن دسترسی ندارد.
  • سرویس اجرا می‌شود ولی برنامه به اینترنت یا دیتابیس وصل نمی‌شود: ترتیب After و Wants را بررسی کنید و مطمئن شوید متغیرهای محیطی از EnvironmentFile خوانده می‌شوند.
  • تغییرات اعمال نمی‌شود: پس از ویرایش فایل سرویس، daemon-reload و سپس restart را فراموش نکنید.

برای نمونهٔ واقعی این روش در اجرای ربات، آموزش اجرای دائمی ربات تلگرام با systemd را ببینید. دستورهای پایه‌ای که در این مسیر لازم دارید در دستورات پرکاربرد لینوکس جمع شده‌اند و اقدامات امنیتی تکمیلی در چک‌لیست امنیت سرور جدید آمده است. اگر برای اجرای سرویس‌های دائمی خود سرور می‌خواهید، سرور مجازی ایران و سرور خارج کلودیفای با دسترسی کامل root تحویل داده می‌شوند.

سؤالات متداول

تفاوت Restart=always و Restart=on-failure چیست؟

on-failure فقط وقتی برنامه با خطا خارج شود یا kill شود آن را دوباره اجرا می‌کند. always در هر حالتی، حتی خروج عادی، برنامه را دوباره اجرا می‌کند. برای سرویس‌هایی که هرگز نباید متوقف بمانند always و برای بقیه on-failure مناسب است.

چرا سرویس من بعد از ری‌استارت سرور اجرا نمی‌شود؟

احتمالاً فقط start کرده‌اید و enable نکرده‌اید. دستور systemctl enable سرویس را به مرحلهٔ بوت اضافه می‌کند. با systemctl is-enabled نام‌سرویس وضعیت را بررسی کنید.

systemd بهتر است یا PM2 برای Node.js؟

هر دو برنامه را دائمی نگه می‌دارند. PM2 امکاناتی مثل حالت cluster و مانیتورینگ مخصوص Node.js دارد، اما systemd روی هر سرور لینوکسی موجود است و نیاز به ابزار اضافه ندارد. برای یک سرویس ساده systemd کافی است.

فایل سرویس را کجا قرار دهم؟

سرویس‌های سفارشی را در /etc/systemd/system قرار دهید. پوشهٔ /lib/systemd/system مخصوص بسته‌های نصب‌شده است و ممکن است هنگام به‌روزرسانی بازنویسی شود.

آماده‌اید؟ سرورتان را بسازید.

از ۱٬۲۰۰ تومان در ساعت · پرداخت ریالی

سرورتان را همین حالا بسازید.

ساعتی بپردازید. هر وقت خواستید حذف کنید.