ساخت سرویس systemd برای اپلیکیشن؛ از فایل unit تا ری استارت خودکار
سرور ریبوت شد و سایتت نیومد بالا؟ با یه فایل unit ساده برنامه Go یا Node رو موقع بوت اجرا کن، بعد از کرش دوباره بالا بیار و لاگش رو با journalctl بخون.

برای ساخت سرویس systemd فقط یه فایل unit با سه بخش [Unit]، [Service] و [Install] توی /etc/systemd/system/ لازم داری. بعدش با systemctl enable --now فعالش می کنی و لاگ هاش رو با journalctl -u می بینی. با Restart=on-failure اگه برنامه کرش کنه دوباره بالا میاد و با enable بعد از ریبوت سرور هم خودکار اجرا می شه. آخر این راهنما یه سرویس Go یا Node داری که خودش بالا میاد و لاگش رو می تونی بخونی.
یکی از سوال های تکراری پشتیبانی همینه: «سرور ریبوت شد و سایتم نیومد بالا، چرا؟» جواب معمولاً اینه که برنامه با دست اجرا شده بوده و کسی به سیستم نگفته بعد از بوت باید دوباره اجراش کنه.
چرا nohup و screen کافی نیست
فرض کن API فروشگاه آنلاینت یا ربات تلگرامت رو با nohup node app.js & روی VPS اجرا کردی. همه چیز خوبه تا شبی که سرور برای به روزرسانی امنیتی ریبوت می شه. صبح مشتری ها می پرسن چرا سفارش ثبت نمی شه و تو تازه می فهمی برنامه از ساعت سه نصفه شب خاموشه.
اجرای دستی مثل روشن کردن ژنراتور با دست می مونه: تا کنارش وایستی کار می کنه. systemd مثل یه سرایدار خوب ساختمونه. صبح در رو باز می کنه، اگه برق رفت و برگشت همه چیز رو دوباره روشن می کنه و برای هر اتفاقی گزارش می نویسه.
بدون هیچ ابزار اضافه، systemd این سه کار رو برات انجام می ده:
- برنامه رو موقع بوت سرور اجرا می کنه.
- بعد از کرش دوباره بالاش میاره.
- خروجی برنامه رو توی یه جای مشخص جمع می کنه، نه توی یه
nohup.outکه هر روز بزرگ تر می شه.
screen و tmux هم مشکل خودشون رو دارن. با ریبوت سشن از بین می ره و کرش برنامه هم کسی رو خبر نمی کنه.
قبل از شروع چی لازم داری
یه چک لیست کوتاه:
- یه سرور لینوکسی با دسترسی
sudo. دستورهای این راهنما روی اوبونتو و دبیان یکسانه. اگه هنوز توزیع رو انتخاب نکردی، مقایسه Ubuntu و Debian برای سرور کمکت می کنه. - باینری Go یا پروژه Node که دستی اجرا می شه و جواب می ده.
- یه مسیر مشخص برای برنامه، مثلاً
/opt/myapp.
هاست اشتراکی معمولاً دسترسی root و systemd نمی ده. برای این کار یه سرور مجازی ایران لازم داری.
ساخت سرویس systemd؛ فایل unit رو این طوری بنویس
اول یه کاربر جدا برای برنامه بساز. اگه برنامه با root اجرا بشه و یه روز باگی توش پیدا بشه، کل سرور دست مهاجمه.
sudo useradd --system --no-create-home --shell /usr/sbin/nologin myapp
sudo chown -R myapp:myapp /opt/myapp
بعد فایل /etc/systemd/system/myapp.service رو با sudo nano یا ویرایشگر دلخواهت بساز. هر بخش یه کار مشخص داره:
[Unit]: توضیح سرویس (Description) و ترتیب اجرا (After=network.target).[Service]: دستور اجرا، کاربر و پوشه کاری.[Install]: باWantedBy=multi-user.targetمی گی سرویس توی بوت عادی سرور فعال بشه.
نمونه برای سرویس Go
فرض کن باینری کامپایل شده در /opt/myapp/myapp قرار داره:
[Unit]
Description=My Go API
After=network.target
[Service]
Type=simple
User=myapp
Group=myapp
WorkingDirectory=/opt/myapp
ExecStart=/opt/myapp/myapp
Restart=on-failure
RestartSec=5
EnvironmentFile=/etc/myapp/myapp.env
[Install]
WantedBy=multi-user.target
ExecStart باید مسیر مطلق داشته باشه. systemd شل تو رو نداره و ./myapp براش معنی نداره. فایل /etc/myapp/myapp.env هم باید وجود داشته باشه، وگرنه سرویس بالا نمیاد. اگه فعلاً متغیری نداری، خط EnvironmentFile رو بردار.
یه نکته ایرانی: باینری رو روی سیستم خودت یا CI بیلد کن و آپلودش کن. این طوری سرور برای دانلود ماژول ها به اینترنت بین الملل وابسته نیست، که گاهی کنده یا قطع و وصل می شه.
برای متغیرهای حساس مثل رمز دیتابیس از EnvironmentFile استفاده کن. فایل رو با chmod 600 ببند تا فقط صاحبش بخونتش. رمز رو مستقیم توی فایل unit ننویس، چون فایل unit و خروجی systemctl show رو هر کاربر سرور می تونه ببینه.
نمونه برای سرویس Node
[Unit]
Description=My Node App
After=network.target
[Service]
Type=simple
User=myapp
WorkingDirectory=/opt/mynode
ExecStart=/usr/bin/node /opt/mynode/server.js
Environment=NODE_ENV=production
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
مسیر node رو با which node پیدا کن و همون رو بنویس. NODE_ENV=production رو هم با Environment= بده، وگرنه بعضی کتابخونه ها توی حالت توسعه بالا میان.
اشتباه رایج: nvm. اگه Node رو با nvm نصب کرده باشی، باینری توی پوشه home همون کاربره، یعنی چیزی مثل ~/.nvm/versions/node/.../bin/node. systemd فایل های .bashrc و .profile رو نمی خونه، پس PATH اون چیزی نیست که توی ترمینال می بینی. یا مسیر کامل همون باینری رو بنویس، یا Node رو سراسری نصب کن. اگه بعدها ProtectHome رو هم فعال کنی، مسیر داخل home اصلاً در دسترس نمی مونه.
پکیج های پروژه رو هم بهتره با npm ci روی سیستم خودت یا CI نصب کنی و پوشه آماده رو آپلود کنی. دلیلش همون وابستگی به اینترنت جهانیه.

