اولین باری که در یک پروژه واقعی با Docker روبرو شدم، تصورم این بود که این ابزار فقط برای مهندسان زیرساخت است؛ کافی است چند دستور حفظ کنم و بقیه کارها خودکار انجام می‌شود. تجربه سه ماه بعد خلاف این را ثابت کرد: اگر مدل ذهنی درست نداشته باشی، Docker نه‌فقط سرعت نمی‌آورد، بلکه باگ‌های مبهم هم می‌سازد. امروز بعد از چند سال کار با Docker در پروژه‌های متنوع، این مسیر یادگیری را در قالب یک راهنمای عملی برای شما می‌نویسم، با تمرکز روی همان مفاهیمی که واقعاً در پروژه‌ها به آن‌ها نیاز پیدا کردم.

Docker چیست و چه مشکلی را حل می‌کند؟

Docker یک پلتفرم برای ساخت، توزیع و اجرای اپلیکیشن‌ها در محیط‌های ایزوله به نام Container است. برای آشنایی با تاریخچه و پیشینه این فناوری، صفحه Docker در ویکی‌پدیا نقطه شروع خوبی است. اما سوال اصلی این است: Docker چه مشکلی را حل می‌کند که قبل از آن وجود داشت؟

در تجربه من، بزرگ‌ترین درد Docker همان چیزی است که توسعه‌دهنده‌ها سال‌ها با آن دست‌وپنجه نرم می‌کردند: این اپلیکیشن روی سیستم من کار می‌کند، اما روی سرور کار نمی‌کند. ریشه این مشکل در تفاوت محیط‌ها بود — نسخه PHP، نسخه کتابخانه‌های سیستمی، تنظیمات شبکه، مسیر فایل‌ها و صدها چیز دیگر. Docker این تفاوت‌ها را با بسته‌بندی کامل محیط اجرا همراه کد حل می‌کند. یعنی اپلیکیشن شما در یک بسته ایزوله قرار می‌گیرد که روی هر سروری با Docker نصب‌شده، دقیقاً همان رفتار را دارد.

مزیت دوم Docker، سرعت راه‌اندازی محیط توسعه است. در پروژه‌ای که راه‌اندازی محیط توسعه‌اش به‌طور سنتی یک روز کامل وقت می‌گرفت (نصب دیتابیس، کش، صف، تنظیمات PHP، نصب وابستگی‌ها)، با Docker این زمان به کمتر از ده دقیقه کاهش یافت. اگر می‌خواهید ببینید Docker چطور در سطح صنعت تحول ایجاد کرد، چرا Docker انقلابی در استقرار نرم‌افزار ایجاد کرد؟ این تحول را با جزئیات بررسی کرده است.

مزیت سوم Docker، یکپارچگی با اکوسیستم DevOps است. Docker با اکثر ابزارهای CI/CD، سیستم‌های ارکستراسیون مثل Kubernetes و پلتفرم‌های ابری یکپارچه می‌شود. همین یکپارچگی است که Docker را از یک ابزار ساده به یک استاندارد صنعتی تبدیل کرده است. اگر تازه با DevOps آشنا می‌شوید، چرا DevOps فقط یک ابزار نیست؟ تفاوت فرهنگ، فرآیند و جعبه‌ابزار جایگاه Docker در این اکوسیستم را روشن می‌کند.

Docker فقط یک ابزار نیست؛ یک قرارداد است. وقتی تیم شما روی Docker توافق دارد، تفاوت محیط‌ها از معادله حذف می‌شود و انرژی تیم صرف مسائل واقعی می‌شود.

مدل ذهنی Docker: Image، Container و Registry

پیش از هر دستوری، باید سه مفهوم پایه Docker را درونی کنید: Image، Container و Registry. نبود این درک، دلیل اصلی این است که تازه‌کارها در هفته اول، دستورات را حفظ می‌کنند اما در موقعیت‌های واقعی سرگردان می‌شوند.

Image

Image یک الگوی فقط‌خواندنی است که شامل کد، کتابخانه‌ها، متغیرهای محیطی و تنظیمات لازم برای اجرای یک اپلیکیشن است. تصور کنید Image مثل یک DVD نصب سیستم‌عامل است؛ فقط می‌توانید از روی آن نصب کنید، اما نمی‌توانید خودش را تغییر دهید. Imageها از لایه‌های روی هم ساخته می‌شوند و همین لایه‌بندی، دلیل سبک بودن Docker است.

Container

Container یک نمونه در حال اجرا از یک Image است. اگر Image یک DVD نصب باشد، Container یک سیستم‌عامل نصب‌شده و در حال اجراست. هر Container از Image خودش ایزوله است و می‌توانید چند Container از یک Image بسازید. تفاوت Container با ماشین مجازی در این است که Container سیستم‌عامل کامل ندارد و منابع را با میزبان به اشتراک می‌گذارد. همین باعث می‌شود Containerها چند برابر سریع‌تر و سبک‌تر از VM باشند. اگر می‌خواهید تفاوت دقیق این دو را ببینید، VPS چیست و چه تفاوتی با هاست اشتراکی دارد؟ این مفاهیم را در سطح زیرساخت مقایسه کرده است.

