وقتی محیط ساخت بخشی از پروژه میشود
گاهی یک تغییر کوچک در رابط روی لپتاپ نویسنده ساخته میشود، اما روی دستگاه همکار خطای نصب میدهد. نسخهٔ Node فرق دارد، یک وابستگی بومی برای سیستم دیگری ساخته شده یا فایل lock در مخزن نیست. Docker کمک میکند دستور ساخت و ابزارهای لازم را کنار کد ثبت کنیم. این کمک به معنی یکسان شدن کامل همهٔ دستگاهها نیست؛ معماری پردازنده، فایلسیستم و تنظیمات Docker هنوز اثر دارند. نتیجهٔ مفید، محیطی است که اعضای تیم بتوانند دربارهٔ آن حرف بزنند و مراحلش را دوباره انجام دهند.
برای توسعهدهندهٔ فرانتاند، سؤال اول این نیست که کدام image کوچکتر است. باید روشن شود قرار است چه چیزی داخل container اجرا شود: dev server برای تغییر سریع کد، فرایند تولید فایلهای نهایی، یا برنامهای که برای هر درخواست HTML میسازد. این سه کار نیازهای متفاوتی دارند. یک سایت استاتیک را میتوان با Nginx سرو کرد؛ برنامهٔ SSR معمولاً runtime و دستور شروع خودش را میخواهد. کپی کردن پوشهٔ خروجی هر framework به یک image ثابت، راهحل عمومی نیست.
Image، container و context چه تفاوتی دارند؟
Image بستهای از فایلها و تنظیمات اجرای برنامه است. Container نمونهٔ در حال اجرای آن بسته است و میتوان چند نمونه از یک image ساخت. Dockerfile دستور تهیهٔ image را ثبت میکند؛ Compose چند سرویس، پورت و volume را در یک فایل توصیف میکند. Build context مجموعهٔ فایلهایی است که ابزار ساخت اجازه دارد به آنها دسترسی پیدا کند. اگر کل مخزن را بدون محدودیت بفرستیم، پوشهٔ وابستگیها و فایلهای خصوصی هم ممکن است وارد این مجموعه شوند.
فایل dockerignore برای همین مرز اهمیت دارد. حذف node_modules باعث میشود وابستگیهای نصبشده روی میزبان جای نصب داخل image را نگیرند. حذف dist از کپی شدن خروجی قدیمی جلوگیری میکند. فایلهای محیطی هم باید آگاهانه مدیریت شوند. صرفاً حذف کردن یک نام از context جای سیاست نگهداری secret را نمیگیرد، ولی احتمال انتقال ناخواستهٔ فایلهای محلی را کم میکند. در راهنمای ساخت Docker این موضوع کنار انتخاب پایه و ترتیب لایهها توضیح داده شده است.
یک پروژهٔ کوچک برای دنبال کردن مسیر ساخت
نمونهٔ زیر یک شمارندهٔ استاتیک با Vite است؛ نه API دارد و نه دادهٔ کاربران را ذخیره میکند. روی میزبان Node 24 و npm، و برای بخش container نصب فعال Docker با Compose لازم است. یک پوشهٔ خالی بسازید. فایلهای package.json و index.html را در ریشه، و main.js و style.css را در پوشهٔ src بگذارید. نام فایلهای نمایشی را هنگام کپی به همین نامهای مقصد تغییر دهید. محدودهٔ نسخهٔ Vite در package آموزشی ثبت شده و ادعای ثابت ماندن تمام نسخههای فرعی را ندارد.
{"name":"container-demo","private":true,"version":"1.0.0","type":"module","scripts":{"dev":"vite","build":"vite build","preview":"vite preview"},"devDependencies":{"vite":"^8.0.0"}}
<!doctype html><html lang="fa" dir="rtl"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>شمارنده</title></head><body><main><h1>شمارندهٔ محلی</h1><p>این نمونه دادهای به سرور نمیفرستد.</p><button id="counter" type="button">تعداد: ۰</button></main><script type="module" src="/src/main.js"></script></body></html>
import './style.css';
let count = 0;
const button = document.querySelector('#counter');
button.addEventListener('click', () => {
count += 1;
button.textContent = `تعداد: ${count.toLocaleString('fa-IR')}`;
});
:root { font-family: system-ui, sans-serif; color: #182b36; background: #eef3f5; }
body { margin: 0; padding: 2rem 1rem; }
main { max-width: 36rem; margin: auto; padding: 2rem; background: white; border-radius: 1rem; }
button { font: inherit; padding: .8rem 1.2rem; background: #075c55; color: white; border: 0; border-radius: .5rem; cursor: pointer; }
button:focus-visible { outline: 3px solid #bf6600; outline-offset: 4px; }
یک بار در پوشهٔ پروژه npm install را اجرا کنید تا package-lock.json واقعی تولید شود. این فایل را همراه package.json نگه دارید. npm ci داخل image از همان lock استفاده میکند و اگر اطلاعات دو فایل سازگار نباشد، به جای تغییر بیصدای وابستگیها متوقف میشود. این آموزش فایل lock ساختگی ارائه نمیکند. برای نیازمندی نسخهٔ Node و گزینههای راهاندازی، راهنمای رسمی Vite مرجع پروژه است. پس از آماده شدن lock میتوانید مسیر توسعه یا ساخت نهایی را انتخاب کنید.
ساخت چندمرحلهای برای خروجی استاتیک
Dockerfile زیر را در ریشه بگذارید. مرحلهٔ build وابستگیهای توسعه را نصب میکند و خروجی dist میسازد. مرحلهٔ runtime فقط فایلهای خروجی و تنظیمات وبسرور را میگیرد. بنابراین Node و ابزار ساخت در image نهایی این نمونه حضور ندارند. این رفتار مشخصاً به خروجی استاتیک مربوط است. اگر برنامه در زمان درخواست به Node، اتصال پایگاه داده یا secret سمت سرور احتیاج داشته باشد، باید مرحلهٔ نهایی متناسب با آن طراحی شود.
FROM node:24-alpine AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM nginx:stable-alpine AS runtime
COPY nginx.conf /etc/nginx/conf.d/default.conf
COPY --from=build /app/dist /usr/share/nginx/html
EXPOSE 80
جدا کردن مراحل امکان استفاده از پایههای متفاوت را میدهد. مکانیزم انتقال فایل با COPY از مرحلهٔ قبلی در مستندات multi-stage build آمده است. ترتیب کپی package و lock قبل از بقیهٔ کد، به استفادهٔ دوباره از لایهٔ نصب هنگام تغییر فایلهای رابط کمک میکند. این cache تضمین نمیکند هر ساخت سریع باشد؛ تغییر پایه، lock یا تنظیمات ساخت میتواند لایههای لازم را دوباره تولید کند.
تگهای node:24-alpine و nginx:stable-alpine در مثال قابل تغییرند. برای ساختی که باید دقیقاً به همان پایه وابسته بماند، تیم میتواند digest مشخص و بررسیشده را ثبت کند و روند بهروزرسانی امنیتی داشته باشد. ثابت کردن digest بدون برنامهٔ ارتقا هم مشکلات خودش را دارد. تفاوت imageهای Alpine با توزیعهای دیگر، مخصوصاً هنگام استفاده از وابستگی بومی، باید در انتخاب پایه بررسی شود؛ کوچک بودن فایل به تنهایی معیار کافی نیست.
تنظیم وبسرور را کنار فایلها نگه دارید
فایل زیر با نام nginx.conf در ریشه قرار میگیرد. مسیر assets در صورت نبود فایل پاسخ 404 میدهد؛ پاسخ دادن با HTML به درخواست یک فایل JavaScript گمشده، خطا را مبهم میکند. مسیر اصلی fallback به index.html دارد تا یک برنامهٔ تکصفحهای بتواند مسیرهای سمت کاربر را مدیریت کند. شمارندهٔ فعلی router ندارد، اما این تنظیم مرز رفتار فایل استاتیک و مسیر رابط را واضح نگه میدارد. برای سایت چندصفحهای با مسیرهای واقعی، fallback باید دوباره بررسی شود.
server {
listen 80;
server_name _;
root /usr/share/nginx/html;
index index.html;
location /assets/ {
try_files $uri =404;
}
location / {
try_files $uri $uri/ /index.html;
}
}
node_modules
dist
.git
.env
.env.*
کد دوم را با نام .dockerignore ذخیره کنید. معنای try_files و بررسی مسیرها در مستندات Nginx شرح داده شده است. نمونه روی مسیر ریشهٔ دامنه فرض شده؛ میزبانی زیر یک زیرمسیر ممکن است تنظیم base در Vite و مسیرهای وبسرور را لازم داشته باشد. TLS، دامنه، محدودیت دسترسی و سیاست cache این مثال آموزشی را به استقرار کامل تبدیل نمیکنند و باید برای محیط مقصد تعیین شوند.
Compose برای توسعهٔ روزانه
compose.yaml زیر فایلهای پروژه را به container متصل میکند تا تغییر کد بدون ساخت دوبارهٔ image دیده شود. volume جداگانهٔ dependencies پوشهٔ node_modules داخل محیط لینوکسی را نگه میدارد. اجرای npm ci در شروع ساده و قابل فهم است، ولی در پروژهٔ بزرگ میتواند زمان شروع را زیاد کند؛ تیم میتواند بعداً چرخهٔ نصب را جدا کند. گزینهٔ host اجازه میدهد dev server از بیرون container در پورت نگاشتشده دیده شود. این پورت برای توسعه است و نباید بدون بررسی روی شبکهٔ عمومی باز شود.
services:
web:
image: node:24-alpine
working_dir: /app
command: ["sh", "-c", "npm ci && npm run dev -- --host 0.0.0.0"]
ports:
- "5173:5173"
volumes:
- .:/app
- dependencies:/app/node_modules
volumes:
dependencies:
پس از تولید lock، دستور docker compose up را در ریشه اجرا کنید و localhost:5173 را باز کنید. برای توقف سرویسها docker compose down کافی است؛ volume وابستگیها را عمداً حذف نمیکنیم. در مسیر خروجی نهایی، docker build -t frontend-demo:local . را اجرا کنید و سپس docker run --rm -p 8080:80 frontend-demo:local را بزنید. صفحه در localhost:8080 در دسترس خواهد بود. این دستورها روش اجرای نمونهاند؛ نتیجهٔ اجرا در محیط شما باید همانجا بررسی شود و این مقاله گزارشی از اجرای آزمایش ارائه نمیکند.
متغیر محیطی مرورگر، محل نگهداری رمز نیست
مقداری که هنگام build به کد مرورگر وارد میشود، معمولاً در فایل خروجی قابل دسترسی است. نام environment variable آن را خصوصی نمیکند. کلید دسترسی حساس باید در سرور بماند و مرورگر از API مجاز استفاده کند. Build argument هم ابزار عمومی انتقال secret نیست. اگر فرایند نصب واقعاً به اعتبارنامه نیاز دارد، build secret در Docker مسیر مناسبتری برای دسترسی موقت مرحلهٔ ساخت فراهم میکند؛ خود ابزار یا فرمان نیز نباید مقدار را در خروجی ثبت کند.
رفع خطا با پیدا کردن مرز شکست
وقتی صفحه باز نمیشود، ابتدا روشن کنید شکست در نصب، build، شروع وبسرور یا درخواست مرورگر رخ داده است. نبود lock با خطای تنظیم Nginx یکی نیست. پورت سمت میزبان و container را جدا بخوانید، خروجی سرویس را ببینید و وجود فایل مقصد را بررسی کنید. اگر فایلها فقط روی یک سیستم پیدا نمیشوند، حساسیت نام فایل به حروف بزرگ و کوچک را هم بررسی کنید. تغییر تصادفی پایهٔ image پیش از تشخیص مشکل، ممکن است خطا را جابهجا کند و علت را پنهان نگه دارد.
برای تیمی که هنوز ساخت محلی ساده و قابل اعتماد دارد، اضافه کردن Docker باید نیاز مشخصی را پاسخ دهد. برای تیمی که در CI، ابزارها یا محیط همکاران اختلاف دارد، ثبت مسیر ساخت ارزش بیشتری پیدا میکند. نوشتهٔ مهاجرت به App Router تفاوت برنامهٔ وابسته به runtime و صفحات رابط را روشنتر میکند. همچنین طراحی وضعیت بارگذاری یادآوری میکند که درست ساختن container فقط بخشی از تجربهٔ کاربر است؛ رفتار صفحه هنگام انتظار همچنان به طراحی برنامه وابسته است.
هنوز دیدگاهی ثبت نشده
اولین نفری باشید که نظر میدهد!