آموزش جامع راهاندازی اپلیکیشن Node.js در محیط توسعه با Docker و Docker Compose
در این آموزش گامبهگام, یک اپلیکیشن ساده Node.js را برای محیط توسعه (development) به کمک Docker و Docker Compose کانتینریزه میکنیم؛ طوری که:
- با تغییر کد, live-reload داشته باشید (با
nodemon) - فقط با یک دستور پروژه را راهاندازی کنید
- وابستگیها (
node_modules) داخل کانتینر مدیریت شوند و با کد شما قاطی نشوند - متغیرهای محیطی را بهراحتی مدیریت کنید
- خطاهای متداول را بشناسید و سریع رفع کنید
۱. چرا Docker برای توسعه Node.js مفید است؟
خیلی کوتاه و کاربردی:
-
همسان بودن محیط روی همه سیستمها
فرقی نمیکند روی ویندوز, مک یا لینوکس باشید؛ با Docker همه روی یک محیط استاندارد کار میکنند. -
حذف مشکل “روی سیستم من کار میکنه!”
نسخه Node, پکیجها, سیستمعامل, همه در Docker تعریف میشوند؛ پس تفاوت محیطها کمتر دردسر درست میکند. -
جداسازی وابستگیها
دیگر لازم نیست روی سیستم اصلیتان چند نسخه Node نصب کنید یاnode_modulesهای مختلف داشته باشید. -
نزدیک بودن محیط توسعه به محیط تولید
وقتی از همان ایمیج/تنظیمات (یا نزدیک به آن) در production استفاده کنید, بروز باگهای عجیب کمتر میشود. -
راهاندازی سریع پروژه برای اعضای جدید تیم
فقط کافی است Docker و Docker Compose داشته باشند و یک دستور اجرا کنند.
۲. فایلهای پروژه
یک پوشه جدید بسازید, مثلا:
mkdir node-docker-dev
cd node-docker-dev
در این پوشه, پنج فایل زیر را ایجاد میکنیم (همه را کامل و قابل کپی در ادامه میبینید):
server.jspackage.jsonDockerfile.dockerignoredocker-compose.yml
در ادامه محتوا و توضیح هرکدام را میآوریم.
۲.۱. فایل server.js
یک سرور ساده Express با استفاده از متغیر محیطی PORT:
// server.js
const express = require('express');
const app = express();
// خواندن پورت از متغیر محیطی, در صورت نبودن, 3000
const PORT = process.env.PORT || 3000;
// یک روت ساده برای تست
app.get('/', (req, res) => {
res.json({
message: 'سلام از داخل کانتینر Docker! 🎉',
env: process.env.NODE_ENV || 'development',
});
});
// روت سلامت سرویس
app.get('/health', (req, res) => {
res.json({ status: 'ok' });
});
// استارت سرور
app.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`);
});
(ایموجی در پاسخ HTTP ایرادی ندارد؛ قانون شما مربوط به متن مقاله است.)
۲.۲. فایل package.json
اسکریپت dev با nodemon برای live-reload:
{
"name": "node-docker-dev-example",
"version": "1.0.0",
"description": "نمونه ساده اپلیکیشن Node.js برای توسعه در Docker با Docker Compose و nodemon",
"main": "server.js",
"scripts": {
"start": "node server.js",
"dev": "nodemon --legacy-watch server.js"
},
"author": "Your Name",
"license": "MIT",
"dependencies": {
"express": "^4.19.2"
},
"devDependencies": {
"nodemon": "^3.1.0"
}
}
نکتهها:
devازnodemonاستفاده میکند تا با تغییر فایلها, سرور را خودکار ریاستارت کند.- فلگ
--legacy-watchکمک میکند روی بعضی سیستمها (مثلا Docker روی ویندوز/WSL) نظارت روی فایلها مطمئنتر کار کند.
۲.۳. فایل Dockerfile
# Dockerfile
FROM node:18-alpine
# دایرکتوری کاری داخل کانتینر
WORKDIR /app
# فقط فایلهای وابستگی را کپی میکنیم تا cache بهتر کار کند
COPY package*.json ./
# نصب وابستگیها
RUN npm install
# کپی بقیه کدها داخل کانتینر
COPY . .
# پورت اپلیکیشن
EXPOSE 3000
# دستور پیشفرض (در docker-compose override میکنیم)
CMD [ "npm", "run", "dev" ]
در بخش ۳, همین فایل را خط به خط توضیح میدهیم.
۲.۴. فایل .dockerignore
این فایل دقیقا مثل .gitignore است اما برای Docker؛ به او میگوییم چه چیزهایی را در ایمیج کپی نکند:
node_modules
npm-debug.log
Dockerfile
docker-compose.yml
.dockerignore
.git
.gitignore
.env
*.log
مهمترین آیتم اینجاست:
node_modules
چون میخواهیم node_modules را داخل کانتینر بسازیم, نه از سیستم میزبان کپی کنیم.
۲.۵. فایل docker-compose.yml
فایل اصلی برای راهاندازی کانتینر در محیط توسعه:
version: "3.8"
services:
app:
build: .
container_name: node-docker-dev-app
ports:
- "3000:3000"
environment:
- NODE_ENV=development
- PORT=3000
# برای اطمینان از کارکردن watch در بعضی سیستمها
- CHOKIDAR_USEPOLLING=true
volumes:
- .:/app
- /app/node_modules
command: npm run dev
در بخش ۴ این فایل را خط به خط و مخصوصاً قسمت volumes را توضیح میدهیم.
۳. توضیح Dockerfile خط به خط
حال همان Dockerfile را با توضیح ریز میخوانیم:
FROM node:18-alpine
- استفاده از ایمیج رسمی Node نسخه ۱۸ بر پایه Alpine (سبک و سریع).
- همهچیز روی این بیس ایمیج نصب میشود.
WORKDIR /app
- دایرکتوری کاری داخل کانتینر را
/appقرار میدهیم. - از این به بعد هر دستور (مثل
RUN,COPY,CMD) نسبی به این مسیر است.
COPY package*.json ./
package.jsonو اگر وجود داردpackage-lock.jsonرا به/appکپی میکند.- این ترفند باعث میشود اگر فقط کد تغییر کند و وابستگیها ثابت باشند, Docker از cache این مرحله استفاده کند و دوباره
npm installرا اجرا نکند.
RUN npm install
- همه وابستگیهای پروژه را نصب میکند.
- خروجی این دستور در لایههای ایمیج ذخیره میشود.
COPY . .
- بقیه فایلهای پروژه (از جمله
server.js) را داخل/appکپی میکند. - با
.dockerignoreمطمئن شدیم کهnode_modulesو فایلهای اضافی وارد ایمیج نمیشوند.
EXPOSE 3000
- اعلام میکند این کانتینر روی پورت ۳۰۰۰ گوش میکند.
- فقط جنبهی مستندسازی و کمک به ابزارها را دارد؛ خودبهخود پورت را publish نمیکند (این کار را در docker-compose انجام میدهیم).
CMD [ "npm", "run", "dev" ]
- دستور پیشفرض کانتینر را مشخص میکند: اجرای اسکریپت
dev(یعنیnodemon). - در
docker-compose.ymlاین دستور را دوباره مشخص کردهایم (override) تا اگر بعدا CMD را عوض کردید, همچنان در توسعه مشخص باشد.
۴. توضیح docker-compose.yml خط به خط
فایل کامل:
version: "3.8"
services:
app:
build: .
container_name: node-docker-dev-app
ports:
- "3000:3000"
environment:
- NODE_ENV=development
- PORT=3000
- CHOKIDAR_USEPOLLING=true
volumes:
- .:/app
- /app/node_modules
command: npm run dev
برویم خط به خط:
version: "3.8"
- نسخه فرمت Docker Compose. نسخه ۳٫۸ روی اکثر نسخههای جدید Docker پشتیبانی میشود.
services:
app:
servicesلیستی از سرویسها (کانتینرها) است.- ما فقط یک سرویس داریم به نام
app.
build: .
- به Docker میگوید ایمیج را از روی همین پوشه (
.) بسازد. - یعنی از همین
Dockerfileاستفاده کند.
container_name: node-docker-dev-app
- یک نام خوانا برای کانتینر تعیین میکند تا به جای ID طولانی, با این نام به آن رجوع کنیم.
ports:
- "3000:3000"
- پورت ۳۰۰۰ روی میزبان را به پورت ۳۰۰۰ داخل کانتینر وصل میکند.
- فرمت کلی:
"HOST:CONTAINER" - نتیجه: با رفتن به
http://localhost:3000به اپ داخل کانتینر وصل میشوید.
environment:
- NODE_ENV=development
- PORT=3000
- CHOKIDAR_USEPOLLING=true
- این متغیرها در
process.envداخل Node در دسترس هستند. NODE_ENV=developmentیعنی محیط توسعه.PORT=3000را درserver.jsمیخوانیم.CHOKIDAR_USEPOLLING=trueبرای بهتر کار کردن watch فایلها داخل برخی محیطها (مخصوصا Docker روی ویندوز/macOS).
اگر خواستید میتوانید این متغیرها را در
.envبذارید و درdocker-compose.ymlفقط نامشان را بنویسید, ولی برای سادگی اینجا مستقیم نوشتیم.
۴.۱. توضیح مهم: volumes و جلوگیری از بازنویسی node_modules
volumes:
- .:/app
- /app/node_modules
این دو خط مهمترین قسمت برای توسعه هستند:
۱. - .:/app
- یک bind mount است:
پوشهی فعلی پروژه روی سیستم شما (.) را روی/appداخل کانتینر mount میکند. - نتیجه:
- هر تغییری در فایلهای کد (مثلاً
server.js) روی هاست بکنید, بلافاصله در کانتینر هم دیده میشود. nodemonبا دیدن این تغییرها سرور را دوباره استارت میکند → live-reload.
- هر تغییری در فایلهای کد (مثلاً
اما یک مشکل بالقوه دارد:
- اگر روی سیستمتان پوشه
node_modulesوجود داشته باشد, با این mount, آن پوشه روی/app/node_modulesداخل کانتینر سایه میاندازد (overwrite میکند). - این باعث میشود:
- وابستگیهایی که داخل کانتینر نصب شدند, دیگر دیده نشوند.
- یا اگر روی هاست
node_modulesندارید, آن مسیر خالی میشود و خطای عدم وجود ماژول میگیرید.
۲. - /app/node_modules
این خط دقیقا برای حل همین مشکل است.
- این یک anonymous volume است.
- یعنی میگوید: مسیر
/app/node_modulesداخل کانتینر را روی یک volume داخلی Docker نگهدار؛ نه روی پوشهی پروژه روی هاست. - نتیجه:
- Docker از نتیجهی
npm installداخل خودش مراقبت میکند. - حتی اگر کل پروژه را mount کنیم (
.:/app), پوشش/app/node_modulesبا این volume داخلی محافظت میشود و دچار بازنویسی از طرف هاست نمیشود.
- Docker از نتیجهی
به زبان ساده:
- خط اول: «کد من را از سیستم به کانتینر بده, تا live-reload داشته باشم».
- خط دوم: «ولی لطفاً
node_modulesرا از هاست نگیر؛ خودت داخل کانتینر نگهشان دار».
بدون خط دوم, خیلی وقتها به خطاهای Cannot find module, permission denied یا رفتارهای عجیب با وابستگیها میخورید.
command: npm run dev
- دستور اجرا زمانی که کانتینر برای این سرویس بالا میآید.
CMDداخلDockerfileرا override میکند (در اینجا البته یکی هستند, اما اگر در Dockerfile عوضش کنید, اینجا دست بالا را دارد).npm run devهمانnodemonاست که روی فایلهای پروژه watch میکند و با هر تغییری سرور را ریاستارت میکند.
۵. اجرای پروژه (فقط یک دستور)
تا اینجا همه فایلها را ساختهاید. حالا:
۱. مطمئن شوید داخل پوشهی پروژه هستید (جایی که docker-compose.yml قرار دارد):
cd path/to/node-docker-dev
۲. دستور زیر را اجرا کنید:
docker-compose up --build
این کار:
- ایمیج را (بر اساس Dockerfile) میسازد یا بهروزرسانی میکند.
- کانتینر را بالا میآورد.
- لاگها را در همان ترمینال نشان میدهد.
پس از آماده شدن, در مرورگر بروید به:
http://localhost:3000/→ پیام JSON از سرورhttp://localhost:3000/health→ وضعیت سلامت
حالا یک تغییر کوچک در server.js بدهید (مثلا متن پیام را عوض کنید) و فایل را ذخیره کنید؛ باید ببینید که:
- در ترمینال,
nodemonتشخیص میدهد فایل عوض شده و سرور را ریاستارت میکند. - با رفرش صفحه مرورگر, نتیجه جدید را میبینید.
۶. دستورات مفید Docker Compose در توسعه
چند دستور پایه که در طول توسعه زیاد لازمتان میشود:
۶.۱. دیدن لاگها
اگر کانتینر را در پسزمینه اجرا کردهاید (با -d) یا ترمینال قبلی را بستهاید:
docker-compose logs -f
-fیعنی لاگها را بهصورت زنده (follow) نشان بده.
فقط لاگ یک سرویس خاص (مثلا app):
docker-compose logs -f app
۶.۲. ورود به داخل کانتینر (exec)
برای اجرای دستوری داخل کانتینر در حال اجرا (مثلا چک کردن نسخه Node, اجرای ls و غیره):
docker-compose exec app sh
- این دستور یک شل (
sh) داخل کانتینر باز میکند. - حالا میتوانید مثلا بزنید:
node -v
npm ls
ls
برای خروج: exit
۶.۳. خاموش کردن و پاک کردن کانتینرها
برای متوقف کردن سرویسها:
docker-compose down
اگر میخواهید همهی volumeهای ناشناس (از جمله /app/node_modules) را هم پاک کنید:
docker-compose down -v
این کار مفید است وقتی:
- وابستگیها بههم ریختهاند
- میخواهید از صفر
npm installدر کانتینر انجام شود
۷. رفع خطاهای متداول
در کار با Docker + Node + nodemon چند خطا خیلی رایج است. این بخش را نگه دارید که بعداً سریع برگردید و چک کنید.
۷.۱. مشکل: live-reload کار نمیکند
علائم:
nodemonاجرا میشود, اما وقتی کد را تغییر میدهید, سرور ریاستارت نمیشود.- یا فقط بعضی وقتها حس میکنید تغییرها اعمال نمیشوند.
چکلیست رفع مشکل:
۱. آیا volume اصلی را درست ست کردهاید؟
در docker-compose.yml باید این را داشته باشید:
volumes:
- .:/app
- /app/node_modules
اگر .:/app را حذف کرده باشید, کد روی هاست به کانتینر sync نمیشود و nodemon تغییری نمیبیند.
۲. آیا اسکریپت dev درست است؟
در package.json:
"scripts": {
"dev": "nodemon --legacy-watch server.js"
}
اگر به اشتباه start را اجرا کنید, live-reload ندارید؛ چون node خالی است, نه nodemon.
۳. روی ویندوز / WSL / macOS هستید؟
بعضی وقتها مکانیزم watch فایلها در این محیطها داخل Docker خوب کار نمیکند. راهحل:
- ما قبلا این را در
docker-compose.ymlگذاشتیم:
environment:
- CHOKIDAR_USEPOLLING=true
- و در
package.jsonاز--legacy-watchاستفاده کردیم.
اگر هنوز مشکل دارید, یکبار کانتینر را از نو بسازید:
docker-compose down -v
docker-compose up --build
۷.۲. مشکل: خطای پورت اشغال شده (EADDRINUSE)
علائم:
- در لاگها میبینید:
Error: listen EADDRINUSE: address already in use :::3000
علت:
- یک سرویس دیگر روی سیستم شما (احتمالا نسخه لوکال Node بدون Docker, یا سرویس دیگری) در حال استفاده از پورت ۳۰۰۰ است.
راهحل ساده:
۱. یا آن سرویس را ببندید (اگر مثلا قبلاً npm start محلی اجرا کردهاید آن را متوقف کنید).
۲. یا پورت را عوض کنید. مثلا:
- در
server.js:
const PORT = process.env.PORT || 4000;
- در
docker-compose.yml:
ports:
- "4000:4000"
environment:
- PORT=4000
بعد:
docker-compose up --build
و به http://localhost:4000 بروید.
۷.۳. مشکل: permission denied مخصوصاً روی node_modules
علائم:
- در لاگها یا ترمینال داخل کانتینر ارورهایی مثل زیر میبینید:
EACCES: permission denied, mkdir '/app/node_modules'
Error: EACCES: permission denied, access '/app/node_modules'
علتهای رایج:
node_modulesروی هاست با مالک/سطح دسترسیای ساخته شده که با کاربر داخل کانتینر سازگار نیست.node_modulesروی هاست mount شده و با/app/node_modulesقاطی شده است.
راهحل پیشنهادی (سریع و ساده):
۱. مطمئن شوید در .dockerignore, node_modules را دارید (که داریم).
۲. volume مربوط به کانتینر را پاک کنید و از صفر بسازید:
docker-compose down -v
rm -rf node_modules
docker-compose up --build
down -vهمه volumeها (از جمله/app/node_modules) را پاک میکند.- بعد, Docker دوباره
npm installرا در محیط خودش و با دسترسیهای درست اجرا میکند.
۳. اگر در محیط لینوکس هستید و Docker را با sudo باید اجرا کنید, ترجیحاً کاربر خودتان را در گروه docker قرار دهید تا نیاز به sudo نباشد. ولی برای شروع, اجرای دستورات با sudo هم قابل قبول است:
sudo docker-compose up --build
جمعبندی
در این راهنما:
- یک اپلیکیشن ساده Node.js با Express ساختیم (
server.js+package.json). - یک
Dockerfileسبک با Node 18 Alpine نوشتیم. - با
.dockerignoreاز ورود فایلهای اضافی (بهخصوصnode_modules) به ایمیج جلوگیری کردیم. - با
docker-compose.yml:- پورتها را map کردیم,
- متغیرهای محیطی را تنظیم کردیم,
- با volumes کاری کردیم که:
- کد از هاست به کانتینر sync شود (برای live-reload)
- ولی
node_modulesداخل کانتینر بماند و بازنویسی نشود.
- یاد گرفتیم فقط با یک دستور:
docker-compose up --build
پروژه را در محیط توسعه راهاندازی کنیم.
- و چند دستور مهم (
logs,exec,down) و خطاهای متداول (live-reload, پورت اشغال شده, permission denied) را مرور کردیم.
از این نقطه به بعد, میتوانید:
- روتهای بیشتری اضافه کنید,
- دیتابیس (مثل PostgreSQL یا MongoDB) را به عنوان سرویس جدید در همان
docker-compose.ymlتعریف کنید, - و عملاً یک محیط توسعه کامل و ایزوله روی Docker داشته باشید.