Registry

Registry یک مخزن برای Imageها است. Docker Hub محبوب‌ترین Registry عمومی است که هزاران Image آماده در آن وجود دارد. GitLab Container Registry و GitHub Container Registry هم گزینه‌های محبوب در محیط سازمانی هستند. Image را از Registry pull می‌کنید و پس از ساخت، به Registry خودتان push می‌کنید. اگر روی پروژه‌های تیمی کار می‌کنید و از GitLab استفاده می‌کنید، GitLab برای تیم‌های DevOps: از CI تا امنیت این یکپارچگی را با جزئیات بررسی کرده است.

مفهوممعادل واقعیویژگی کلیدی
ImageDVD نصبفقط‌خواندنی و لایه‌ای
Containerسیستم اجراشدهایزوله و قابل حذف
Registryانبار DVDمرکزی یا محلی
Dockerfileدستور پختساخت Image را توصیف می‌کند
Volumeهارد اکسترنالداده پایدار خارج از Container

اولین Dockerfile خود را بسازید

Dockerfile یک فایل متنی است که به Docker می‌گوید چطور Image بسازد. هر دستور در Dockerfile یک لایه جدید می‌سازد. برای شروع، یک پروژه ساده Node.js را در نظر بگیرید:

FROM node:20-alpine

WORKDIR /app

COPY package*.json ./
RUN npm ci --only=production

COPY . .

EXPOSE 3000

CMD ["node", "server.js"]

بیایید هر خط را با دقت بررسی کنیم:

  • FROM: Image پایه. node:20-alpine نسخه سبک Node.js 20 است. انتخاب Alpine به‌جای نسخه کامل، حجم Image نهایی را از ۹۰۰ مگابایت به کمتر از ۱۵۰ مگابایت کاهش می‌دهد.
  • WORKDIR: پوشه کاری داخل Container. همه دستورات بعدی نسبت به این پوشه اجرا می‌شوند.
  • COPY package*.json ./: اول فایل‌های وابستگی را کپی می‌کنیم. این ترتیب مهم است چون باعث می‌شود لایه نصب وابستگی‌ها cache شود و در ساخت‌های بعدی سریع‌تر اجرا شود.
  • RUN: دستور نصب وابستگی‌ها در زمان ساخت Image. تفاوت اصلی RUN با CMD این است که RUN در زمان Build و CMD در زمان اجرا اجرا می‌شود.
  • COPY . .: کپی باقی کد پروژه. اگر فایل .dockerignore داشته باشید، فایل‌های اضافه مثل node_modules کپی نمی‌شوند.
  • EXPOSE: اعلام پورت پیش‌فرض. این خط فقط مستندسازی است و پورت را به‌طور واقعی باز نمی‌کند.
  • CMD: دستور پیش‌فرضی که در زمان اجرای Container اجرا می‌شود.

پس از نوشتن Dockerfile، Image را با این دستور بسازید:

docker build -t my-app:1.0.0 .

سپس Container را اجرا کنید:

docker run -d -p 3000:3000 --name my-app-container my-app:1.0.0

حالا اپلیکیشن شما روی پورت ۳۰۰۰ قابل دسترسی است. اگر فایل .dockerignore نساخته‌اید، حتماً بسازید. این فایل مانع از کپی شدن فایل‌های اضافه مثل node_modules، .git و .env به داخل Image می‌شود. نبود .dockerignore دو مشکل ایجاد می‌کند: حجم Image بزرگ می‌شود و ممکن است فایل‌های حساس به داخل Image لو بروند.

در Docker، ترتیب دستورات Dockerfile به‌اندازه خود دستورات مهم است. یک جابه‌جایی ساده، cache را از دست می‌دهد و زمان Build را چند برابر می‌کند.

چرخه حیات Container و دستورات روزمره

در کار روزمره با Docker، حدود بیست دستور کافی است. این دستورات را در چهار دسته دسته‌بندی می‌کنم.

مدیریت چرخه حیات

docker run my-app:1.0.0
docker run -d my-app:1.0.0
docker run -it --rm alpine sh
docker stop my-container
docker start my-container
docker restart my-container
docker rm my-container

خط دوم Container را در حالت detached اجرا می‌کند. خط سوم برای کار تعاملی است و پس از خروج، Container را حذف می‌کند. خط چهارم تا هفتم به ترتیب Container را متوقف، راه‌اندازی، ری‌استارت و حذف می‌کنند.

بررسی و دیباگ

docker ps
docker ps -a
docker logs my-container
docker logs -f my-container
docker exec -it my-container sh
docker inspect my-container
docker stats

دستور logs برای دیدن خروجی اپلیکیشن حیاتی است. دستور exec اجازه می‌دهد وارد یک Container در حال اجرا شوید و دستور اجرا کنید — این ابزار اصلی دیباگ است. دستور stats مصرف CPU و RAM همه Containerها را زنده نشان می‌دهد.

