آموزش Docker برای توسعهدهندگان با مثال عملی از صفر
آموزش Docker از صفر برای برنامهنویسان: مفاهیم ایمیج و کانتینر، نوشتن Dockerfile چندمرحلهای، Docker Compose با PostgreSQL و بهترین روشها با مثال Node.js.

فهرست مطالب
آموزش Docker برای هر توسعهدهندهای که با بکاند، دواپس یا حتی فرانتاند سروکار دارد، دیگر یک مهارت اختیاری نیست. Docker به شما اجازه میدهد اپلیکیشن را همراه همه وابستگیهایش در یک بسته استاندارد به نام کانتینر (Container) قرار دهید؛ بستهای که روی لپتاپ شما، سرور تست و محیط عملیاتی دقیقاً یکسان اجرا میشود.
در این آموزش یک API ساده Node.js را قدمبهقدم کانتینری میکنیم و بعد با Docker Compose یک پایگاه داده PostgreSQL را هم کنارش بالا میآوریم.
مفاهیم پایه: ایمیج، کانتینر و رجیستری
پیش از نوشتن اولین خط، سه مفهوم را روشن کنیم:
- ایمیج (Image): قالبی فقطخواندنی شامل سیستمفایل، کد و تنظیمات اجرا. ایمیجها از لایهها (Layers) ساخته میشوند و هر دستور در Dockerfile یک لایه میسازد.
- کانتینر (Container): نمونه در حال اجرای یک ایمیج. میتوانید از یک ایمیج چندین کانتینر مستقل بسازید.
- رجیستری (Registry): مخزنی برای نگهداری و اشتراک ایمیجها؛ مثل Docker Hub یا GitHub Container Registry.
اگر ایمیج را به «کلاس» تشبیه کنیم، کانتینر «شیء» ساختهشده از آن است.
نصب Docker و اولین کانتینر
در ویندوز و مک، Docker Desktop را از سایت رسمی Docker نصب کنید. در لینوکس، Docker Engine را طبق مستندات رسمی توزیع خود نصب کنید. سپس نصب را آزمایش کنید:
docker --version
docker run hello-world
دستور docker run اگر ایمیج را محلی نداشته باشد، آن را از رجیستری دانلود و سپس اجرا میکند. یک مثال جذابتر، اجرای وبسرور Nginx است:
docker run -d -p 8080:80 --name web nginx:alpine
حالا http://localhost:8080 را باز کنید. فلگ -d کانتینر را در پسزمینه اجرا میکند و -p 8080:80 پورت ۸۰۸۰ سیستم شما را به پورت ۸۰ کانتینر وصل میکند.
دستورهای ضروری Docker
| دستور | کاربرد |
|---|---|
docker ps |
فهرست کانتینرهای در حال اجرا (-a برای همه) |
docker logs -f web |
دنبال کردن لاگهای کانتینر |
docker exec -it web sh |
باز کردن شل داخل کانتینر |
docker stop web |
متوقف کردن کانتینر |
docker rm web |
حذف کانتینر |
docker image ls |
فهرست ایمیجهای محلی |
docker system prune |
پاکسازی منابع بلااستفاده |
آموزش Docker با یک پروژه واقعی Node.js
یک پوشه به نام devna-api بسازید و فایل package.json را در آن قرار دهید:
{
"name": "devna-api",
"version": "1.0.0",
"main": "server.js",
"scripts": {
"start": "node server.js"
},
"dependencies": {
"express": "^5.1.0"
}
}
سپس فایل server.js:
const express = require("express");
const app = express();
const port = process.env.PORT || 3000;
app.get("/", (req, res) => {
res.json({ message: "Hello from inside a container!" });
});
app.get("/health", (req, res) => {
res.send("ok");
});
app.listen(port, () => {
console.log(`API listening on port ${port}`);
});
با npm install وابستگیها را نصب کنید تا فایل package-lock.json ساخته شود.
نوشتن Dockerfile چندمرحلهای
Dockerfile دستورالعمل ساخت ایمیج است. ما از ساخت چندمرحلهای (Multi-stage Build) استفاده میکنیم: یک مرحله وابستگیها را نصب میکند و مرحله نهایی فقط چیزهای لازم برای اجرا را برمیدارد.
# syntax=docker/dockerfile:1
# Stage 1: install production dependencies
FROM node:24-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
# Stage 2: minimal runtime image
FROM node:24-alpine
WORKDIR /app
ENV NODE_ENV=production
COPY --from=deps /app/node_modules ./node_modules
COPY . .
USER node
EXPOSE 3000
CMD ["node", "server.js"]
چند نکته مهم در این فایل:
- ترتیب دستورها مهم است. اول فقط
package.jsonو فایل قفل را کپی میکنیم. تا وقتی وابستگیها عوض نشدهاند، Docker لایهnpm ciرا از کش برمیدارد و ساختهای بعدی چند برابر سریعتر میشوند. npm ciدقیقاً نسخههای فایل قفل را نصب میکند و برای ساختهای تکرارپذیر مناسبتر ازnpm installاست.USER nodeکانتینر را با کاربر غیر root اجرا میکند؛ یک اصل امنیتی ساده اما حیاتی.- شکل آرایهای
CMDباعث میشود Node.js سیگنالهای توقف را مستقیم دریافت کند.
فایل .dockerignore را فراموش نکنید
بدون این فایل، node_modules محلی و حتی فایلهای رمز شما وارد ایمیج میشوند:
node_modules
npm-debug.log
.git
.env
Dockerfile
compose.yaml
ساخت و اجرای ایمیج
docker build -t devna-api:1.0 .
docker run -d -p 3000:3000 --name api devna-api:1.0
curl http://localhost:3000
اگر پاسخ JSON را دیدید، اولین اپلیکیشن کانتینری شما آماده است. با docker image ls هم میتوانید حجم ایمیج نهایی را ببینید.
Docker Compose: چند سرویس با یک دستور
اپلیکیشنهای واقعی تنها نیستند؛ پایگاه داده، کش و صف پیام هم دارند. Docker Compose اجازه میدهد همه سرویسها را در یک فایل YAML تعریف کنید. فایل compose.yaml را در ریشه پروژه بسازید:
services:
api:
build: .
ports:
- "3000:3000"
environment:
DATABASE_URL: postgres://devna:secret@db:5432/devna
depends_on:
db:
condition: service_healthy
db:
image: postgres:17-alpine
environment:
POSTGRES_USER: devna
POSTGRES_PASSWORD: secret
POSTGRES_DB: devna
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U devna -d devna"]
interval: 5s
timeout: 3s
retries: 5
volumes:
pgdata:
در این فایل چند مفهوم کلیدی هست:
- شبکه داخلی: سرویسها با نامشان همدیگر را پیدا میکنند؛ برای همین در آدرس پایگاه داده بهجای
localhostنوشتهایمdb. - Volume: دادههای PostgreSQL در Volume نامدار
pgdataذخیره میشوند و با حذف کانتینر از بین نمیروند. - healthcheck و depends_on: سرویس
apiفقط وقتی بالا میآید که پایگاه داده واقعاً آماده پذیرش اتصال باشد.
حالا کل مجموعه را اجرا کنید:
docker compose up -d --build
docker compose ps
docker compose logs -f api
docker compose down # stop and remove containers (volume is kept)
رمز عبور را در پروژه واقعی مستقیم در فایل Compose ننویسید. از فایل
.envکه در.gitignoreقرار دارد یا از سازوکار secrets استفاده کنید.
Docker در حین توسعه: تغییر کد بدون build دوباره
ساختن دوباره ایمیج بعد از هر تغییر کوچک در کد خستهکننده است. در محیط توسعه میتوانید پوشه پروژه را با Bind Mount مستقیم به کانتینر وصل کنید تا تغییرات فوراً دیده شوند. Node.js از نسخه 18.11 به بعد فلگ --watch دارد که با تغییر فایلها، برنامه را دوباره اجرا میکند:
docker run --rm -it -p 3000:3000 -v "$(pwd)":/app -v /app/node_modules -w /app node:24-alpine sh -c "npm install && node --watch server.js"
Volume دوم (/app/node_modules) باعث میشود پوشه node_modules داخل کانتینر با نسخه سیستم شما بازنویسی نشود؛ نکتهای مهم وقتی سیستمعامل شما با لینوکس کانتینر فرق دارد. Docker Compose هم برای همین کار قابلیت develop.watch دارد که همگامسازی فایلها را خودکار انجام میدهد.
عیبیابی رایج
- خطای «port is already allocated»: پورت روی سیستم شما اشغال است. پورت دیگری مثل
-p 3001:3000انتخاب کنید یا برنامهای را که پورت را گرفته متوقف کنید. - کانتینر بلافاصله متوقف میشود: با
docker logs <name>خروجی آخرین اجرا را ببینید. کانتینر تا وقتی زنده است که فرایند اصلیاش در حال اجرا باشد. - اپلیکیشن داخل کانتینر به
localhostپایگاه داده وصل نمیشود: داخل کانتینر،localhostیعنی خود کانتینر. از نام سرویس در Compose استفاده کنید. - پر شدن دیسک: ایمیجها و لایههای قدیمی جمع میشوند. با
docker system dfمصرف را ببینید و باdocker system pruneپاکسازی کنید.
بهترین روشها برای ایمیجهای حرفهای
- ایمیج پایه کوچک و مشخص انتخاب کنید. برچسب
latestرا در محیط عملیاتی به کار نبرید؛ نسخه را صریح بنویسید، مثلnode:24-alpine. - در هر کانتینر فقط یک دغدغه. اپلیکیشن و پایگاه داده را در یک کانتینر نگذارید.
- ایمیج را اسکن کنید. ابزارهایی مثل Docker Scout یا Trivy آسیبپذیریهای شناختهشده را در لایهها پیدا میکنند.
- HEALTHCHECK و لاگ روی stdout. Docker و ابزارهای اورکستراسیون از همینها برای پایش سلامت استفاده میکنند.
- رازها را در ایمیج نگذارید. هر چیزی که در یک لایه کپی شود، حتی اگر بعداً پاک شود، در تاریخچه ایمیج باقی میماند.
قدم بعدی
حالا ایمیجی دارید که هر جا اجرا شود، یکسان رفتار میکند. قدم بعدی خودکار کردن ساخت و انتشار آن است؛ در آموزش CI/CD با GitHub Actions همین ایمیج را در هر push بهطور خودکار میسازیم و در رجیستری منتشر میکنیم. وقتی تعداد سرویسها و سرورها زیاد شد، نوبت Kubernetes است؛ تازههای Kubernetes 1.37 را هم از دست ندهید.



