parvanehborzouei.com — یک پلتفرم مطب، و چهارمین ساکن سرور
پلتفرم انتشار دوزبانه برای یک مطب روانشناسی، فعال روی سروری دوهستهای در فرانکفورت که از قبل سه سرویس زنده رویش اجرا میشد. طوری ساخته شده که هیچ واقعیتی دربارهی کارفرما قابل ساختن نباشد، و اشتباهی در اینجا به همسایهها نرسد.
- کارفرما
- پروانه برزویی — مطب روانشناسی
- دامنهی کار
- پلتفرم انتشار دوزبانه، زیرساخت و خط استقرار
- نقش
- تنها — معماری، سیستم طراحی، زیرساخت و CI
- سال
- ۲۰۲۶
وبسایت یک مطب روانشناسی، فعال روی parvanehborzouei.com به فارسی و انگلیسی. بخش جالبِ مهندسی این کار ساختن سایت نبود. اضافهکردن چهارمین سرویسِ زنده به یک سرور دوهستهای در فرانکفورت بود که از قبل سه سرویس دیگر رویش اجرا میشد و یکی از آنها سوابق مالی آدمهای واقعی را نگه میدارد — و انجامدادن این کار در حالی که تقریباً هیچکدام از محتوای خود کارفرما هنوز وجود نداشت.
مسئله
دو محدودیت هر تصمیمی را شکل داد، و هیچکدام درخواست یک قابلیت نبود.
هیچ چیزی را نمیشد ساخت. در شروع پروژه، زندگینامه، مدارک، تخصصها و حوزههای کاری این روانشناس بهعنوان متن وجود نداشتند. برای بیشتر سایتها این یک دردسر زمانبندی است که با متن نمایشی پر میشود تا کلمههای واقعی برسند. برای یک متخصص حوزهی سلامت اینطور نیست: مدرکی که «باورپذیر بهنظر میرسد» روی سایت یک روانشناس، ادعایی دربارهی صلاحیت حرفهای یک آدم است که زیر نام خودش منتشر میشود. سایت باید میتوانست بهصورت عمومی و با نبودِ بیشتر محتوایش زنده شود — و باید از نظر ساختاری ناتوان از جایگزینکردن یک حدس میبود.
بودجهی زیرساخت، یک سرویس دیگر روی سروری بود که از قبل پر بود. سرور مقصد مالتو را اجرا میکند که حسابهای کاربری و سوابق مالی واقعی در پایگاه دادهاش دارد؛ یک پشتهی اتوماسیون n8n؛ یک VPN؛ و همین نمونهکارها را. حدود ۲٫۴۹ گیگابایت در دسترس، دو هسته، و swap از قبل درگیر. حالت خرابیای که اهمیت داشت «سایت جدید کند است» نبود. این بود که یک اشتباه در منابع یا در پیکربندیِ سرویس چهارم، به سه سرویس دیگر میرسد.
راهکار
شکل استقرار از مهاجرتی که هفتهها پیش روی همین سرور کامل شده بود منتقل شد، همراه با کامنتهایش — تقریباً هر کدام از آنها باگی را ثبت کرده که یکبار بابتش هزینه داده شده. آنچه تغییر کرد، هر چیزی بود که فرض میگرفت سرور یک ساکن دارد.
- ایمیجها در CI ساخته میشوند و از رجیستری کشیده میشوند، هرگز روی خود سرور ساخته نمیشوند. اوج مصرف حافظهی یک بیلد Next.js خیلی بیشتر از چیزی است که اینجا آزاد است، و هر دو هسته را برای تمام مدت هر استقرار از همسایهها میگرفت.
- پورت ۳۰۰۲، چون ۳۰۰۰ مال مالتو است، ۳۰۰۱ مال همین سایت، ۵۶۷۸ مال n8n و ۲۰۱۹ مال رابط مدیریت Caddy. چهار ساکن یعنی نقشهی پورتها یک منبع مشترک است، نه یک پیشفرض.
- مهاجرتهای پایگاه داده در یک کانتینر یکبارمصرف پیش از
up -d --waitاجرا میشوند، تا مهاجرتِ شکستخورده یک استقرار قرمز باشد، نه یک حلقهی کرش پشت خطاهای ۵۰۲. - Caddy لایهی TLS را میبندد با گواهی Let's Encrypt که از راه
tls-alpn-01در حدود ده ثانیه صادر شد، وwwwبا ۳۰۸ به دامنهی اصلی هدایت میشود. - پشتیبانگیری شبانهی رمزگذاریشدهی خارج از سرور این پایگاه داده را هم پوشش میدهد، که داخل اسکریپت پشتیبانگیریِ موجود و متعلق به root وصله شده، نه اینکه کنارش سرهم شده باشد.
تصمیمهای طراحی
نام صریح برای پروژهی Compose، چون پیشفرض نامِ پوشه است. داکر Compose نام پروژه — و در نتیجه نام والیومها — را از پوشهی دربرگیرنده میسازد. مالتو در پوشهای به نام repo است، پس نام پروژهاش دقیقاً repo است و والیوم repo_pgdata را در اختیار دارد. هر پشتهی دیگری که در پوشهای با همان نام قرار بگیرد با آن تصادم میکند، و یک docker compose down -v در جای اشتباه، پایگاه دادهای را نابود میکند که حسابهای کاربری واقعی دارد. این پروژه نامش را صریح تعیین میکند و در پوشهای است که اصلاً امکان تصادم ندارد. نام صریح دفاع اصلی است؛ نام پوشه پشتیبان آن.
بودجهی PostgreSQL دوباره محاسبه شد، کپی نشد. تنظیمات پشتهی خواهر فرض میگیرند تنها پایگاه دادهی اضافی روی سرور است. این دومی است، روی ماشینی که از قبل در swap است، پس اعداد بهجای ارثبردن دوباره استخراج شدند: ۹۶ مگابایت shared buffers در برابر ۱۹۲ مگابایتِ خواهر، و حداکثر پانزده اتصال در برابر بیست. هر دو سرویس سقف حافظهی سخت هم دارند، که در روز خوب هیچ کاری نمیکند و در روز بد تمام هدفش همان است — رویداد کمبود حافظه در این پشته نمیتواند به مالتو برسد.
متن دوزبانه در ساختار تایپدار TypeScript زندگی میکند، نه در فایلهای JSON. این یک انحراف عمدی از سایت خواهر است که از جفت متعارف messages/en.json و messages/fa.json استفاده میکند. آن الگو یک خرابی مشخص دارد: کلیدی که در یک فایل هست و در دیگری نیست، باعث میشود فریمورک خودِ مسیر کلید را بهعنوان متن قابلمشاهدهی صفحه رندر کند، بدون اینکه خطایی پرتاب شود، چیزی لاگ شود، یا بررسیکنندهی تایپ بتواند ببیندش — چون فایلهای پیام دادهاند نه کد. سایت خواهر برای گرفتن این مورد به یک تست زماناجرا نیاز دارد. اینجا هر دو زبان در یک ساختار تایپدار هستند، پس نبودِ یک رشتهی انگلیسی خطای کامپایل است و آن تست لازم نیست. ضمناً پرچمِ تأیید کارفرما به تفکیک زبان جایی برای زندگی پیدا میکند، که JSON هیچ جایی برایش ندارد.
واقعیتهای کارفرما بهطور پیشفرض غایباند، و کد این را الزام میکند. هر فیلد پروفایل حرفهای یا جاینگهدارِ علامتخورده است یا واقعاً تعریفنشده، و رندر با «تعریفنشده» مثل «این بخش را حذف کن» رفتار میکند، نه مثل نشانهای برای جایگزینکردن چیزی باورپذیر. صفحهی درباره بهجای یک پوستهی خالی، حالت خالیِ صریح نشان میدهد؛ بخش ابتدایی وقتی عنوان تأییدشدهای نیست زیرعنوانش را حذف میکند، و وقتی عکسی نیست کل ستون تصویر را. جدا از این، هر رشته یا تأییدشده توسط کارفرما علامت خورده یا در انتظار تأیید، و رشتههای در انتظار در محیط آزمایشی نشانهی دیداری دارند تا کارفرما دقیقاً ببیند چه چیزی را هنوز تأیید نکرده. یک آرگومان بیلد این نشانه را کنترل میکند و پیشفرضش خاموش است، پس خروجی نهایی هرگز نمیتواند آن را نشان بدهد.
پالت رنگ: مرکب، کاغذ و یک لاجورد. حرکت بدیهی برای سایت یک مطب رواندرمانی، فیروزهای ملایم یا آبیوسفیدِ کلینیکی است، و دقیقاً به همین دلیل تقریباً همهشان شبیه یک قالب آمادهی واحد بهنظر میرسند. این یکی جهانش را از صفحهی خطیِ فارسی میگیرد: زمینهی کاغذیِ گرم، مرکبِ قهوهای‑سیاهِ مازو بهجای سیاه مطلق، و لاجورد — رنگدانهی تذهیب ایرانی و ریشهی واژهی azure — بهعنوان تنها رنگ تأکید. نسخهی قبلی رنگ تأکید دومی به رنگ خاکِ گرم داشت که فقط در یک جا ارزشش را داشت و همهجای دیگر سیگنال را رقیق میکرد؛ بهجای کمرنگشدن، حذف شد.
قلم نمایشی حرفبهحرف بررسی شد، از فهرست اسمها انتخاب نشد. بیشتر قلمهایی که زیرمجموعهی عربی دارند عربی‑محورند، و این یک سایت فارسی است. قلم انتخابی پیش از پذیرش در مرورگر بررسی شد: اینکه چهار حرف مخصوص فارسی گ چ پ ژ به شکل فارسی کشیده شدهاند، اینکه ک و ی بهجای ك و ي عربی به شکل فارسی رندر میشوند، اینکه نیمفاصله در میشود و کتابها بهصورت اتصال خوانده میشود نه فاصلهی کلمه، و اینکه ارقام به شکل ۰۱۲۳ درمیآیند نه ٠١٢٣. خوانندهی فارسی هر کدام از اینها را فوری میگیرد. هیچکدامشان در یک diff دیده نمیشوند.
نتیجه
پلتفرم زنده است و هر دو زبان را ارائه میدهد، با مسیرهای عمومی سایت به فارسی و انگلیسی و بخش مدیریت که عمداً بیرون از ساختار آدرسهای محلیشده قرار دارد.
پشتیبانها با بازگرداندن تأیید میشوند، نه با بررسی وجود یک فایل. آرشیو رمزگذاریشدهی واقعیِ خارج از سرور دانلود شد، رمزگشایی شد، فایل دامپش استخراج و در یک پایگاه دادهی موقت بازگردانده شد، و تعداد ردیفها جدولبهجدول با محیط اصلی مقایسه شد. رکورد مدیر با هش رمزِ ۱۶۱ نویسهایاش سالم رفتوبرگشت کرد. پشتیبانی که کسی بازگردانیاش نکرده، یک باور است نه یک پشتیبان.
یک حادثه، بدون لاپوشانی. حذف بلوک موقت این پروژه از پیکربندی مشترک Caddy با یک عبارت باقاعده انجام شد که دقیقاً یکبار مطابقت پیدا کرد — و بسیار بیش از اندازه مطابقت پیدا کرد. سرآیند کامنتِ یکسانی روی یک بلوک بیربط بالاتر در همان فایل باعث شد مطابقت غیرحریصانه از آنجا شروع شود و در مسیرش تا رسیدن به یک آکولاد بسته، از روی سه نام میزبان دیگر رد شود. همین نمونهکارها حدود سه دقیقه از دسترس خارج بود تا پشتیبانِ پیش از ویرایش بازگردانده شد. ادعای «دقیقاً یک مطابقت انتظار میرود» یک مطابقتِ بیشازحد بزرگ را نمیگیرد. جایگزینش روی خط نام میزبانِ یکتا لنگر میاندازد، آکولادها را رو به جلو میپیماید، رشتهی پیوستهی کامنتها را رو به عقب جذب میکند، و بعد ادعا میکند که برش حذفشده هیچ بلوک سطحبالای دیگری در خود ندارد — و همین بررسی است که جلوی آن اتفاق را میگرفت.
آنچه هنوز ساخته نشده. مدلهای محتوا، صفحههای مقاله و کتاب، دورههای ویدیویی و لایهی پرداخت دوارزی مرحلهی بعدند؛ پایگاه داده در حال حاضر فقط دو جدول حساب کاربری دارد. بخشهای عمومی سایت همین را صریح میگویند، بهجای نشاندادن پوستههای خالی. این همان قاعدهی متنِ کارفرماست: سایت آنچه را که امروز درست است میگوید.
پشتهی فناوری
Next.js 16 (App Router) · TypeScript · React 19 · Tailwind CSS v4 · Prisma 7 · PostgreSQL 17 · next-intl · Docker Compose · GitHub Actions ← GHCR · Caddy · systemd