مدیریت Image

docker images
docker pull nginx:alpine
docker push my-registry.com/my-app:1.0.0
docker rmi my-app:1.0.0
docker image prune -a

دستور prune در انتها، Imageهای بدون استفاده را حذف می‌کند. یک عادت مفید: هر ماه یک بار روی سرور Development این دستور را بزنید تا فضای دیسک را آزاد کنید.

پاکسازی سیستم

docker system df
docker system prune
docker system prune -a --volumes

خط آخر خطرناک است؛ همه Imageها و Volumeهای بدون استفاده را حذف می‌کند. از آن فقط روی محیط Development استفاده کنید و نه روی Production. اگر می‌خواهید ببینید مدیریت منابع سرور چطور در سطح سیستمی مشابه است، چگونه مصرف منابع هاست را کاهش دهیم؟ اصول مشابهی را در سطح سرور بررسی کرده است.

Volumes: داده‌های پایدار در Container

یکی از مفاهیمی که تازه‌کارها اغلب دیر یاد می‌گیرند، Volumes است. Container به‌طور ذاتی گذرا است؛ اگر Container را حذف کنید، همه داده‌های داخل آن هم از بین می‌روند. Volumes این محدودیت را حل می‌کند و اجازه می‌دهد داده‌ها در خارج از Container و روی میزبان نگه داشته شوند.

Docker سه نوع mount ارائه می‌دهد: Volumes، Bind Mounts و tmpfs. تفاوت‌ها را در جدول زیر خلاصه می‌کنم:

نوعمحل ذخیرهکاربرد اصلی
Volumeپوشه مدیریت‌شده Dockerدیتابیس، کش، داده پایدار
Bind Mountپوشه دلخواه میزبانتوسعه، اشتراک کد
tmpfsحافظه RAMداده موقت و حساس

Volume در عمل

docker volume create db-data
docker run -d --name mysql -v db-data:/var/lib/mysql mysql:8

در این مثال، داده‌های MySQL روی یک Volume به نام db-data ذخیره می‌شوند. حتی اگر Container را حذف کنید، داده‌ها باقی می‌مانند و می‌توانید در Container جدید به آن متصل شوید. در پروژه‌های واقعی، همیشه برای دیتابیس، فایل‌های آپلود و کش از Volume استفاده کنید.

Bind Mount برای توسعه

docker run -d -p 3000:3000 -v $(pwd):/app node:20-alpine npm run dev

این دستور پوشه فعلی میزبان را به پوشه /app در Container متصل می‌کند. هر تغییری در کد میزبان، فوراً در Container دیده می‌شود؛ این الگو در توسعه روزمره رایج است. یک نکته مهم: Bind Mount در Production معمولاً توصیه نمی‌شود چون باعث وابستگی به ساختار میزبان می‌شود. در Production از Volume یا کپی کد در Image استفاده کنید.

Networking: اتصال Containerها به هم

یک اپلیکیشن واقعی معمولاً از چند سرویس تشکیل شده است: وب، دیتابیس، کش و صف. این سرویس‌ها باید به هم متصل باشند، اما بدون لو رفتن به دنیای بیرون. Docker سه حالت شبکه ارائه می‌دهد که هرکدام برای سناریوی خاصی طراحی شده است.

bridge (پیش‌فرض)

در حالت bridge، هر Container یک IP داخلی می‌گیرد و می‌تواند با Containerهای دیگر همان شبکه صحبت کند. برای اتصال دنیای بیرون به یک سرویس، باید پورت را به میزبان map کنید.

docker network create my-network
docker run -d --name db --network my-network mysql:8
docker run -d --name app --network my-network -p 3000:3000 my-app:1.0.0

در این مثال، Containerها روی شبکه my-network با هم در ارتباط هستند. اپلیکیشن می‌تواند با اسم db (نه IP) به دیتابیس متصل شود. این قابلیت DNS داخلی Docker است و کار با شبکه را بسیار ساده می‌کند.

host

در این حالت، Container مستقیماً از شبکه میزبان استفاده می‌کند و جداسازی شبکه‌ای وجود ندارد. این حالت در لینوکس قابل استفاده است و بیشتر برای سناریوهایی که latency مهم است به‌کار می‌رود.

none

Container بدون هیچ شبکه‌ای اجرا می‌شود. این حالت برای پردازش‌های batch و امنیت حداکثری مفید است.

یک نکته مهم درباره امنیت شبکه: در حالت پیش‌فرض، همه Containerها روی یک شبکه مشترک هستند. تجربه من این است که در پروژه‌های چندسرویسی، همیشه یک شبکه اختصاصی برای هر اپلیکیشن بسازید تا Containerهای سایر اپلیکیشن‌ها به آن دسترسی نداشته باشند. اگر می‌خواهید اصول امنیت شبکه را در سطح سرور هم ببینید، فایروال نرم‌افزاری در سرور: راهنمای عملی دیدگاه مشابهی ارائه می‌دهد.

Docker Compose: مدیریت چند سرویس

وقتی اپلیکیشن شما از چند Container تشکیل شده، اجرای دستورات docker run یکی‌یکی غیرقابل نگهداری می‌شود. Docker Compose این مشکل را با یک فایل YAML حل می‌کند که تمام سرویس‌ها را در یک جا تعریف می‌کند.

version: "3.9"

services:
  web:
    build: .
    ports:
      - "3000:3000"
    environment:
      - DATABASE_URL=postgres://user:pass@db:5432/app
      - REDIS_URL=redis://cache:6379
    depends_on:
      - db
      - cache

  db:
    image: postgres:16-alpine
    environment:
      - POSTGRES_USER=user
      - POSTGRES_PASSWORD=pass
      - POSTGRES_DB=app
    volumes:
      - db-data:/var/lib/postgresql/data

  cache:
    image: redis:7-alpine

volumes:
  db-data:

با این فایل، کل استک را با یک دستور می‌توانید راه‌اندازی کنید:

docker compose up -d
docker compose logs -f web
docker compose down

نکته مهم در Compose، مفهوم depends_on است. این گزینه ترتیب راه‌اندازی سرویس‌ها را تعیین می‌کند اما تضمین نمی‌کند که سرویس وابسته آماده باشد. در پروژه‌های واقعی، اغلب نیاز به wait-for-it یا healthcheck دارید تا مطمئن شوید دیتابیس قبل از اپلیکیشن آماده است. یک الگو که در پروژه‌ها زیاد استفاده کرده‌ام: اضافه کردن healthcheck به دیتابیس و کش، و سپس استفاده از depends_on با شرط service_healthy.

Compose همچنین امکان استفاده از فایل‌های Compose جداگانه برای محیط‌های مختلف را فراهم می‌کند. معمولاً یک فایل پایه (docker-compose.yml) و دو فایل override یکی برای Development و یکی برای Production دارم. این الگو اجازه می‌دهد تنظیمات محیطی مثل پورت‌ها، متغیرهای محیطی و Volumeها بین محیط‌ها متفاوت باشند. اگر می‌خواهید ببینید چطور این تنظیمات به CI/CD وصل می‌شود، راه‌اندازی CI/CD برای پروژه‌های کوچک این یکپارچگی را بررسی کرده است.

Multi-Stage Build: ایمیج‌های سبک

یکی از بزرگ‌ترین اشتباهات تازه‌کارها در Docker، ساخت ایمیج‌های بزرگ و سنگین است. یک اپلیکیشن Node.js ساده می‌تواند به‌راحتی به یک ایمیج یک گیگابایتی تبدیل شود، در حالی که با Multi-Stage Build می‌توان همان را به زیر ۱۰۰ مگابایت رساند.

ایده Multi-Stage این است که در یک Dockerfile چند مرحله داشته باشید. مرحله اول برای Build و تست، مرحله دوم برای اجرا. فقط خروجی مرحله اول به مرحله دوم منتقل می‌شود، نه کل ابزارهای Build.

# مرحله Build
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# مرحله Production
FROM node:20-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/package*.json ./
RUN npm ci --only=production
EXPOSE 3000
CMD ["node", "dist/server.js"]

تفاوت حجم بین یک ایمیج تک‌مرحله‌ای و Multi-Stage می‌تواند از چند صد مگابایت به چند ده مگابایت برسد. چرا این مهم است؟ چون انتقال ایمیج به سرور، زمان Push و Pull، فضای دیسک و زمان راه‌اندازی Container همگی به‌طور مستقیم به حجم Image وابسته هستند. در پروژه‌های واقعی، کاهش حجم Image می‌تواند زمان Deployment را چند برابر سریع‌تر کند.

سه نکته اضافه که در ساخت ایمیج‌های Production به‌طور جدی رعایت می‌کنم:

  • استفاده از .dockerignore جامع که هم .git و هم node_modules و هم فایل‌های تست را حذف کند.
  • استفاده از Imageهای Alpine یا Distroless به‌جای نسخه‌های کامل سیستم‌عامل.
  • Pin کردن نسخه‌ها با شماره دقیق (مثل node:20.10.0-alpine3.19) به‌جای tagهای شناور مثل latest.

یک الگوی مفید که بعد از یادگیری بیشتر پیشنهاد می‌کنم: استفاده از Docker BuildKit برای کش هوشمندتر و Build موازی. BuildKit در نسخه‌های جدید Docker به‌طور پیش‌فرض فعال است اما اگر فعال نیست، با DOCKER_BUILDKIT=1 می‌توانید آن را فعال کنید.

استقرار روی Production

در محیط Production، سه تفاوت مهم با Development وجود دارد: پایداری، امنیت و نظارت. Docker در Production نیازمند رویکرد متفاوتی است.

سه قاعده اساسی

