نصب نسخهٔ برنامه با مالکیت درست؛ از پوشهٔ ورودی تا اجرای غیرریشه
در این درس ۴ بخش
تا اینجا فایلهای بررسیشده در خانهٔ مدیر قرار دارند. اکنون باید بین «کسی که برنامه را منتشر میکند» و «هویتی که برنامه با آن اجرا میشود» فاصله بگذاریم. مدیر با sudo اجازهٔ نصب فایل و تغییر سرویس دارد؛ فرایند برنامه برای پاسخدادن به درخواستها به چنین اختیاری نیاز ندارد. اگر هر دو یک هویت باشند، خطای برنامه یا سوءاستفاده از آن ممکن است به تغییر کد و تنظیمات اصلی هم برسد. در این درس یک حساب سیستمی به نام darsnode میسازیم، نسخهٔ r1 را در /opt/darsavand-node/releases/r1 نصب میکنیم و آن را با پیوند current انتخاب میکنیم. تمام فرمانهای این درس، مگر جایی که صریحاً گفته شود، در Bash نشست SSH همان اوبونتوی آزمایشی اجرا میشوند. این دستورها برای سرور مشترک یا مسیر قدیمی دارای داده طراحی نشدهاند.
اگر مفهوم مالک و مجوز هنوز روشن نیست، ابتدا درس «مجوزها، مالکیت و استفادهٔ سنجیده از sudo» را باز کنید: https://www.darsavand.com/courses/linux-server-from-zero?lesson=a6cafdad-8b05-44a8-bfc6-f7ba647ee095
برای تفاوت مسیر کاری با محل ماژول نیز درس فایل و مسیر Node را ببینید: https://www.darsavand.com/courses/nodejs-windows-backend-start?lesson=08d325f8-3c97-4498-9f94-984029361479
این ارجاعها برای رفع پیشنیازند؛ ادامهٔ درس فرض میکند میتوانید توضیح دهید چرا مجوز نوشتن در پوشه با مجوز خواندن یک فایل فرق دارد. مدل برنامه هنوز همان #object و #validation-contracts درس قبل را دارد. کار ما در اینجا تغییر منطق پذیرش title نیست؛ تعیین این است که آن کد از کجا خوانده شود و با چه اختیاری به فایلسیستم دسترسی داشته باشد. این دو نوع قرارداد باید جداگانه بررسی شوند.
نسخه، پوشهٔ انتشار و پیوند current چه هستند؟
یک release در این دوره مجموعهای مشخص از کدهای پذیرفتهشده است که پس از نصب، در همان محل ویرایش نمیکنیم. r1 نام اولین مجموعه است، نه شمارهٔ نسخهٔ Node و نه شناسهٔ درخواست. پوشهٔ releases میتواند بعداً r2 و نسخهٔ قبلی را کنار هم نگه دارد. پیوند نمادین یا symlink یک نام است که به مسیر دیگری اشاره میکند؛ current انتخاب فعال را نشان میدهد. سرویس مسیر ثابت current/server.mjs را میخواند، در حالی که مدیر میتواند مقصد current را طبق روند انتشار تغییر دهد. خود پیوند پشتیبان نیست و تغییر آن هم فرایند از قبل اجراشده را خودکار عوض نمیکند. فرایند فعلی همچنان کدی را دارد که هنگام شروع بار کرده است؛ بعد از تغییر کنترلشدهٔ انتخاب، راهاندازی دوباره و آزمون لازم خواهد بود.
کد برنامه زیر /opt قرار میگیرد، چون در این سناریو برنامهای مستقل از بستههای معمول توزیع را نصب میکنیم. تنظیمات وابسته به سرور در /etc/darsavand-node نگه داشته میشود. دادهٔ ماندگار، اگر بعداً طراحی شود، باید مسیر و سیاست پشتیبان جداگانه داشته باشد و نباید بیبرنامه داخل پوشهٔ نسخه نوشته شود. برنامهٔ فعلی دادهٔ ماندگار ندارد. پس واژهٔ «نسخهٔ تغییرناپذیر» در این راهنما یک قاعدهٔ عملی انتشار است: کاربر سرویس مجوز تغییرش را ندارد و مدیر نیز برای اصلاح، نسخهٔ تازه میسازد. root همچنان از نظر سیستم عامل توان تغییر فایل را دارد؛ ما با این نام، قفل فیزیکی یا خاصیت جادویی به فایل اضافه نکردهایم. ثبت مقصد پیوند و هش نسخه کمک میکند بعداً بدانیم دقیقاً چه چیزی اجرا شده است.
پیش از نوشتن در مسیرهای سیستمی، شرطها را بسنجید
بلوک بعدی یک بررسی خواندنی است. Node باید از مسیر صریح درس سوم پیدا شود، پوشهٔ ورودی باید همان هفت فایل و manifest را داشته باشد و هفت چکیده باید OK شوند. نمایش getent برای کاربر یا گروه darsnode یعنی نام از قبل مصرف شده است؛ در این نوبت آن را به حساب تازه تبدیل یا حذف نکنید. ممکن است متعلق به اجرای قبلی خودتان باشد، اما تا این موضوع روشن نشده نباید مراحل ساخت نخستین نصب را تکرار کنید. همچنین اگر current یا r1 از قبل وجود دارد، جریان ادامه برای اولین نصب مناسب نیست؛ از روند بهروزرسانی فصل آخر استفاده میشود. اصل مهم این است که راهنمای «نصب تازه» را با «ترمیم یک نصب موجود» یکی نگیریم و از روی پیام خطا به حذف گسترده نرسیم.
/opt/node-current/bin/node --version
cd "$HOME/node-upload/r1"
sha256sum -c SHA256SUMS
getent passwd darsnode || true
getent group darsnode || true
ls -ld /opt/darsavand-node /opt/darsavand-node/current /opt/darsavand-node/releases/r1 2>/dev/null || trueاسکریپت نصب در یک زیرپوستهٔ داخل پرانتز اجرا میشود. set -eu در همان زیرپوسته، شکست فرمان و متغیر تعریفنشده را متوقف میکند؛ شل SSH شما را با تغییر ماندگار گزینهها درگیر نمیکند. پیش از ساخت حساب یا پوشه، همهٔ مسیرهای حساسی که باید تازه باشند بررسی میشوند. اگر نام یا مسیری وجود داشته باشد، پیام STOP میگیرید؛ علت را بفهمید و فرمان را با حذف شرطها دوباره اجرا نکنید. adduser حسابی سیستمی و گروه همنام میسازد؛ این حساب برای ورود روزمرهٔ انسانی نیست و خانهٔ قابل استفاده ساخته نمیشود. فایلهای کد با مالک root و مجوز ۶۴۴ و پوشهها با ۷۵۵ نصب میشوند: سرویس میتواند بخواند و عبور کند، ولی نمیتواند کد را بازنویسی کند. هیچ chmod 777 یا chown بازگشتی روی مسیر عمومی لازم نیست.
(
set -eu
upload_dir="$HOME/node-upload/r1"
test -x /opt/node-current/bin/node
cd "$upload_dir"
sha256sum -c SHA256SUMS
if getent passwd darsnode >/dev/null || getent group darsnode >/dev/null; then
printf 'STOP: darsnode already exists; inspect the existing installation.
' >&2
exit 1
fi
for target in /opt/darsavand-node /etc/darsavand-node; do
if sudo test -e "$target" || sudo test -L "$target"; then
printf 'STOP: target already exists: %s
' "$target" >&2
exit 1
fi
done
sudo adduser --system --group --no-create-home --home /nonexistent darsnode
sudo install -d -o root -g root -m 0755 /opt/darsavand-node/releases/r1
sudo install -o root -g root -m 0644 package.json model.mjs app.mjs server.mjs app.test.mjs .env.example .gitignore SHA256SUMS /opt/darsavand-node/releases/r1/
sudo ln -s /opt/darsavand-node/releases/r1 /opt/darsavand-node/current
sudo install -d -o root -g root -m 0755 /etc/darsavand-node
sudo install -o root -g root -m 0600 /dev/null /etc/darsavand-node/app.env
printf 'PORT=3000
NODE_ENV=production
' | sudo tee /etc/darsavand-node/app.env >/dev/null
)پایان بدون خطای بلوک نصب هنوز به معنی اجرای سالم برنامه نیست. ابتدا readlink -f /opt/darsavand-node/current را بزنید؛ مقصد باید r1 باشد. سپس stat برای فایل و پوشه، مالک و مجوز را نشان میدهد. با sudo -u darsnode یک بررسی را دقیقاً با هویت سرویس اجرا میکنیم؛ این با دیدن فایل توسط مدیر تفاوت دارد. اگر مدیر فایل را میخواند ولی کاربر سرویس نمیخواند، مجوز یکی از پوشههای مسیر یا فایل را بررسی کنید. namei -l مسیر را جزءبهجزء نمایش میدهد و برای یافتن پوشهٔ بدون مجوز عبور مفید است. مجوز نوشتن سرویس باید منفی و مجوز خواندن مثبت باشد. این مشاهدهها را ثبت کنید؛ اگر بعداً سرویس خطای دسترسی داد، شاهد قبل و بعد خواهید داشت، نه فقط یک حدس دربارهٔ sudo.
readlink -f /opt/darsavand-node/current
stat -c '%U:%G %a %n' /opt/darsavand-node/releases/r1 /opt/darsavand-node/releases/r1/app.mjs /etc/darsavand-node/app.env
namei -l /opt/darsavand-node/current/server.mjs
sudo -u darsnode test -r /opt/darsavand-node/current/app.mjs && printf 'service can read code
'
sudo -u darsnode test ! -w /opt/darsavand-node/current/app.mjs && printf 'service cannot rewrite code
'تنظیم محیط را با کد و اطلاعات محرمانه یکی نکنید
app.env فعلی فقط دو تنظیم غیرمحرمانه دارد. PORT پورتی است که برنامه واقعاً میخواند و NODE_ENV یک قرارداد رایج نامگذاری محیط است؛ در این برنامه تغییر NODE_ENV بهتنهایی منطق امنیت، احراز هویت یا پایگاه داده اضافه نمیکند. مجوز ۶۰۰ فایل یعنی مدیر سیستم میتواند آن را بخواند و بنویسد و کاربران عادی مستقیم نمیخوانند. در درس بعد، مدیر سیستمی systemd پیش از اجرای برنامه، EnvironmentFile را میخواند و مقدارها را به محیط فرایند میدهد؛ خود darsnode لازم نیست فایل ریشهمالک را باز کند. فایل تنظیم، اسکریپت Bash نیست و آن را source نمیکنیم. متغیر محیطی نیز محل ایدهآل نگهداری رمز نیست؛ میتواند به فرایندهای فرزند برسد یا از مسیرهای تشخیصی افشا شود. برای رمز واقعی باید روش متناسب مانند credential سرویس و طراحی برنامهٔ خوانندهٔ آن بررسی شود.
به درس تنظیمات دورهٔ قبل ارجاع میدهیم تا تفاوت منبع مقدار روشن بماند: https://www.darsavand.com/courses/nodejs-windows-backend-start?lesson=e5ebc43b-785b-432e-bd25-b5e825d5364c
در اجرای دستی بعدی، PORT را صریح روی همان خط قرار میدهیم؛ برای اثبات اینکه حساب سرویس میتواند برنامه را اجرا کند، نیازی به بازکردن مجوز app.env نداریم. env فقط محیط همان فرایند را با مقدارهای دادهشده میسازد. #type-conversion در این نقطه مهم است: مقدار محیط همیشه متن است و server.mjs آن را پیش از تبدیل به عدد، با شکل و محدودهٔ مجاز بررسی میکند. اگر PORT=abc باشد، خروج ناموفق انتظار درست است. راهحل آن بازکردن فایروال یا تغییر Nginx نیست؛ خطا هنوز پیش از شنیدن پورت رخ داده است. هر مرحله را در همان لایهٔ مسئول بررسی کنید.
پیش از اجرای طولانی، چهار فایل جاوااسکریپت را از نظر نحو بررسی و آزمونها را با هویت سرویس اجرا کنید. استفاده از مسیر کامل Node مهم است؛ محیط sudo و سرویس الزاماً PATH ترمینال تعاملی شما را ندارد. برای این پروژه به npm install یا npm ci نیاز نداریم، چون وابستگی خارجی ندارد؛ package.json نیز به تنهایی وجود lockfile را تضمین نمیکند. در پروژهٔ دارای وابستگی، نصب و آزمون در مرحلهٔ ساخت یا آمادهسازی نسخه انجام میشود، نه با root هنگام پاسخ به اولین درخواست. آزمون اینجا پورت موقت آزاد میگیرد و state مستقل دارد؛ نتیجهٔ آن ردیفهای سرور واقعی را تغییر نمیدهد. اگر ۲۲ آزمون موفق نشد، سرویس را نسازید و کد خروج و نخستین مورد شکستخورده را همراه نسخهٔ Node بررسی کنید.
(
set -eu
for file in app.mjs model.mjs server.mjs app.test.mjs; do
sudo -u darsnode /opt/node-current/bin/node --check "/opt/darsavand-node/current/$file"
done
sudo -u darsnode /opt/node-current/bin/node --test /opt/darsavand-node/current/app.test.mjs
)اجرای پیشزمینه و اولین درخواست واقعی روی خود اوبونتو
پیشزمینه یعنی برنامه به همین ترمینال متصل اجرا میشود و تا پایانش خط فرمان برای دستور بعدی برنمیگردد. این حالت برای مشاهدهٔ نخستین خطا مناسب است، اما روش نگهداری دائمی نیست. در نشست اول SSH، فرمان زیر را اجرا کنید. پیام Task API و نشانی سلامت باید چاپ شود و ترمینال منتظر بماند؛ این انتظار طبیعی است. در نشست دوم SSH روی همان اوبونتو، curl درخواست میفرستد. در این مرحله درخواست از مرورگر ویندوز به ۱۲۷.۰.۰.۱ سرور نمیرود؛ ۱۲۷.۰.۰.۱ روی هر دستگاه، خود همان دستگاه است. اگر آن را در مرورگر لپتاپ بنویسید، به لپتاپ اشاره میکند. برای دسترسی خصوصی از لپتاپ، بعداً تونل SSH را معرفی میکنیم و پورت ۳۰۰۰ را عمومی باز نمیکنیم.
cd /opt/darsavand-node/current
sudo -u darsnode env PORT=3000 NODE_ENV=production /opt/node-current/bin/node /opt/darsavand-node/current/server.mjsدر نشست دوم، درخواست سلامت باید کد ۲۰۰ و بدنهٔ {"ok":true} بدهد. سپس یک کار ساختگی میسازیم، id برگرداندهشده را از JSON میخوانیم و همان کار را انجامشده میکنیم. #async-await در دستور کوتاه Node برای خواندن ورودی استاندارد استفاده شده است؛ دادهٔ پاسخ بهعنوان متن خوانده و سپس تجزیه میشود، نه اینکه با eval به فرمان تبدیل شود. متغیر task_id فقط در همین Bash مقدار میگیرد. اگر POST خطا داد، ادامه ندهید؛ وجود بدنهٔ خطا به معنی داشتن task.id نیست. --fail-with-body باعث میشود خطای HTTP کد خروج ناموفق بدهد و بدنهٔ تشخیصی هنوز دیده شود. عنوان نمونه شخصی نیست و بعد از توقف برنامه از بین خواهد رفت. هیچکدام از این فرمانها را به دامنهٔ عمومی یا سرویس نامرتبط نفرستید.
(
set -eu
curl --fail-with-body --silent --show-error -i http://127.0.0.1:3000/health
created=$(curl --fail-with-body --silent --show-error http://127.0.0.1:3000/tasks -H 'Content-Type: application/json' --data-binary '{"title":"آزمایش استقرار"}')
printf '%s\n' "$created"
task_id=$(printf '%s' "$created" | /opt/node-current/bin/node --input-type=module -e "let text=''; for await (const part of process.stdin) text += part; console.log(JSON.parse(text).task.id)")
curl --fail-with-body --silent --show-error -X PATCH "http://127.0.0.1:3000/tasks/$task_id" -H 'Content-Type: application/json' --data-binary '{"done":true}'
printf '\n'
curl --fail-with-body --silent --show-error http://127.0.0.1:3000/tasks
)اگر curl با connection refused شکست خورد، ابتدا ببینید برنامهٔ نشست اول هنوز باز است و پیام Cannot start نداده است. EADDRINUSE یعنی آدرس و پورت در اختیار شنوندهٔ دیگری است؛ با ss -ltnp و نشانی ۳۰۰۰ بررسی کنید و فرایند ناشناخته را نکشید. خطای پیدانشدن ماژول اغلب به انتقال ناقص، نام با حروف متفاوت یا پیوند اشتباه مربوط است. در لینوکس بزرگی و کوچکی حروف نام فایل اهمیت دارد؛ App.mjs و app.mjs یکسان نیستند. اگر پاسخ ۴۲۲ گرفتید، اتصال برقرار بوده و باید قرارداد داده را بررسی کنید. اگر پس از چند درخواست کارها ظاهر میشوند ولی با اجرای دوباره خالیاند، این نقص انتقال نیست؛ ویژگی حافظهای همین مدل است. تشخیص از روی شاهد، شما را از تغییر همزمان فایل، مجوز و شبکه بینیاز میکند.
در پایان، در نشست اول Ctrl+C بزنید و منتظر برگشت خط فرمان بمانید. درخواست سلامت در نشست دوم باید اکنون شکست اتصال بگیرد؛ یعنی فرایند دستی واقعاً متوقف شده است. این بررسی جلوی رقابت دو اجرا برای یک پورت را در درس systemd میگیرد. تمرین: همکار شما برای رفع permission denied پیشنهاد میدهد کل /opt را قابل نوشتن همه کنید. پاسخ تشریحی: اول هویت و جزء مسیری که رد شده را پیدا میکنیم، چون برنامه فقط خواندن کد میخواهد؛ دادن نوشتن عمومی علت را پنهان و دامنهٔ آسیب را بزرگ میکند. تمرین دوم: آیا پاسشدن node:test ثابت میکند داده پس از reboot باقی میماند؟ خیر، آزمونهای این مدل حتی استقلال state هر سرور را نشان میدهند. معیار تحویل، ثبت مقصد current، مالکیت فایلها، ۲۲ آزمون، پاسخهای ایجاد و ویرایش و توقف اجرای دستی است.
منابع رسمی انگلیسی:
Ubuntu adduser: https://manpages.ubuntu.com/manpages/noble/man8/adduser.8.html
Ubuntu install: https://manpages.ubuntu.com/manpages/noble/man1/install.1.html
Ubuntu ln: https://manpages.ubuntu.com/manpages/noble/man1/ln.1.html
Node.js 24 — Process: https://nodejs.org/docs/latest-v24.x/api/process.html
Node.js 24 — Test runner: https://nodejs.org/docs/latest-v24.x/api/test.html
systemd 255 — EnvironmentFile و دسترسیها: https://www.freedesktop.org/software/systemd/man/255/systemd.exec.html
تألیف و مثالهای اختصاصی: تحریریه درسآوند. منابع رسمی انگلیسی برای مطالعهٔ تکمیلی معرفی شدهاند.
متن درسهای عمومی را بدون ورود بخوانید.