چرا Docker Compose؟

Docker Compose راهی استاندارد برای تعریف و اجرای چندین سرویس وابسته به هم در یک فایل YAML است. به‌جای اجرای دستی کانتینرها، شبکه‌ها و ولوم‌ها، همه‌چیز را در docker-compose.yml تعریف می‌کنید و با یک دستور بالا می‌آورید. نتیجه؟ راه‌اندازی سریع‌تر، تکرارپذیر، و قابل‌نسخه‌سازی از محیط توسعه تا استیجینگ و تولید.

پیش‌نیازها

  • نصب Docker و Docker Compose (کامند docker compose version)

  • آشنایی مقدماتی با Dockerfile

  • آشنایی با مفاهیم شبکه، ولوم، متغیرهای محیطی

معماری نمونه: Nginx (Reverse Proxy) + API (Node.js) + PostgreSQL + Redis

در این نمونه:

  • nginx ورودی‌ها را دریافت و به api هدایت می‌کند.

  • api (Node.js/Express) به postgres وصل می‌شود و از redis برای کش استفاده می‌کند.

  • healthcheck برای اطمینان از آماده‌بودن سرویس‌ها.

  • استفاده از .env، ولوم پایدار دیتابیس، شبکه اختصاصی داخلی.

ساختار پوشه‌ها

project-root/
├─ nginx/
│  ├─ default.conf
├─ api/
│  ├─ Dockerfile
│  ├─ package.json
│  └─ src/
│     └─ index.js
├─ .env
└─ docker-compose.yml

فایل docker-compose.yml (Compose v2)

version: "3.9"

name: multi-service-stack

services:
  nginx:
    image: nginx:1.27-alpine
    container_name: web
    depends_on:
      api:
        condition: service_healthy
    ports:
      - "۸۰:۸۰"
    volumes:
      - ./nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
    networks:
      - app_net

  api:
    build:
      context: ./api
      dockerfile: Dockerfile
    container_name: api
    environment:
      NODE_ENV: ${NODE_ENV:-development}
      DB_HOST: postgres
      DB_USER: ${POSTGRES_USER}
      DB_PASSWORD: ${POSTGRES_PASSWORD}
      DB_NAME: ${POSTGRES_DB}
      REDIS_HOST: redis
      REDIS_PORT: 6379
      PORT: 3000
    expose:
      - "۳۰۰۰"
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
    healthcheck:
      test: ["CMD-SHELL", "wget -qO- http://localhost:3000/health || exit 1"]
      interval: 10s
      timeout: 3s
      retries: 5
      start_period: 10s
    networks:
      - app_net

  postgres:
    image: postgres:16-alpine
    container_name: db
    environment:
      POSTGRES_USER: ${POSTGRES_USER:-appuser}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-apppass}
      POSTGRES_DB: ${POSTGRES_DB:-appdb}
    volumes:
      - db_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-appuser} -d ${POSTGRES_DB:-appdb}"]
      interval: 10s
      timeout: 3s
      retries: 5
    networks:
      - app_net

  redis:
    image: redis:7-alpine
    container_name: cache
    command: ["redis-server", "--save", "60", "1000", "--appendonly", "yes"]
    volumes:
      - redis_data:/data
    healthcheck:
      test: ["CMD", "redis-cli", "PING"]
      interval: 10s
      timeout: 3s
      retries: 5
    networks:
      - app_net

volumes:
  db_data:
  redis_data:

networks:
  app_net:
    driver: bridge

nginx/default.conf (Reverse Proxy)

server {
  listen 80;
  server_name _;

  location / {
    proxy_pass http://api:3000/;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
  }

  location /healthz {
    return 200 "ok";
    add_header Content-Type text/plain;
  }
}

api/Dockerfile (Node.js 20 + production-ready)

FROM node:20-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev

FROM node:20-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build || echo "no build step"

FROM node:20-alpine
WORKDIR /app
ENV NODE_ENV=production
COPY --from=builder /app ./
EXPOSE 3000
CMD ["node", "src/index.js"]

api/src/index.js (نمونه API با مسیر سلامت)

import http from "http";
import { Client } from "pg";
import { createClient } from "redis";

const PORT = process.env.PORT || 3000;

const db = new Client({
  host: process.env.DB_HOST,
  user: process.env.DB_USER,
  password: process.env.DB_PASSWORD,
  database: process.env.DB_NAME,
});
const redis = createClient({ url: `redis://${process.env.REDIS_HOST}:6379` });

async function start() {
  await db.connect();
  await redis.connect();

  const server = http.createServer(async (req, res) => {
    if (req.url === "/health") {
      try {
        await db.query("SELECT 1");
        await redis.ping();
        res.writeHead(200, { "Content-Type": "application/json" });
        return res.end(JSON.stringify({ status: "ok" }));
      } catch (e) {
        res.writeHead(500);
        return res.end("unhealthy");
      }
    }

    if (req.url === "/") {
      res.writeHead(200, { "Content-Type": "text/plain" });
      return res.end("Hello from API!");
    }

    res.writeHead(404);
    res.end("Not Found");
  });

  server.listen(PORT, () => console.log(`API on :${PORT}`));
}

start().catch((err) => {
  console.error(err);
  process.exit(1);
});

فایل .env (نمونهٔ امنِ محلی)

NODE_ENV=development
POSTGRES_USER=appuser
POSTGRES_PASSWORD=apppass
POSTGRES_DB=appdb

اجرای استک

بالا آوردن:

docker compose up -d --build

مشاهده لاگ‌ها:

docker compose logs -f api

توقف/پاک‌سازی:

docker compose down
docker compose down -v   # با حذف ولوم‌ها

بهترین‌تمرین‌ها (Best Practices)

  1. healthcheck و depends_on با condition: مطمئن شوید سرویس‌ها فقط پس از آماده‌شدن وابستگی‌ها بالا بیایند.

  2. .env و secrets: متغیرهای حساس را در .env و برای تولید از secrets و مخازن امن (Vault) استفاده کنید.

  3. ولوم پایدار برای دیتابیس: هرگز دیتابیس را بدون ولوم پایدار بالا نیاورید.

  4. شبکه اختصاصی: شبکه‌ی bridge اختصاصی برای جداسازی سرویس‌ها ایجاد کنید.

  5. تصویرهای کوچک و امن: از تصاویر پایه‌ی کم‌حجم (alpine) استفاده کنید، کاربر non-root، حذف ابزارهای اضافی.

  6. Lint و اسکن آسیب‌پذیری: Trivy/Grype را در CI اضافه کنید.

  7. Observability: لاگ ساختاریافته، metrics و tracing (Prometheus/Grafana/OTel) را بیفزایید.

  8. Profiles: با profiles محیط dev/stage/prod را از هم جدا کنید.

  9. Rate limits در Nginx: برای محافظت از API در ورودی.

  10. Readiness در اپ: مسیر /health یا /ready را سبک و سریع نگه دارید.

نمونه Profiles

services:
  api:
    profiles: ["dev"]
  nginx:
    profiles: ["dev","prod"]

اجرا برای prod:

docker compose --profile prod up -d

خطاهای رایج و رفع آن‌ها

  • پورت اشغال است: خطای bind: address already in use → پورت را عوض کنید یا پروسه‌ی اشغال‌کننده را ببندید.

  • اتصال DB/Redis برقرار نمی‌شود: نام سرویس را به‌عنوان hostname استفاده کنید (postgres، redis).

  • لوپ رستارت: healthcheck سخت‌گیرانه یا وابستگی دیرآماده. بازه‌ها و retries را تنظیم کنید.

  • Permission در ولوم‌ها: در کانتینر user بسازید یا مالکیت مسیر را تنظیم کنید.

  • کندی در Mac/Windows: bind mount سنگین است؛ از ولوم یا cached/delegated استفاده کنید.

نکات تولید (Production)

  • Compose vs Swarm/Kubernetes: Compose برای توسعه و تیم‌های کوچک عالی است؛ برای مقیاس و HA به Swarm/K8s فکر کنید.

  • Rolling updates: در Swarm از بخش deploy استفاده کنید؛ در Kubernetes از Deployment/StatefulSet.

  • Backup & Restore: ولوم‌های دیتابیس را با اسکریپت‌های زمان‌بندی‌شده پشتیبان‌گیری کنید.

  • Zero-downtime: چند replica + لودبالانسر (nginx/haproxy/traefik).

  • TLS اجباری: در production حتماً HTTPS (با Certbot/Traefik) فعال باشد.

پرسش‌های متداول (FAQ)

۱) چرا سرویس‌ها همدیگر را با localhost نمی‌بینند؟
داخل شبکه‌ی Compose از نام سرویس به‌عنوان hostname استفاده کنید، نه localhost.

۲) آیا می‌توانم چند فایل Compose داشته باشم؟
بله:

docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d

۳) چگونه مهاجرت دیتابیس را اتوماتیک کنم؟
ورودی command یا اسکریپت entrypoint برای اجرای migrations در startup.

چک‌لیست سریع راه‌اندازی

  • Docker/Compose نصب شد

  • فایل‌های Dockerfile و compose آماده‌اند

  • .env تکمیل شد

  • healthcheck تنظیم شد

  • ولوم پایدار دیتابیس تعریف شد

  • reverse proxy و rewriteها تست شدند

  • لاگ‌ها و متریک‌ها رصد می‌شوند

۵/۵ - (۵ امتیاز)