قاعده اول: همیشه از tag مشخص استفاده کنید، نه latest. در Production، هر بار که Container را ری‌استارت کنید، نباید Image تغییر کند. Tag مثل latest ممکن است بین دو اجرا تغییر کند و باعث رفتار غیرقابل پیش‌بینی شود.

قاعده دوم: همیشه --restart always یا unless-stopped را تنظیم کنید. اگر سرور ری‌استارت شد، Containerها باید خودکار بالا بیایند. بدون این تنظیم، سرور شما بعد از یک ری‌استارت، اپلیکیشن را از دست می‌دهد.

قاعده سوم: از Secret Management استفاده کنید، نه متغیرهای محیطی hard-coded. رمز دیتابیس، توکن API و کلیدهای رمزنگاری هرگز نباید در Dockerfile یا فایل Compose باشند. گزینه‌های متعددی وجود دارد: Docker Secrets، متغیرهای محیطی از فایل، Vault و سرویس‌های ابری. در تجربه من، حتی در پروژه‌های کوچک، اختصاص یک فایل .env برای Secretها و خواندن آن‌ها در زمان اجرا، اولین قدم خوب است.

پایش و لاگ

Docker به‌طور پیش‌فرض لاگ‌ها را در فایل‌های JSON نگه می‌دارد که در پروژه‌های پرمخاطره به‌سرعت روی دیسک انباشته می‌شوند. تنظیمات log rotation را حتماً فعال کنید:

docker run -d \
  --log-driver json-file \
  --log-opt max-size=10m \
  --log-opt max-file=3 \
  my-app:1.0.0

برای پایش، ابزارهای متعددی وجود دارد. اما پیش از اضافه کردن هر ابزار، پایه را با docker stats و docker events بسازید. در پروژه‌های کوچک، همین دو دستور به‌تنهایی کافی است.

نکته‌ای که در تجربه به آن رسیده‌ام: وقتی از Docker در Production استفاده می‌کنید، همه اصول امنیت سرور همچنان باید رعایت شوند. Docker یک لایه اضافی است، نه جایگزین امنیت سرور. اگر تازه سرور راه‌اندازی کرده‌اید، راه‌اندازی VPS امن برای میزبانی وردپرس پایه‌های ضروری را پوشش می‌دهد.

امنیت Docker: نکات حیاتی

امنیت Docker یکی از موضوعاتی است که در منابع مبتدی کمتر به آن پرداخته می‌شود، اما در پروژه‌های واقعی می‌تواند تفاوت بین یک حادثه امنیتی و یک سیستم سالم باشد. پنج نکته کلیدی که در هر پروژه Production رعایت می‌کنم.

یک: از root داخل Container پرهیز کنید

به‌طور پیش‌فرض، دستورات داخل Container با کاربر root اجرا می‌شوند. اگر Container به هر دلیلی مورد نفوذ قرار بگیرد، مهاجم دسترسی root داخل Container دارد. این دسترسی می‌تواند با اکسپلویت‌های Container escape به root روی میزبان هم تبدیل شود. راه‌حل ساده است:

RUN addgroup -S app && adduser -S app -G app
USER app

این دو خط، کاربر غیر‌root می‌سازد و دستورات بعدی را با آن اجرا می‌کند.

دو: Image را از منبع رسمی بگیرید

Imageهای Docker Hub در سه دسته هستند: Official، Verified و Community. برای Production همیشه از دو دسته اول استفاده کنید. Imageهای Community بدون بررسی امنیتی منتشر می‌شوند و می‌توانند شامل کد مخرب باشند. قبل از استفاده، Image را با ابزاری مثل Trivy یا Grype اسکن کنید.

سه: نسخه‌ها را Pin کنید

استفاده از tag latest در Image پایه، یک ریسک امنیتی است چون هر بار ممکن است نسخه متفاوتی دریافت کنید. همیشه نسخه دقیق را انتخاب کنید: postgres:16.1-alpine به‌جای postgres:latest.

چهار: فایل Docker Socket را محدود کنید

اگر Containerای دارید که به Docker Socket روی میزبان دسترسی دارد، در واقع به کل Docker دسترسی دارد و می‌تواند Containerهای دیگر را تحت کنترل بگیرد. این تنظیمات را فقط برای ابزارهای مدیریتی مثل Watchtower یا Portainer استفاده کنید، نه برای اپلیکیشن عادی.

پنج: ایمیج را با ابزار امنیتی اسکن کنید

ابزارهایی مثل Trivy، Grype و Snyk می‌توانند Imageهای شما را برای آسیب‌پذیری‌های شناخته‌شده بررسی کنند. در تجربه من، اجرای این اسکن‌ها در Pipeline CI/CD، جلوی ورود آسیب‌پذیری‌های شناخته‌شده به Production را می‌گیرد. برای آشنایی با مفهوم آسیب‌پذیری، CVE چیست و چه نقشی در امنیت دارد؟ نقطه شروع خوبی است.

Docker امنیت را به‌طور خودکار نمی‌آورد؛ فقط سطح حمله را تغییر می‌دهد. امنیت واقعی از ترکیب تنظیمات درست، Imageهای معتبر و ابزارهای اسکن می‌آید.

اتصال Docker به Pipeline CI/CD

ترکیب Docker با CI/CD یکی از قدرتمندترین ترکیب‌های DevOps است. ایده اصلی ساده است: در هر push، یک Image جدید ساخته می‌شود، تست می‌شود و در نهایت به Registry push می‌شود. سپس سرور Production این Image را pull و اجرا می‌کند.

نمونه Pipeline در GitLab CI

stages:
  - build
  - test
  - push
  - deploy

build-image:
  stage: build
  image: docker:latest
  services:
    - docker:dind
  script:
    - docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA .

push-image:
  stage: push
  image: docker:latest
  services:
    - docker:dind
  script:
    - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
    - docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
deploy:
  stage: deploy
  script:
    - ssh user@server "docker pull $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA && docker compose up -d"
  when: manual

این Pipeline چهار مرحله دارد: ساخت Image، تست، Push به Registry و استقرار. متغیرهای CI_REGISTRY_IMAGE، CI_REGISTRY_USER و CI_REGISTRY_PASSWORD به‌طور خودکار توسط GitLab تنظیم می‌شوند. اگر با GitLab CI آشنا نیستید، GitLab برای تیم‌های DevOps: از CI تا امنیت این مفاهیم را با جزئیات توضیح می‌دهد.

نمونه Pipeline در GitHub Actions

name: Docker Build and Push

on:
  push:
    tags:
      - "v*"

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: docker/setup-buildx-action@v3
      - uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
      - uses: docker/build-push-action@v5
        with:
          push: true
          tags: ghcr.io/${{ github.repository }}:${{ github.ref_name }}

این Pipeline با هر tag نسخه، Image می‌سازد و به GitHub Container Registry push می‌کند. اگر با GitHub Actions آشنایی ندارید، GitHub فراتر از میزبانی کد: همکاری و اتوماسیون دیدگاه کلی‌تری از قابلیت‌های این پلتفرم ارائه می‌دهد.

یک مثال واقعی: استقرار اپلیکیشن وردپرس

برای اینکه مفاهیم را در یک سناریوی واقعی ببینید، بیایید استقرار یک اپلیکیشن وردپرس را با Docker مرور کنیم. این مثال، ترکیب همه چیزهایی است که تا اینجا گفتیم.

version: "3.9"

services:
  wordpress:
    image: wordpress:php8.2-apache
    ports:
      - "8080:80"
    environment:
      WORDPRESS_DB_HOST: db
      WORDPRESS_DB_USER: wp
      WORDPRESS_DB_PASSWORD: secret
      WORDPRESS_DB_NAME: wordpress
    volumes:
      - wp-content:/var/www/html/wp-content
    depends_on:
      - db

  db:
    image: mysql:8.0
    environment:
      MYSQL_DATABASE: wordpress
      MYSQL_USER: wp
      MYSQL_PASSWORD: secret
      MYSQL_ROOT_PASSWORD: rootsecret
    volumes:
      - db-data:/var/lib/mysql

  cache:
    image: redis:7-alpine

volumes:
  wp-content:
  db-data:

این فایل Compose سه سرویس را راه‌اندازی می‌کند. نکته کلیدی این است که پوشه wp-content روی Volume نگه داشته می‌شود، چون شامل قالب و افزونه‌های سفارشی شما است. در تجربه من، این الگو بهترین تعادل بین سادگی و پایداری را برای پروژه‌های کوچک وردپرسی فراهم می‌کند.

اگر پروژه شما بزرگ‌تر است و به چندین محیط Deployment نیاز دارد، این الگو را می‌توانید با Compose override برای محیط‌های Development، Staging و Production گسترش دهید. اگر روی سرور اختصاصی این استقرار را انجام می‌دهید، سرور چیست و چگونه کار می‌کند؟ پیش‌نیاز مفیدی است که مفاهیم پایه را پوشش می‌دهد.

