یک متغیر محیطی که سایت را سه دقیقه از دسترس خارج کرد
نشت NODE_ENV=development از فایل .env به بیلد Next.js، بیلد ناقص را ساخت و ریاستارت همان را سرو کرد؛ ریشهیابی و راهحل آن.
برای اجرای یک اسکریپت تست، فایل .env را source کردیم. اشتباه ما این بود که همین کار را در همان فرمانی کردیم که npm run build را هم اجرا میکرد. نتیجه یک بیلد ناقص بود و ریاستارتی که همان بیلد ناقص را سرو کرد. سایت حدود سه دقیقه پایین بود. این یادداشت شرح همان اتفاق است: چه شد، چرا شد و چه چیزی را در روال استقرارمان عوض کردیم.
ماجرا و علامتها
میخواستیم یک اسکریپت تست را با تنظیمات پروژه اجرا کنیم. برای اینکه متغیرها در شل در دسترس باشند، فایل .env را source کردیم و در همان فرمان، بیلد پروژه را هم زدیم. فرمان بیلد از یک شل ساخته شده بود که متغیرهای .env را به ارث برده بود.
در .env مقدار NODE_ENV=development بود. این مقدار به فرایند بیلد Next.js رسید. در نتیجه پیشرندر صفحههای 404 و 500 شکست خورد و پوشه خروجی ناقص ماند. بعد پروسه را ریاستارت کردیم و پروسه تازه همان خروجی ناقص را بالا آورد و سرو کرد. سایت حدود سه دقیقه از دسترس خارج بود.
نکته بدِ ماجرا این بود که ریاستارت کار را بدتر کرد. پروسه قدیمی تا قبل از ریاستارت در حافظه نسخه سالم را داشت. ما با ریاستارت آن را با خروجی خراب جایگزین کردیم.
نکات کلیدی
- علت: source کردن .env در همان شلی که بیلد را اجرا میکرد
- اثر: پیشرندر 404 و 500 شکست خورد و خروجی ناقص ماند
- تشدید: ریاستارت، خروجی ناقص را سرو کرد
- مدت قطعی: حدود سه دقیقه
ردگیری علت
در این ردگیری مسیر منطقی را مینویسم، نه یک خط زمانی دقیقهبهدقیقه. خطای بیلد به پیشرندر صفحههای خطا اشاره میکرد، یعنی صفحههایی که در کد خودمان کار خاصی روی آنها نکرده بودیم. وقتی بیلدی که تا دیروز سالم بود بدون تغییر کد میشکند، باید سراغ محیط اجرا رفت و نه کد.
پس پرسیدیم این بار بیلد با چه محیطی اجرا شده بود. جواب این بود: با شلی که چند لحظه قبل .env را در آن source کرده بودیم. با بررسی متغیرهای آن شل، NODE_ENV=development پیدا شد. این مقدار هیچوقت قرار نبود وارد بیلد production شود، ولی از .env آمده بود.
درس روششناسی: وقتی کد تغییر نکرده، محیط را مقایسه کن. تفاوت بین «شلی که همیشه بیلد میگیرد» و «شلی که این بار بیلد گرفت» همین بود.
نکات کلیدی
- بیلد بدون تغییر کد شکست خورد؛ پس سراغ تفاوت محیط رفتیم
- متغیرهای شل بیلد را بررسی کردیم و NODE_ENV=development را دیدیم
علت اصلی و مفهوم پشت آن
NODE_ENV در اکوسیستم Node و React یک قرارداد است و فقط سه مقدار دارد: production برای next build، development برای next dev و test برای تست. طبق مستندات Next.js، مقدار غیراستاندارد میتواند رفتار وابستگیها را خراب کند و حذف کد مرده (dead code elimination) را بشکند. اگر NODE_ENV تنظیم نشده باشد، Next.js خودش برای next dev مقدار development و برای بقیه فرمانها production را میگذارد.
نکتهای که ما را گرفت ترتیب بررسی متغیرها است. طبق مستندات، Next.js متغیرها را به ترتیب از process.env، سپس فایلهای .env.$(NODE_ENV).local، .env.local، .env.$(NODE_ENV) و در آخر .env میخواند و به اولین مقدار پیدا شده بسنده میکند. مستندات واژه «override» را به کار نمیبرد، اما از این ترتیب نتیجه میشود که هر چیزی که از قبل در process.env باشد از همه فایلهای .env جلوتر است.
source کردن .env در شل یعنی متغیرها در محیط آن شل و فرمانهای بعدیاش قرار میگیرند. اگر با set -a یا export همراه باشد، به فرایندهای فرزند هم ارث میرسند. پس NODE_ENV=development وارد process.env بیلد شد، یعنی در بالاترین اولویت قرار گرفت. Next.js هم دیگر مقدار production را برای next build نگذاشت. بیلد در حالت development اجرا شد و پیشرندر صفحههای خطا در آن شرایط شکست خورد.
مشکل دوم ساختاری بود: بیلد مستقیم در همان پوشهای انجام میشد که پروسه از آن سرو میکرد. بیلد وسط راه شکست خورد و پوشه نیمهساخته ماند. هیچ مرحلهای نبود که بگوید «اگر بیلد موفق نبود، خروجی قبلی را دست نزن».
نکات کلیدی
- process.env در ترتیب بررسی Next.js از همه فایلهای .env جلوتر است
- NODE_ENV فقط production، development و test را باید داشته باشد
- بیلد در پوشه سروشونده، نبود مرز بین «ساختهشده» و «در حال سرو» را نشان داد
راهحل
سه تغییر اعمال کردیم. یک، بیلد همیشه در شل تمیز و با env -u NODE_ENV اجرا میشود تا هر مقدار ارثرسیده حذف شود. دو، بیلد در پوشهای جدا انجام میشود و فقط بعد از موفقیت کامل، جابهجایی انجام میشود. سه، بعد از ریاستارت یک چک سلامت اجرا میشود.
برای جابهجایی اتمیک از rename روی یک symlink استفاده میکنیم. طبق man page سیستمcall rename(2)، اگر مقصد وجود داشته باشد بهصورت اتمیک جایگزین میشود و هیچ لحظهای نیست که پروسه دیگری آن را غایب ببیند. فرمان mv -T روی symlink همین کار را میکند، به شرط اینکه هر دو روی یک فایلسیستم باشند. در نمونه زیر مسیرها و دامنه ساختگی هستند:
#!/usr/bin/env bash
set -euo pipefail
APP=/srv/app # مسیر نمونه
REL="$APP/releases/$(date +%Y%m%d%H%M%S)"
mkdir -p "$REL"
cp -a "$APP/src/." "$REL/"
cd "$REL"
# بیلد در محیط تمیز؛ NODE_ENV را خود Next.js تعیین میکند
env -u NODE_ENV npm run build
# فقط اگر بیلد موفق بود، symlink بهصورت اتمیک عوض میشود
ln -sfn "$REL" "$APP/current.tmp"
mv -T "$APP/current.tmp" "$APP/current"
pm2 restart app
# چک سلامت بعد از ریاستارت
for i in 1 2 3 4 5; do
if curl -fsS -o /dev/null https://example.com/ ; then
echo "healthy"; exit 0
fi
sleep 2
done
echo "health check failed; roll back" >&2
exit 1نکات کلیدی
- env -u NODE_ENV: بیلد هیچ NODE_ENV ارثرسیدهای نمیبیند
- بیلد در پوشه release جدا؛ پوشه فعال تا لحظه آخر دستنخورده میماند
- mv -T روی symlink جابهجایی اتمیک را انجام میدهد
- چک سلامت بعد از ریاستارت، قبل از اینکه کسی خراببودن را از بیرون بفهمد
درسها و چکلیست
درس اصلی ما این است که بیلد و اسکریپتهای عملیاتی نباید محیط مشترک داشته باشند. اسکریپت تست ممکن است به DATABASE_URL یا NODE_ENV=development نیاز داشته باشد. بیلد production اینها را نباید ببیند. هر دو را در یک شل نگهداشتن یعنی منتظر یک اتفاق مثل این بودن.
درس دوم این است که اگر در .env به NODE_ENV نیاز نداری، آن را نگذار. مستندات Next.js هم میگوید اگر مفهوم محیط دیگری لازم است (مثل staging)، از نامی مثل APP_ENV استفاده کن و NODE_ENV را به خود ابزار بسپار.
درس سوم: ریاستارت پروسه ابزار بازیابی نیست، فقط وضعیت فعلی دیسک را بالا میآورد. اگر دیسک خراب باشد، ریاستارت آن را سرو میکند.
چکلیست ما:
۱. بیلد همیشه با env -u NODE_ENV و در شل تمیز. ۲. هیچ NODE_ENV در .env نباشد؛ برای مفهوم محیط از APP_ENV استفاده شود. ۳. source کردن .env فقط در زیرشل یا برای همان فرمان اسکریپت، نه در شل اصلی. ۴. بیلد در پوشه جدا و جابهجایی اتمیک فقط بعد از موفقیت. ۵. چک سلامت بعد از هر ریاستارت، با خروج ناموفق اگر پاسخ نیامد. ۶. پوشه release قبلی تا تأیید سلامت حذف نشود تا بازگشت سریع ممکن باشد.
نکات کلیدی
- بیلد و اسکریپتهای عملیاتی محیط مشترک نداشته باشند
- NODE_ENV را در .env نگذارید
- ریاستارت فقط وضعیت دیسک را بالا میآورد، آن را درست نمیکند
جمعبندی
سه دقیقه قطعی از یک خط ساده آمد: source کردن .env کنار بیلد. علت واقعی فقط NODE_ENV نبود؛ نبود مرز بین بیلد و محیط اجرا بود. با شل تمیز، بیلد جدا، جابهجایی اتمیک و چک سلامت، همان خطا دیگر نمیتواند به سایت زنده برسد.
منابع
- Next.js Docs: Non-Standard NODE_ENVnextjs.org
- Next.js Docs: Environment Variablesnextjs.org
- rename(2) — Linux man pagemanpages.opensuse.org