فعالش کن و اولین بار اجرا کن
فایل رو که ذخیره کردی، این سه دستور رو بزن:
sudo systemctl daemon-reload
sudo systemctl enable --now myapp
sudo systemctl status myapp
daemon-reload به systemd می گه فایل های unit رو دوباره بخونه. بدون اون، سرویس جدیدت رو نمی شناسه. enable یعنی «بعد از ریبوت هم بالا بیا» و --now یعنی «همین الان هم شروع کن». این دو تا کار جدان و اگه فقط start بزنی، با اولین ریبوت برنامه خاموش می مونه.
توی خروجی status دنبال عبارت active (running) بگرد. اگه failed دیدی، نترس. مرحله بعد دقیقاً برای همینه.
اول لاگ رو بخون، بعد حدس بزن
systemd خروجی استاندارد و خطای برنامه رو خودش توی journal جمع می کنه. نیازی نیست توی برنامه فایل لاگ بسازی.
# صد خط آخر
sudo journalctl -u myapp -n 100 --no-pager
# دنبال کردن زنده، مثل tail -f
sudo journalctl -u myapp -f
# فقط نیم ساعت اخیر
sudo journalctl -u myapp --since "30 min ago"
# فقط خطاها
sudo journalctl -u myapp -p err
# فقط بوت فعلی
sudo journalctl -u myapp -b
دو تا نکته هست که زیاد گیر آدم می ندازه:
- ساعت سرور. خیلی از سرورها روی UTC تنظیم هستن. اگه دنبال خطای ساعت نه شب می گردی ولی لاگ ها ساعت دیگه ای رو نشون می دن، اشتباه از تو نیست. با
timedatectlساعت و منطقه زمانی سرور رو ببین. - حجم لاگ. با
journalctl --disk-usageمی فهمی journal چقدر جا گرفته. اگه زیاد شد،sudo journalctl --vacuum-size=500Mقدیمی ترها رو پاک می کنه. ذخیره دائمی لاگ به تنظیمات سرور بستگی داره و روی بعضی نصب ها بعد از ریبوت لاگ های قبلی نمی مونن.
اگه برنامه ات لاگ ساختارمند می نویسه، عالیه. فقط یادت باشه روی stdout و stderr بنویسه، نه فقط توی فایل.
ری استارت خودکار؛ چه موقع و چقدر
Restart تعیین می کنه systemd بعد از مرگ برنامه چی کار کنه:
| مقدار | چه موقع ری استارت می کنه | کجا به درد می خوره |
|---|---|---|
no | هیچ وقت | کارهای یک بار مصرف |
on-failure | خروج با خطا، کرش، تایم اوت | بیشتر API ها و ربات ها |
always | همیشه، حتی خروج عادی | سرویس هایی که نباید هیچ وقت خاموش بمونن |
برای بیشتر برنامه ها on-failure انتخاب درستیه. اگه خودت با systemctl stop برنامه رو متوقف کنی، با هیچ کدوم از این حالت ها دوباره بالا نمیاد. systemd می فهمه این خاموشی عمدی بوده.
RestartSec=5 فاصله بین کرش و تلاش دوباره است. بدون اون، برنامه ای که موقع بالا اومدن کرش می کنه توی یه حلقه سریع گیر می افته و CPU رو می خوره. اگه دیتابیس یا یه سرویس بیرونی موقتاً قطع باشه، پنج ثانیه مکث بهش فرصت نفس کشیدن می ده.
حلقه کرش رو هم می شه محدود کرد. این دو خط رو توی بخش [Unit] بذار:
StartLimitIntervalSec=60
StartLimitBurst=5
یعنی اگه توی یه دقیقه بیشتر از پنج بار تلاش برای بالا اومدن شکست بخوره، systemd دست می کشه و وضعیت سرویس failed می شه. بعد از رفع مشکل باید sudo systemctl reset-failed myapp بزنی و دوباره start کنی. اگه پیام start request repeated too quickly دیدی، دلیلش همینه.
حالا یه آزمایش ساده: برنامه رو عمداً بکش.
sudo systemctl show -p MainPID myapp
sudo kill -9 <شماره PID>
sudo systemctl status myapp
اگه بعد از چند ثانیه دوباره active (running) دیدی، ری استارت خودکار کار می کنه.