اشتباهات رایج در یادگیری Docker

  • استفاده از tag latest: این tag شناور است و در هر pull ممکن است تغییر کند. همیشه از نسخه دقیق استفاده کنید، چه در Image پایه و چه در Image اپلیکیشن.
  • ذخیره داده در داخل Container: بدون Volume، هر بار که Container را حذف کنید، داده‌ها از بین می‌روند. برای دیتابیس و فایل‌های آپلود، همیشه Volume تعریف کنید.
  • نبود .dockerignore: بدون این فایل، node_modules، .git، .env و فایل‌های اضافه به Image کپی می‌شوند. این خطا هم حجم Image را بزرگ می‌کند و هم می‌تواند امنیت را تهدید کند.
  • اجرای Container با کاربر root: در Imageهای Production، همیشه کاربر غیر‌root تعریف کنید تا اکسپلویت‌های Container escape محدود شوند.
  • نبود log rotation: بدون تنظیم max-size و max-file، لاگ‌های Container می‌توانند به‌سرعت دیسک را پر کنند و سرور را از کار بیندازند.
  • بی‌توجهی به healthcheck: در Compose، depends_on فقط ترتیب راه‌اندازی را تعیین می‌کند اما آمادگی سرویس را تضمین نمی‌کند. همیشه با healthcheck ترکیب کنید.
  • نپین کردن نسخه در requirements: در PHP، Python و Node، اگر نسخه‌های وابستگی را pin نکنید، هر Build ممکن است Image متفاوتی بسازد. این رفتار در CI/CD، منبع باگ‌های سخت است.
  • استفاده از Bind Mount در Production: Bind Mount به ساختار میزبان وابسته است و کد شما را از Image جدا می‌کند. در Production، کد باید داخل Image باشد.
  • نادیده گرفتن حجم Image: یک Image چند گیگابایتی، انتقال و راه‌اندازی را کند می‌کند. از Multi-Stage Build و Imageهای سبک استفاده کنید.
  • بی‌توجهی به Docker Compose برای محیط‌های مختلف: یک فایل Compose برای همه محیط‌ها استفاده‌نکردن، ریسک پیکربندی اشتباه در Production را بالا می‌برد.
  • نبود برنامه بکاپ برای Volumeها: Volumeها به‌طور خودکار بکاپ نمی‌شوند. برای دیتابیس و فایل‌های مهم، برنامه بکاپ داشته باشید. اصول مشابه در راه‌اندازی بکاپ خودکار در وردپرس پوشش داده شده است.
  • اجرای همه چیز در یک Container: یک Container باید فقط یک مسئولیت داشته باشد. اگر در یک Container هم وب‌سرور، هم دیتابیس و هم کش اجرا می‌کنید، از مزیت اصلی Docker بی‌بهره مانده‌اید.

پرسش‌های پرتکرار درباره Docker

آیا Docker جایگزین ماشین مجازی است؟

نه به‌طور کامل. Docker و VM دو ابزار متفاوت برای دو مسئله متفاوت هستند. VM برای اجرای یک سیستم‌عامل کامل در محیط ایزوله استفاده می‌شود، در حالی که Container یک فرآیند ایزوله است که منابع سیستم‌عامل میزبان را به اشتراک می‌گذارد. Containerها سریع‌تر و سبک‌تر هستند، اما VMها ایزوله‌تر. در پروژه‌های واقعی، معمولاً هر دو در کنار هم استفاده می‌شوند.

تفاوت Docker و Kubernetes چیست؟

Docker یک ابزار برای ساخت و اجرای Containerها است؛ Kubernetes یک سیستم ارکستراسیون است که چندین Container را روی چند سرور مدیریت می‌کند. برای پروژه‌های کوچک، Docker و Docker Compose کافی است. برای پروژه‌های بزرگ‌تر با نیاز به مقیاس‌پذیری، Kubernetes ضروری می‌شود. اگر علاقه‌مندید، Kubernetes برای مبتدیان: مدیریت کانتینرها در مقیاس دیدگاه عمیق‌تری ارائه می‌دهد.

آیا Docker برای پروژه‌های وردپرسی مناسب است؟

بله، به‌خصوص برای راه‌اندازی محیط Development یکپارچه و استقرار روی سرور. برای پروژه‌های وردپرسی ساده، ممکن است Docker سربار اضافه به‌نظر برسد. اما برای پروژه‌هایی که چند محیط دارند یا باید تکرارپذیر باشند، Docker ارزش خود را ثابت می‌کند. مثال Compose که در همین مقاله دیدید، نقطه شروع خوبی است.

چطور از Docker در Windows استفاده کنم؟

Docker Desktop برای Windows و macOS وجود دارد و روی WSL2 در ویندوز کار می‌کند. تجربه من این است که در ویندوز، کارایی Docker کمی کمتر از لینوکس است اما برای توسعه کافی است. اگر روی پروژه‌های Production کار می‌کنید، همیشه روی سرور Linux استقرار دهید.

آیا می‌توانم بدون Docker Compose کار کنم؟

بله، اما برای هر اپلیکیشن چندسرویسی، Compose زمان زیادی صرفه‌جویی می‌کند. تجربه من این است که حتی در پروژه‌های تک‌سرویسی، Compose ارزش دارد چون تنظیمات را مستند و قابل بازتولید می‌کند.

چطور حجم Image را کاهش دهم؟

چهار تکنیک بیشترین تأثیر را دارند: استفاده از Multi-Stage Build، انتخاب Image پایه سبک مثل Alpine، حذف فایل‌های موقت در همان لایه RUN، و تنظیم .dockerignore جامع. با همین چهار تکنیک، معمولاً حجم Image به نصف یا کمتر کاهش می‌یابد.

آیا Docker در Production از VM سبک‌تر است؟

در بیشتر موارد بله. Containerها سیستم‌عامل کامل ندارند و منابع را به اشتراک می‌گذارند، بنابراین از VM سبک‌تر هستند. اما در سناریوهایی که به ایزوله‌سازی سخت نیاز دارید (مثل multi-tenant)، VMها ممکن است انتخاب مناسب‌تری باشند.

