این پروژه را بهعنوان کارمند Webnevisan، در نقش فرانتاند / دستیار DevOps کار کردم. Webnevisan شرکتی بود که برای ساخت و نگهداری پلتفرم در aramspace.com قرارداد داشت — خود Aram Space متعلق و ادارهشده توسط شرکت مشتری است، نه Webnevisan یا من؛ من بخشی از تیمی بودم که آن را تحویل میداد، نه مالک پروژه.
چالش: این وظیفه بدون هیچ انتخابی در این موضوع آمد — باید روی سرورهای Azure اجرا میشد. رساندن اپلیکیشن بیلدشده به Azure و اجرای آن زیر یک process supervisor مدام بهروشهایی میشکست که ریشهاش این بود که Azure بیسروصدا نسخهی Node.js خودش را به اپلیکیشن تحمیل میکرد.
چند کلمه دربارهی خود اپلیکیشن
بکاند PHP است، و من درگیرش نبودم — فقط روی فرانتاند Next.js کار میکردم. اما آن فرانتاند مستقیماً از مرورگر بکاند PHP را صدا نمیزند: هر فراخوانی بکاند از طریق یک مسیر API در Next.js که نقش پروکسی را دارد مسیردهی میشود. صفحات به رندر سمت سرور استاندارد Next.js با ISR تکیه میکنند، و یک مسیر اختصاصی /api/revalidate — محافظتشده با یک توکن هدر مخفی — به کلاینت اجازه میدهد مسیرهای مشخص، یا کل سایت را همان لحظهای که چیزی باید فوراً منتشر شود پاک کند، بهجای منتظر ماندن برای پایان بازهی ISR. نتیجه مثل یک سایت کاملاً پویا خوانده میشود با اینکه بیشترش از کش سرو میشود.
مشکل: نسخهی Node در Azure یک پیشنهاد نبود
دیپلوی کردن اپلیکیشن بیلدشده مستقیماً روی یک سرور Azure و اجرای آن زیر یک supervisor، مدام به همان دیوار میخورد: رانتایم Node خود Azure به هر نسخهای که میخواست ارائه دهد قفل بود، و وقتی آن با چیزی که اپلیکیشن با آن بیلد و تست شده بود مطابقت نداشت، باگهای کوچک اما واقعی سر برمیآوردند — رفتاری که به این بستگی داشت کدام نسخهی Node واقعاً کد را اجرا میکند، نه به چیزی در خود کد. هیچ راه تمیزی برای فقط گذاشتن اپلیکیشن روی سرور و اعتماد به اینکه همانطور که محلی اجرا میشد اجرا شود، وجود نداشت.
راهحل: توقف ارسال کد، شروع ارسال یک ایمیج
وعدهی اصلی Docker — اینکه رانتایم همراه اپلیکیشن ارسال میشود، بهجای هرچیزی که میزبان اتفاقاً ارائه میدهد — دقیقاً همان ناسازگاریای بود که باید بسته میشد. بهجای دادن سورس خام به Azure و امید به اینکه Node آن همراستا شود، یک Dockerfile نوشتیم که نسخهی Node را داخل خود ایمیج قفل میکند: یک بیلد چندمرحلهای — یک مرحله وابستگیها را نصب میکند، یک مرحله next build را اجرا میکند، و یک مرحلهی نهایی سبک فقط خروجی بیلدشده (output: "standalone") را روی دقیقاً همان نسخهی Node که اپلیکیشن نیاز دارد اجرا میکند. نسخهی Node خود Azure دیگر نمیتوانست به آن دست بزند، چون کانتینر نسخهی خودش را میآورد.
رساندن ایمیج به Azure
روند کار: بیلد کردن ایمیج بهصورت محلی، docker login به یک Azure Container Registry (احرازهویتشده از طریق Azure CLI)، تگ زدن ایمیج محلی با آدرس رجیستری، بعد docker push آن با تگ latest. هر بیلد جدید بهسادگی چیزی که latest در رجیستری به آن اشاره میکرد را بازنویسی میکرد.

ماشین دوم، و بینیازی از ریستارت کردنش
یک سرور جداگانهی Azure به آن رجیستری اشاره داشت، و یک اسکریپت را اجرا میکرد بهجای اینکه هر بار یک نفر با SSH وارد شود تا یک supervisor را دستی ریستارت کند. اسکریپت هرچه که فعلاً latest است را pull میکند و کانتینر در حال اجرا را جابهجا میکند. ارسال یک تغییر دیگر بهمعنای «وارد یک سرور شو و چیزی را ریستارت کن» نبود و بهمعنای «یک ایمیج جدید push کن» شد.
چرا این مهمتر از خود اپلیکیشن بود
ساختن فرانتاند — Next.js، Bootstrap، و فهرست بلندی از کتابخانههای دیگر برای تقویمها، نمودارها، خروجی PDF، و بقیه — واقعاً کار فرانتاند معمولی و سرگرمکنندهای بود. مشکل دیپلوی همان بخشی بود که واقعاً چیزی قابلانتقال یاد داد: همانجایی که Docker از یک کلمهی پرطرفدار که دستدوم دربارهاش خوانده بودم بودن دست کشید و تبدیل شد به ابزاری که یک باگ واقعی و تکرارپذیر را جلوی مشتریای که یک پلتفرم ابری خاص را الزامی کرده بود حل کرد، چه آن پلتفرم کار را آسان میکرد چه نه.
نتیجهی نهایی
اپلیکیشن روی Azure داخل کانتینری منتشر شد که رانتایم Node خودش را حمل میکند بهجای قرض گرفتن هرچه Azure ارائه میداد. ایمیجها یکبار بیلد و به Azure Container Registry push میشوند؛ یک سرور دوم آنها را pull و بدون ریستارت دستی جابهجا میکند. مشتری همان دیپلوی Azure که الزامی کرده بود را گرفت، و این پروژه آموزش واقعی من به Docker بهعنوان یک ابزار روزمره شد، نه چیزی که فقط دربارهاش خوانده بودم.
چه چیزی از این تجربه یاد گرفتم
- یک الزام پلتفرمی مثل «باید روی Azure اجرا شود» محدود میکند کجا کد اجرا میشود، نه چه چیزی آنجا اجرا میشود — Docker رانتایم واقعی را ثابت نگه میدارد، مهم نیست میزبان چه چیزی سعی میکند به شما بدهد.
- رانتایم را داخل ایمیج قفل کنید، نه در یک دستورالعمل README که امید دارد هرکسی که آن را دیپلوی میکند بخواندش — یک نسخهی Node اجباری دقیقاً همان نوع انحرافی است که بهشکل یک باگ ظریف ظاهر میشود، نه یک شکست سخت.
- نیمهی بیجلوهی یک پروژه — پایپلاینی که هیچکس بیرون از تیم هرگز نمیبیند — گاهی همان نیمهای است که مهارت قابلانتقال را یاد میدهد، درحالیکه نیمهی قابلمشاهده ارزشمند اما معمولی باقی میماند.