ترتیب بالا اومدن؛ وقتی برنامه به دیتابیس نیاز داره
بعد از ریبوت، همه سرویس ها تقریباً هم زمان بالا میان. اگه API تو قبل از آماده شدن PostgreSQL یا Redis شروع بشه، اولین اتصالش خطا می ده و کرش می کنه. Restart معمولاً این رو جبران می کنه، ولی بهتره ترتیب رو هم مشخص کنی:
[Unit]
After=network.target postgresql.service
Wants=postgresql.service
After فقط ترتیب شروع رو مشخص می کنه و تضمین نمی کنه دیتابیس واقعاً آماده پذیرش اتصاله. برای همین کد برنامه هم باید موقع اتصال چند بار دوباره تلاش کنه. اگه هنوز بین دیتابیس ها مردد هستی، مقایسه PostgreSQL و MySQL می تونه کمکت کنه.
برای ورکر صف هم همین منطق برقراره. اگه کارهای سنگین مثل ارسال پیامک یا ساخت PDF رو توی صف پس زمینه می ذاری، ورکر رو یه unit جدا تعریف کن تا ری استارت و لاگش از API جدا باشه.
اگه کل پروژه ات با Docker Compose بالا میاد، لازم نیست برای هر کانتینر unit بنویسی. Docker Compose برای پروژه کوچک توضیح داده این راه کی کافیه.
خطاهای رایج و معنی شون
وقتی سرویس بالا نمیاد، اول status و بعد journalctl رو نگاه کن. این کدها بیشتر از بقیه دیده می شن:
status=203/EXEC: مسیرExecStartغلطه، فایل وجود نداره یا اجازه اجرا نداره. مسیر رو باls -lچک کن و اگه لازم بودchmod +xبزن.status=200/CHDIR: پوشهWorkingDirectoryوجود نداره یا کاربر سرویس بهش دسترسی نداره.status=217/USER: کاربرUser=رو نساختی یا اسمش رو اشتباه نوشتی.EADDRINUSE: پورت از قبل اشغاله. معمولاً یه نمونه قدیمی که باnohupاجرا کرده بودی هنوز زنده است. باsudo ss -tlnp | grep 3000ببین کی پورت رو گرفته.Permission deniedروی پورت ۸۰ یا ۴۴۳: کاربر غیر root به صورت پیش فرض نمی تونه پورت های زیر ۱۰۲۴ رو باز کنه. بهترین راه اینه که برنامه روی یه پورت بالا بمونه و Nginx جلوش باشه. راه دومAmbientCapabilities=CAP_NET_BIND_SERVICEتوی[Service]است.- تغییر دادی ولی اثر نکرد: بعد از هر ویرایش فایل unit باید
sudo systemctl daemon-reloadو بعدشrestartبزنی.
یه مورد هم از سمت کسب و کار: اگه سرویس شبانه افتاده باشه، درگاه پرداخت نمی تونه نتیجه تراکنش رو به سرور تو برگردونه. اگه بعد از رفع مشکل سفارش ها هنوز ثبت نمی شن، عیب یابی بازگشت از درگاه پرداخت رو هم نگاه کن.
چند تنظیم امنیتی که هزینه ای ندارن
بعد از این که سرویس پایدار شد، چند خط به بخش [Service] اضافه کن:
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=full
ProtectHome=true
این ها دسترسی برنامه به بقیه سیستم رو محدود می کنن. یعنی اگه یه روز کسی از راه باگ برنامه وارد شد، نمی تونه به راحتی کل سرور رو زیر و رو کنه. یکی یکی اضافه کن و بعد از هر کدوم برنامه رو تست کن. مثلاً ProtectHome=true اگه برنامه ات چیزی از home بخونه یا Node رو با nvm نصب کرده باشی، مشکل درست می کنه. ProtectSystem=strict هم تقریباً همه جا رو فقط خواندنی می کنه و باید مسیرهای قابل نوشتن رو خودت با ReadWritePaths= مشخص کنی.
اگه نمی خوای خودت درگیر این جزئیات بشی، تیم ما این کار رو توی مدیریت و امن سازی سرور انجام می ده.
نسخه جدید رو چطور بالا ببری
برای نسخه جدید، فایل رو آپلود می کنی و sudo systemctl restart myapp می زنی. سه نکته داره:
- باینری Go رو روی فایل در حال اجرا کپی نکن. خطای
text file busyمی گیری. اول با یه اسم موقت آپلودش کن و بعد باmvجایگزین کن. - ساعت اوج ترافیک ری استارت نکن، مثلاً شب های پرفروش. چند ثانیه قطعی هم برای درخواست های در حال انجام مهمه.
- اگه هر بار این مراحل رو دستی می زنی، وقتشه خودکارش کنی. استقرار خودکار با CI/CD همین مسیر رو از push تا ری استارت سرویس پوشش می ده.
سوالات متداول
فرق enable و start چیه؟
start سرویس رو همین الان اجرا می کنه ولی بعد از ریبوت اثری نداره. enable فقط تعیین می کنه سرویس موقع بوت بالا بیاد و خودش چیزی رو اجرا نمی کنه. برای همین معمولاً enable --now می زنیم تا هر دو کار با هم انجام بشه.
تفاوت restart و reload چیه؟
restart برنامه رو کامل متوقف و دوباره اجرا می کنه، پس چند لحظه قطعی داری. reload فقط وقتی کار می کنه که برنامه سیگنال بارگذاری مجدد تنظیمات رو پشتیبانی کنه و توی unit هم ExecReload تعریف کرده باشی. اگه مطمئن نیستی، restart امن تره.
چرا بعد از ریبوت سرویس بالا نیومد؟
اول با systemctl is-enabled myapp چک کن که enable شده باشه. اگه شده بود، journalctl -u myapp -b رو بخون. معمولاً دلیلش وابستگی به دیتابیس یا مسیری است که بعد از بوت هنوز آماده نبوده، یا دسترسی فایلی که فقط توی ترمینال خودت کار می کرد.
لاگ ها تا کی می مونن؟
بستگی به تنظیم journal و فضای دیسک داره. روی بعضی سرورها بعد از ریبوت پاک می شن و روی بعضی ذخیره دائمی هست. با journalctl --disk-usage حجم فعلی رو ببین و با --vacuum-size یا --vacuum-time کنترلش کن.
قبل از بستن ترمینال این سه تا رو چک کن
ساخت سرویس systemd از یه فایل کوچیک شروع می شه، ولی نتیجه اش اینه که دیگه شب ها نگران ریبوت سرور نیستی. قبل از رفتن سراغ کار بعدی، این سه تا رو امتحان کن:
systemctl is-enabled myappرو بزن و مطمئن شو جوابenabledباشه.- برنامه رو یه بار با
kill -9بکش و ببین خودش برمی گرده. - یه ریبوت واقعی بزن و بعدش
statusوjournalctl -u myapp -bرو نگاه کن.
اگه هر سه تا پاس شد، دیگه لازم نیست نگران ساعت سه نصفه شب و ریبوت های ناخواسته باشی. اگه هم جایی گیر کردی، خروجی journalctl رو برای تیم پشتیبانی فنی بفرست. با همون چند خط معمولاً مشکل سریع پیدا می شه.