آیا Docker امن است؟

Docker به‌طور پیش‌فرض امن نیست اما می‌تواند امن شود. پنج عمل کلیدی: استفاده از کاربر غیر‌root، Imageهای معتبر، pin نسخه‌ها، تنظیم read-only برای فایل سیستم، و اسکن منظم Imageها. در تجربه من، رعایت همین پنج نکته، سطح امنیت را محسوس بالا می‌برد.

چطور Container را در زمان اجرا با فایل .env راه‌اندازی کنم؟

از پرچم --env-file استفاده کنید:

docker run --env-file .env my-app:1.0.0

در Docker Compose، می‌توانید مستقیماً از env_file در تعریف سرویس استفاده کنید. این روش، متغیرهای محیطی را از یک فایل جداگانه می‌خواند و آن‌ها را به داخل Container منتقل می‌کند. فایل .env هرگز نباید در مخزن Git قرار بگیرد.

آیا می‌توانم چند Container را از یک Image بسازم؟

بله، هر Container از یک Image یک نمونه مستقل است. تفاوت‌ها در مقداردهی محیطی، Volumeها و پورت‌های اختصاصی است. این قابلیت در سناریوهایی مثل اجرای چند نسخه از یک API در پورت‌های مختلف کاربرد دارد.

چطور Container را برای دیباگ متوقف نگه دارم؟

از دستور docker run -it --entrypoint sh my-image استفاده کنید. این دستور Container را با یک Shell تعاملی شروع می‌کند، به‌جای اجرای دستور CMD. این تکنیک در پروژه‌های واقعی، ابزار اصلی برای دیباگ Buildهای مشکل‌دار است.

آیا Docker برای اپلیکیشن‌های ساده سربار است؟

برای پروژه‌های بسیار ساده مثل یک وبلاگ شخصی روی هاست اشتراکی، Docker سربار اضافه است. اما به‌محض اینکه پروژه شما چند سرویس داشته باشد یا به تکرارپذیری محیط Development نیاز داشته باشید، Docker از سربار به سرمایه‌گذاری تبدیل می‌شود.

آیا Docker روی VPS کار می‌کند؟

بله، به‌طور معمول Docker روی VPS‌هایی با سیستم‌عامل Linux کار می‌کند. برای پروژه‌های Production، VPS‌های با حداقل ۴ گیگابایت RAM و فضای SSD توصیه می‌شود. اگر تازه به‌فکر مهاجرت از هاست اشتراکی به VPS هستید، VPS چیست و چه تفاوتی با هاست اشتراکی دارد؟ نقطه شروع خوبی است.

مسیری برای ادامه یادگیری

یادگیری Docker مثل یادگیری هر ابزار دیگری است: سه هفته اول سخت به‌نظر می‌رسد، سه ماه بعد ضروری می‌شود و شش ماه بعد بی‌آنکه بخواهید بخشی از جریان کارتان می‌شود. سه نکته نهایی که در تجربه چند ساله‌ام به آن رسیده‌ام: اول، از پروژه‌های واقعی خودتان شروع کنید، نه از مثال‌های کتابی؛ Docker فقط با تمرین بر روی پروژه واقعی جا می‌افتد. دوم، پیش از ورود به Kubernetes و ابزارهای سطح بالاتر، حداقل شش ماه با Docker و Compose کار کنید؛ پرش به ابزارهای پیچیده‌تر بدون فهم دقیق پایه‌ها، به بدهی فنی منجر می‌شود. سوم، در همه پروژه‌ها به‌جای حفظ کردن دستور، سعی کنید مدل ذهنی را بسازید؛ این مدل ذهنی است که شما را در موقعیت‌های جدید نجات می‌دهد.

Docker در نهایت یک ابزار است؛ نه هدف. ارزش آن در حل مسئله‌های واقعی تیم‌های نرم‌افزاری است: تکرارپذیری، یکپارچگی محیط، سرعت تحویل و کاهش هزینه‌های نگهداری. اگر روی این اصل حرکت کنید، Docker را نه به‌عنوان یک دستور، بلکه به‌عنوان یک راه‌حل یاد می‌گیرید. راه‌حلی که در همه پروژه‌های جدی، جایی برای خودش پیدا می‌کند.

اگر تجربه‌ای از پیاده‌سازی Docker در پروژه‌های خودتان دارید — چه در محیط Development، چه در Production و چه در ترکیب با CI/CD — خوشحال می‌شوم آن را در دیدگاه‌ها بخوانم. به‌خصوص اگر با چالش‌های خاصی مثل Multi-Stage Build، مدیریت Volumeهای بزرگ یا مهاجرت از محیط سنتی به Docker روبرو شده‌اید؛ این تجربه‌ها برای خواننده بعدی که در آستانه یادگیری Docker است، از هر مقاله مرجعی ارزشمندترند. 🐳