# مقادیر محرمانه از فایل .env ریشه خوانده می‌شوند — از .env.example کپی کنید: # cp .env.example .env (یا: bash scripts/gen-env.sh) # برای انتشار با TLS، روی این فایل docker-compose.prod.yml هم سوار شود (README): # bash scripts/stack.sh deploy-prod # # لایه‌های اختیاری: # docker-compose.monitoring.yml — Prometheus/Grafana/Loki (پروفایل زیرساخت) # پروفایل ops — پشتیبان ساعتی دیتابیس # پروفایل tools — ابزار mc برای پشتیبان‌گیری دستی x-default-logging: &default-logging # بدون این، لاگ json-file بی‌مرز رشد می‌کند و دیسک سرور را پر می‌کند driver: json-file options: max-size: "10m" max-file: "3" services: # نقطه ورود واحد — مسیریابی / (سایت استاتیک)، /api (بک‌اند)، /media (MinIO)، # /chat (چت‌بات)، / (آنالیتیکس روی analytics.localhost — پایین همین فایل) traefik: image: traefik:v3.3 restart: unless-stopped logging: *default-logging command: - --providers.docker=true - --providers.docker.exposedbydefault=false # فقط کانتینرهای همین استک — با استک‌های دیگر هم‌میزبان تداخل نام روتر پیش نمی‌آید - --providers.docker.constraints=Label(`com.docker.compose.project`, `aramesh-backend`) - --entrypoints.web.address=:80 # healthcheck داخلی + متریک‌ها برای Prometheus + لاگ دسترسی - --ping=true - --accesslog=true - --metrics.prometheus=true ports: - "${TRAEFIK_HTTP_PORT:-5173}:80" volumes: - /var/run/docker.sock:/var/run/docker.sock:ro networks: [edge] healthcheck: test: ["CMD", "traefik", "healthcheck", "--ping"] interval: 15s timeout: 5s retries: 5 start_period: 10s deploy: resources: limits: memory: 256M db: # pgvector/pgvector:pg17 = همان PostgreSQL 17 + افزونهٔ vector برای چت‌بات image: pgvector/pgvector:pg17 restart: unless-stopped logging: *default-logging environment: POSTGRES_USER: aramesh POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?POSTGRES_PASSWORD را در .env تنظیم کنید} POSTGRES_DB: arameshplaza volumes: - db_data:/var/lib/postgresql/data shm_size: 256m networks: [internal] healthcheck: test: ["CMD-SHELL", "pg_isready -U aramesh -d arameshplaza"] interval: 5s timeout: 5s retries: 12 start_period: 10s deploy: resources: limits: memory: 512M minio: image: minio/minio:RELEASE.2025-04-22T22-12-26Z restart: unless-stopped logging: *default-logging command: server /data --console-address ":9001" environment: MINIO_ROOT_USER: ${MINIO_ROOT_USER:-aramesh} MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD:?MINIO_ROOT_PASSWORD را در .env تنظیم کنید} volumes: - minio_data:/data networks: [edge, internal] healthcheck: test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"] interval: 5s timeout: 5s retries: 12 start_period: 10s ports: # در تولید، docker-compose.prod.yml این‌ها را فقط روی 127.0.0.1 می‌بندد — # کنسول رمز root دارد و نباید به اینترنت باز باشد - "9001:9001" - "19000:9000" labels: - traefik.enable=true - traefik.http.routers.media.rule=PathPrefix(`/media`) - traefik.http.routers.media.entrypoints=web - traefik.http.routers.media.priority=100 - traefik.http.middlewares.media-rewrite.replacepathregex.regex=^/media/(.*) - traefik.http.middlewares.media-rewrite.replacepathregex.replacement=/aramesh-media/$$1 - traefik.http.routers.media.middlewares=media-rewrite - traefik.http.services.media.loadbalancer.server.port=9000 deploy: resources: limits: memory: 512M # یک‌بار اجرا می‌شود: ساخت باکت، بازگذاشتن خواندن عمومی و پیش‌بارگذاری تصویرهای seed # فقط وقتی باکت خالی است — تا هر deploy، کپی تکراری تصویرها را تکرار نکند. minio-setup: image: minio/mc:RELEASE.2025-04-08T15-39-49Z depends_on: minio: condition: service_healthy environment: MINIO_ROOT_USER: ${MINIO_ROOT_USER:-aramesh} MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD:?MINIO_ROOT_PASSWORD را در .env تنظیم کنید} MINIO_BUCKET: aramesh-media volumes: - ./seed-media:/seed-media:ro restart: "no" networks: [internal] logging: *default-logging entrypoint: > /bin/sh -c " mc alias set local http://minio:9000 $$MINIO_ROOT_USER $$MINIO_ROOT_PASSWORD && mc mb --ignore-existing local/$$MINIO_BUCKET && mc anonymous set download local/$$MINIO_BUCKET && COUNT=$$(mc ls --recursive local/$$MINIO_BUCKET | wc -l) && if [ \"$$COUNT\" -eq 0 ]; then echo 'minio-setup: bucket ready — mirroring seed media'; mc mirror --overwrite /seed-media local/$$MINIO_BUCKET/; else echo \"minio-setup: bucket ready — $$COUNT object(s) already present, seed mirror skipped\"; fi " # یک‌بار اجرا می‌شود: دیتابیس‌های جانبی روی همان نمونهٔ Postgres # (umami برای آنالیتیکس، aramesh_chatbot برای بردارهای چت‌بات) db-init: image: postgres:17-alpine restart: "no" depends_on: db: condition: service_healthy environment: PGHOST: db PGUSER: aramesh PGPASSWORD: ${POSTGRES_PASSWORD:?POSTGRES_PASSWORD را در .env تنظیم کنید} networks: [internal] logging: *default-logging entrypoint: > /bin/sh -c " for DBN in umami aramesh_chatbot; do psql -tAc \"SELECT 1 FROM pg_database WHERE datname='$$DBN'\" | grep -q 1 || psql -c \"CREATE DATABASE $$DBN\"; echo \"db-init: $$DBN ready\"; done " backend: build: ./backend restart: unless-stopped logging: *default-logging environment: DATABASE_URL: postgresql+psycopg://aramesh:${POSTGRES_PASSWORD}@db:5432/arameshplaza SECRET_KEY: ${SECRET_KEY:?SECRET_KEY را در .env تنظیم کنید} CORS_ORIGINS: ${CORS_ORIGINS:-["http://localhost:5173","http://127.0.0.1:5173"]} ADMIN_USERNAME: ${ADMIN_USERNAME:-admin} ADMIN_PASSWORD: ${ADMIN_PASSWORD:?ADMIN_PASSWORD را در .env تنظیم کنید} S3_ENDPOINT: http://minio:9000 S3_ACCESS_KEY: ${MINIO_ROOT_USER:-aramesh} S3_SECRET_KEY: ${MINIO_ROOT_PASSWORD:?MINIO_ROOT_PASSWORD را در .env تنظیم کنید} S3_BUCKET: aramesh-media MEDIA_PUBLIC_BASE: /media # نشانی عمومی سایت — برای sitemap و آدرس مطلق OG (مثل https://aramesh.example) SITE_URL: ${SITE_URL:-} # بیلد فرانت‌اند برای تزریق متای سئو در صفحات /blog و /projects FRONTEND_DIST: /srv/frontend volumes: - ./frontend/dist:/srv/frontend:ro networks: [edge, internal] depends_on: db: condition: service_healthy minio: condition: service_healthy db-init: condition: service_completed_successfully healthcheck: test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/api/health')"] interval: 15s timeout: 5s retries: 5 start_period: 20s labels: - traefik.enable=true - traefik.http.routers.api.rule=PathPrefix(`/api`) - traefik.http.routers.api.entrypoints=web - traefik.http.routers.api.priority=100 # لایه دوم محافظت از فرم‌ها — سیلاب درخواست را پیش از رسیدن به بک‌اند می‌بُرد؛ # محدودسازی دقیق هر فرم در خود بک‌اند است (rate_limit.py) - traefik.http.routers.api.middlewares=api-flood - traefik.http.middlewares.api-flood.ratelimit.average=50 - traefik.http.middlewares.api-flood.ratelimit.period=1s - traefik.http.middlewares.api-flood.ratelimit.burst=150 - traefik.http.services.api.loadbalancer.server.port=8000 # robots.txt و sitemap.xml و صفحات وبلاگ/پروژه با متای سئو — اولویت بالاتر از SPA # مسیرهای /en/... هم همه به بک‌اند می‌روند تا متای سئوی انگلیسی و html انگلیسی تزریق شود - traefik.http.routers.seo.rule=PathPrefix(`/blog/`) || Path(`/blog`) || PathPrefix(`/projects/`) || Path(`/projects`) || Path(`/en`) || PathPrefix(`/en/`) || PathPrefix(`/robots.txt`) || PathPrefix(`/sitemap.xml`) - traefik.http.routers.seo.entrypoints=web - traefik.http.routers.seo.priority=50 - traefik.http.routers.seo.service=api # /metrics فقط برای Prometheus — روتری تعریف نمی‌شود، از بیرون در دسترس نیست deploy: resources: limits: memory: 768M # فقط سرو فایل‌های استاتیک SPA — بیلد روی هاست (npm run build) انجام شده است frontend: build: ./frontend restart: unless-stopped logging: *default-logging depends_on: backend: condition: service_healthy networks: [edge] healthcheck: test: ["CMD", "wget", "-q", "--spider", "http://localhost/"] interval: 15s timeout: 5s retries: 5 start_period: 5s labels: - traefik.enable=true - traefik.http.routers.web.rule=PathPrefix(`/`) - traefik.http.routers.web.entrypoints=web - traefik.http.routers.web.priority=1 - traefik.http.services.web.loadbalancer.server.port=80 deploy: resources: limits: memory: 64M # دستیار هوشمند — RAG روی محتوای سایت با pgvector # اولین اجرا مدل‌ها را دانلود می‌کند (~۵۰۰MB روی حجم chatbot_hf) — کند است، طبیعی است chatbot: build: ./chatbot restart: unless-stopped logging: *default-logging environment: DATABASE_URL: postgresql+psycopg://aramesh:${POSTGRES_PASSWORD}@db:5432/arameshplaza LLM_BASE_URL: ${LLM_BASE_URL:-} LLM_API_KEY: ${LLM_API_KEY:-} LLM_MODEL: ${LLM_MODEL:-openai/gpt-oss-120b} CHAT_REINDEX_TOKEN: ${CHAT_REINDEX_TOKEN:-} volumes: - chatbot_hf:/data networks: [edge, internal] depends_on: db: condition: service_healthy db-init: condition: service_completed_successfully labels: - traefik.enable=true - traefik.http.routers.chat.rule=PathPrefix(`/chat`) - traefik.http.routers.chat.entrypoints=web - traefik.http.routers.chat.priority=100 - traefik.http.routers.chat.middlewares=api-flood - traefik.http.services.chat.loadbalancer.server.port=8000 deploy: resources: limits: memory: 2G # آنالیتیکس محصول (Umami) — بازدید، ارجاع، رخدادهای سفارشی (فرم/تلفن/چت‌بات) # داشبورد: analytics.localhost (توسعه) یا analytics.DOMAIN (تولید) umami: image: ghcr.io/umami-software/umami:postgresql-v2.17.0 restart: unless-stopped logging: *default-logging environment: DATABASE_URL: postgresql://aramesh:${POSTGRES_PASSWORD}@db:5432/umami # اسکریپت ردیاب به ارجاع‌دهندهٔ واقعی نیاز دارد — پشت Traefik CLIENT_IP_HEADER: x-forwarded-for DISABLE_TELEMETRY: 1 networks: [edge, internal] depends_on: db: condition: service_healthy db-init: condition: service_completed_successfully healthcheck: test: ["CMD-SHELL", "wget -q --spider http://localhost:3000/api/heartbeat || exit 1"] interval: 30s timeout: 5s retries: 5 start_period: 60s labels: - traefik.enable=true - traefik.http.routers.analytics.rule=Host(`analytics.localhost`) - traefik.http.routers.analytics.entrypoints=web - traefik.http.services.analytics.loadbalancer.server.port=3000 deploy: resources: limits: memory: 512M # ابزار پشتیبان‌گیری — فقط با پروفایل tools اجرا می‌شود (scripts/backup.sh این کار را می‌کند) mc: image: minio/mc:RELEASE.2025-04-08T15-39-49Z profiles: ["tools"] depends_on: minio: condition: service_healthy environment: MINIO_ROOT_USER: ${MINIO_ROOT_USER:-aramesh} MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD:?MINIO_ROOT_PASSWORD را در .env تنظیم کنید} networks: [internal] # پشتیبان ساعتی خودکار دیتابیس — بدون نیاز به cron میزبان؛ فقط با پروفایل ops # docker compose --profile ops up -d db-backup # RPO را از ۲۴ ساعت (cron شبانه) به ۱ ساعت می‌رساند؛ scripts/backup.sh برای # پشتیبان کامل (دیتابیس + تصاویر MinIO) سر جایش است. db-backup: image: prodrigestivill/postgres-backup-local:17 profiles: ["ops"] restart: unless-stopped logging: *default-logging depends_on: db: condition: service_healthy environment: POSTGRES_HOST: db POSTGRES_DB: arameshplaza POSTGRES_USER: aramesh POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?POSTGRES_PASSWORD را در .env تنظیم کنید} SCHEDULE: "@hourly" BACKUP_NUM_KEEP: "48" BACKUP_DIR: /backups volumes: - backups:/backups networks: [internal] deploy: resources: limits: memory: 128M networks: edge: # سرویس‌هایی که Traefik به آن‌ها مسیریابی می‌کند internal: # دیتابیس و ذخیره‌سازی — فرانت‌اند راهی به این شبکه ندارد volumes: db_data: minio_data: chatbot_hf: backups: