# لایه تولید — TLS با Let's Encrypt و انتشار روی پورت‌های استاندارد ۸۰/۴۳. # پیش‌نیاز: متغیرهای DOMAIN و ACME_EMAIL در .env؛ به‌علاوه رکورد DNS دامنه به این سرور. # # bash scripts/stack.sh deploy-prod # (معادل: docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d --build) # # همه درخواست‌های HTTP به HTTPS هدایت می‌شوند و مسیریابی‌ها فقط روی دامنه DOMAIN پاسخ می‌دهند. # برای داشبوردها: analytics.DOMAIN (آنالیتیکس) و grafana.DOMAIN (مانیتورینگ، با فایل monitoring). # # ---- پیش‌نمایش خصوصی و انتشار (scripts/preview-mode.sh) ---- # در حالت پیش‌نمایش (PREVIEW_MODE=on) کل سایت پشت Basic Auth است و noindex می‌گیرد: # تیم با یک نام کاربری/رمز مشترک وارد می‌شود، محتوا را ویرایش می‌کند و هیچ‌کس دیگری — # از جمله گوگل — به سایت دسترسی ندارد. برای انتشار عمومی: # bash scripts/preview-mode.sh off → deploy مجدد → ثبت sitemap در Search Console. # # DOMAIN دامنه اصلی است و DOMAIN_ALIAS دامنه دوم (هر دو مسیر و گواهی می‌گیرند). # در زمان انتشار، برای جلوگیری از محتوای تکراری، دامنه دوم را به اصلی ریدایرکت کنید: # ALIAS_REDIRECT=on (در .env) — ریدایرکت ۳۰۱ دائمی به دامنه اصلی. services: traefik: command: !override - --providers.docker=true - --providers.docker.exposedbydefault=false - --providers.docker.constraints=Label(`com.docker.compose.project`, `aramesh-backend`) - --entrypoints.web.address=:80 - --entrypoints.web.http.redirections.entrypoint.to=websecure - --entrypoints.web.http.redirections.entrypoint.scheme=https - --entrypoints.websecure.address=:443 - --certificatesresolvers.le.acme.email=${ACME_EMAIL:?ACME_EMAIL را در .env تنظیم کنید} - --certificatesresolvers.le.acme.storage=/letsencrypt/acme.json - --certificatesresolvers.le.acme.httpchallenge=true - --certificatesresolvers.le.acme.httpchallenge.entrypoint=web - --ping=true - --accesslog=true - --metrics.prometheus=true ports: !override - "80:80" - "443:443" volumes: !override - /var/run/docker.sock:/var/run/docker.sock:ro - letsencrypt_data:/letsencrypt # MinIO کنسول (رمز root) و API مستقیمش فقط از localhost سرور در دسترس باشند — # از راه دور با تونل SSH: ssh -L 9001:127.0.0.1:9001 server minio: ports: !override - "127.0.0.1:9001:9001" - "127.0.0.1:19000:9000" labels: - traefik.enable=true - traefik.http.routers.media.rule=Host(`${DOMAIN}`) && PathPrefix(`/media`) - traefik.http.routers.media.entrypoints=websecure - traefik.http.routers.media.tls.certresolver=le - traefik.http.routers.media.priority=100 - traefik.http.routers.media.middlewares=${PREVIEW_MIDDLEWARES:+$PREVIEW_MIDDLEWARES,}media-rewrite,sec-headers - traefik.http.middlewares.media-rewrite.replacepathregex.regex=^/media/(.*) - traefik.http.middlewares.media-rewrite.replacepathregex.replacement=/aramesh-media/$$1 - traefik.http.services.media.loadbalancer.server.port=9000 backend: environment: # سایت هم‌مبدأ است — CORS لازم نیست CORS_ORIGINS: '[]' SITE_URL: https://${DOMAIN:?DOMAIN را در .env تنظیم کنید} # پیش‌نمایش خصوصی — هدر noindex + robots مسدودکننده (لایه دوم؛ لایه اول گیت Traefik) PREVIEW_MODE: ${PREVIEW_MODE:-off} labels: - traefik.enable=true - traefik.http.routers.api.rule=Host(`${DOMAIN}`) && PathPrefix(`/api`) - traefik.http.routers.api.entrypoints=websecure - traefik.http.routers.api.tls.certresolver=le - traefik.http.routers.api.middlewares=${PREVIEW_MIDDLEWARES:+$PREVIEW_MIDDLEWARES,}api-flood,sec-headers # سئو: همهٔ صفحات عمومی از بک‌اند سرو می‌شوند تا متا در HTML اولیه تزریق شود — # مسیرهای استاتیک دقیق‌اند (Path) تا assets/ فونت‌ها از Caddy سرو شوند - traefik.http.routers.seo.rule=Host(`${DOMAIN}`) && (Path(`/`) || Path(`/about`) || Path(`/services`) || Path(`/investment`) || Path(`/faq`) || Path(`/contact`) || PathPrefix(`/blog/`) || Path(`/blog`) || PathPrefix(`/projects/`) || Path(`/projects`) || Path(`/en`) || PathPrefix(`/en/`) || PathPrefix(`/robots.txt`) || PathPrefix(`/sitemap.xml`) || PathPrefix(`/llms.txt`)) - traefik.http.routers.seo.entrypoints=websecure - traefik.http.routers.seo.tls.certresolver=le - traefik.http.routers.seo.middlewares=${PREVIEW_MIDDLEWARES:+$PREVIEW_MIDDLEWARES,}sec-headers frontend: labels: - traefik.enable=true - traefik.http.routers.web.rule=Host(`${DOMAIN}`) && PathPrefix(`/`) - traefik.http.routers.web.entrypoints=websecure - traefik.http.routers.web.tls.certresolver=le - traefik.http.routers.web.middlewares=${PREVIEW_MIDDLEWARES:+$PREVIEW_MIDDLEWARES,}sec-headers # تعریف یک‌بارهٔ میان‌افزار هدرهای امنیتی — روی همهٔ روترهای websecure استفاده می‌شود - traefik.http.middlewares.sec-headers.headers.stsSeconds=31536000 - traefik.http.middlewares.sec-headers.headers.stsIncludeSubdomains=true - traefik.http.middlewares.sec-headers.headers.stsPreload=true - traefik.http.middlewares.sec-headers.headers.contentTypeNosniff=true # ---- گیت پیش‌نمایش خصوصی ---- # ورود با نام کاربری/رمز مشترک (htpasswd) — تا وقتی PREVIEW_MIDDLEWARES تنظیم است فعال می‌شود - traefik.http.middlewares.preview-auth.basicauth.users=${PREVIEW_AUTH_USERS:-disabled:nope} - traefik.http.middlewares.preview-auth.basicauth.realm=Aramesh private preview # لایه دوم جلوگیری از ایندکس — حتی اگر گیت حذف شد، گوگل چیزی ذخیره نکند - traefik.http.middlewares.preview-noindex.headers.customresponseheaders.X-Robots-Tag=noindex, nofollow, noarchive # ---- ریدایرکت دامنه دوم به اصلی (زمان انتشار: ALIAS_REDIRECT=on) ---- - traefik.http.middlewares.to-primary.redirectregex.regex=^https?://[^/]+(.*) - traefik.http.middlewares.to-primary.redirectregex.replacement=https://${DOMAIN}$$1 - traefik.http.middlewares.to-primary.redirectregex.permanent=true - traefik.http.routers.alias-redirect.rule=Host(`${DOMAIN_ALIAS:-invalid.invalid}`) - traefik.http.routers.alias-redirect.entrypoints=websecure - traefik.http.routers.alias-redirect.tls.certresolver=le - traefik.http.routers.alias-redirect.priority=1000 - traefik.http.routers.alias-redirect.middlewares=${ALIAS_REDIRECT_MIDDLEWARES:-sec-headers} - traefik.http.routers.alias-redirect.service=web chatbot: labels: - traefik.enable=true - traefik.http.routers.chat.rule=Host(`${DOMAIN}`) && PathPrefix(`/chat`) - traefik.http.routers.chat.entrypoints=websecure - traefik.http.routers.chat.tls.certresolver=le - traefik.http.routers.chat.priority=100 - traefik.http.routers.chat.middlewares=${PREVIEW_MIDDLEWARES:+$PREVIEW_MIDDLEWARES,}api-flood,sec-headers umami: labels: - traefik.enable=true - traefik.http.routers.analytics.rule=Host(`analytics.${DOMAIN}`) - traefik.http.routers.analytics.entrypoints=websecure - traefik.http.routers.analytics.tls.certresolver=le - traefik.http.routers.analytics.middlewares=sec-headers - traefik.http.services.analytics.loadbalancer.server.port=3000 # داشبورد Grafana (docker-compose.monitoring.yml) در تولید با TLS نیاز به روتر # Host(`grafana.DOMAIN`) دارد — چون آن سرویس فقط با فایل monitoring بالا می‌آید، # برچسب‌هایش را در override سرور بگذارید (README، بخش مانیتورینگ): # docker-compose.override.yml روی سرور: # services: # grafana: # labels: # - traefik.http.routers.grafana.rule=Host(`grafana.YOURDOMAIN`) # - traefik.http.routers.grafana.entrypoints=websecure # - traefik.http.routers.grafana.tls.certresolver=le # - traefik.http.services.grafana.loadbalancer.server.port=3000 volumes: letsencrypt_data: