Security: deep /api/health (DB+MinIO probes, 503 on failure) + /api/live; rate-limited public forms with honeypots; real client IP behind Traefik (--proxy-headers); TipTap URL sanitization on write (nh3); non-root containers + slim .dockerignore; prod MinIO console bound to 127.0.0.1; HSTS/nosniff on TLS routers; JWT 8h; no default credentials in code. Reliability: Alembic migrations (create_all baseline); graceful shutdown (exec uvicorn); hardened backups (checksums, sanity check, freshness gauge, optional rclone off-site) + restore.sh + hourly db-backup sidecar (ops profile); stack.sh ops tooling (deploy/logs/health/backup/restore/monitoring); .gitattributes LF for shell scripts. Orchestration: json-file log rotation, pinned images, per-service memory limits, edge/internal networks, Traefik ping/accesslog/metrics + flood limiter, conditional minio-setup, pgvector db image, one-shot db-init (umami + aramesh_chatbot databases). Chatbot: unified from site-chatbot branch (RAG + pgvector, SSE, ChatWidget). Monitoring: Prometheus/Alertmanager/Loki/Promtail/Grafana + cAdvisor, node/postgres/blackbox exporters, provisioned dashboards and 10 alert rules; alerts delivered to Bale (tapi.bale.ai — Telegram-compatible). Analytics: Umami on shared Postgres, build-time tracker injection, custom events (contact/newsletter/phone/chat/lang) via frontend/src/analytics.ts. Hygiene: CSV export for contact messages and subscribers; upload image optimization (Pillow); CI script + GitHub-compatible workflow; audit.sh; gen-env.sh for strong credentials.
363 lines
14 KiB
YAML
363 lines
14 KiB
YAML
# مقادیر محرمانه از فایل .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:
|