- docker-compose.uat.yml: map the 11 keys dev compose gained since the Oct-16 promo (TREG_TOKEN, GOOGLE_VISION_API_KEY, REVERSE_LOOKUP_PUBLIC_BASE_URL, CLOUDFLARE_ACCOUNT_ID, CLOUDFLARE_API_TOKEN, SCRAPER_PROXY_URL, SCRAPER_PROXY_BRANDS, BD_SCRAPE_WSS, BD_SCRAPER_WSS, BRIGHTDATA_API_KEY, BD_UNLOCKER_ZONE) into shared x-backend-env (backend + worker). - MAIL_DRY_RUN mapped with safe default true (was unmapped; backend silently fell back to code default False). - .env.example: document the new keys (empty values only). Values come from host .env.uat (gitignored), filled pre-deploy.
259 lines
12 KiB
YAML
259 lines
12 KiB
YAML
# SAPL Garment Intelligence — UAT Docker Compose
|
|
# Same services as production but with:
|
|
# - UAT-specific volume (sapl_uat_data)
|
|
# - UAT-specific subdomain for Traefik
|
|
# - UAT-specific env vars
|
|
# - UAT-specific Docker Compose project name
|
|
# - PostgreSQL database (postgres service) instead of SQLite
|
|
#
|
|
# Deploy: auto-deployed via Gitea Actions on push to uat branch
|
|
# Access: https://uat.ai-automation-hub.tech
|
|
#
|
|
# Security model (reviewed 2026-08-03):
|
|
# * NO secret values are committed. Credentials (incl. DATABASE_URL and
|
|
# POSTGRES_PASSWORD) are injected at deploy time via ${VAR:?...}
|
|
# (required) or ${VAR:-} (optional, empty) — from Dokploy environment
|
|
# variables, or `--env-file .env.uat` for local/CI deploys. A missing
|
|
# required secret fails `docker compose` loudly instead of silently
|
|
# deploying with a hardcoded value.
|
|
# * Networking: only `frontend` attaches to the external dokploy-network
|
|
# (Traefik). `backend`, `worker` and `postgres` stay on the private
|
|
# project network — never directly reachable from Traefik or the
|
|
# internet. No host ports are published; nginx proxies /api etc. to the
|
|
# backend over the private network.
|
|
#
|
|
# Background sync loops (Stitch/WFX/Gmail) run in the dedicated `worker`
|
|
# service (backend/sync_worker.py), NOT in the web backend. The web backend
|
|
# sets RUN_SYNC_LOOPS=false so gunicorn can safely scale to multiple workers
|
|
# without duplicating sync loops. Local dev (uvicorn --reload) still runs
|
|
# loops in-process via the config default RUN_SYNC_LOOPS=true.
|
|
|
|
# Shared environment for the Python services (backend web + worker).
|
|
# Anchored so both services see the exact same configuration.
|
|
x-backend-env: &backend-env
|
|
# ── Database — must point at the `postgres` service below with
|
|
# matching POSTGRES_USER / POSTGRES_PASSWORD / POSTGRES_DB. ──
|
|
DATABASE_URL: "${DATABASE_URL:?DATABASE_URL is required (postgresql URL for the postgres service)}"
|
|
# ── Paths / non-secret config ──
|
|
CHROMADB_PATH: "/app/data/chromadb"
|
|
IMAGES_PATH: "/app/data/images"
|
|
CORS_ORIGINS: "https://uat.ai-automation-hub.tech"
|
|
QR_BASE_URL: "https://uat.ai-automation-hub.tech"
|
|
# Google client ID is a PUBLIC identifier (exposed to the browser via the
|
|
# "Sign in with Google" button) — safe to commit.
|
|
GOOGLE_CLIENT_ID: "800260541735-6o4rechdhoufgjann9scs57gi8a73fas.apps.googleusercontent.com"
|
|
GOOGLE_ALLOWED_DOMAIN: "saplind.com"
|
|
# Web Push (issue #524): optional - empty disables push gracefully.
|
|
VAPID_PRIVATE_KEY: "${VAPID_PRIVATE_KEY:-}"
|
|
VAPID_PUBLIC_KEY: "${VAPID_PUBLIC_KEY:-}"
|
|
VAPID_SUBJECT: "${VAPID_SUBJECT:-}"
|
|
# ── Release notes / "What's new" notifications ──
|
|
# Token the deploy workflow presents as X-Release-Token. EMPTY = publishing
|
|
# DISABLED (the endpoint answers 503) — set it in .env.uat.
|
|
RELEASE_PUBLISH_TOKEN: "${RELEASE_PUBLISH_TOKEN:-}"
|
|
# Which tier's release notes this deployment serves. Must be explicit: an
|
|
# unset value falls back to "dev" and would label these releases dev.
|
|
SAPL_TIER: "${SAPL_TIER:-uat}"
|
|
# ── Required secrets — no defaults (Dokploy env or --env-file .env.uat) ──
|
|
# Auth token signing secret; the app's built-in dev default is forgeable.
|
|
SECRET_KEY: "${SECRET_KEY:?SECRET_KEY is required (Dokploy env or .env.uat)}"
|
|
MISTRAL_API_KEY: "${MISTRAL_API_KEY:?MISTRAL_API_KEY is required}"
|
|
STITCH_USERNAME: "${STITCH_USERNAME:?STITCH_USERNAME is required}"
|
|
STITCH_PASSWORD: "${STITCH_PASSWORD:?STITCH_PASSWORD is required}"
|
|
STITCH_API_KEY: "${STITCH_API_KEY:-}"
|
|
WFX_USERNAME: "${WFX_USERNAME:?WFX_USERNAME is required}"
|
|
WFX_PASSWORD: "${WFX_PASSWORD:?WFX_PASSWORD is required}"
|
|
WFX_Token_ID: "${WFX_Token_ID:-}"
|
|
WFX_Token_Secret: "${WFX_Token_Secret:-}"
|
|
Mail_Auth_User: "${Mail_Auth_User:?Mail_Auth_User is required}"
|
|
Mail_App_Password: "${Mail_App_Password:-}"
|
|
# Send-as alias for ALL outbound email (trigger_mail From header). UAT uses
|
|
# the BOM alias so recipients see bom@saplind.com, never the SMTP auth account.
|
|
Mail_From: "${Mail_From:?Mail_From is required (BOM send-as alias, e.g. bom@saplind.com)}"
|
|
# UAT never sends real SMTP mail: dry-run logs instead of delivering.
|
|
# Belt-and-braces alongside the allowlist (unmapped before promo 20261023,
|
|
# which silently fell back to the code default False).
|
|
MAIL_DRY_RUN: "${MAIL_DRY_RUN:-true}"
|
|
# ── Optional secrets — empty default (inject only if the feature is used) ──
|
|
# Backend treats these as optional: Groq/Gemini are AI-provider fallbacks,
|
|
# Claude is not read by backend/config.py, and the Google client secret is
|
|
# not used by the ID-token SSO flow.
|
|
GROQ_API_KEY: "${GROQ_API_KEY:-}"
|
|
CLAUDE_API_KEY: "${CLAUDE_API_KEY:-}"
|
|
GEMINI_API_KEY: "${GEMINI_API_KEY:-}"
|
|
# Reverse Lookup S2+S3 (#1525): Lens lookup via treg + Vision fallback,
|
|
# then PDP trace + extract via Cloudflare Browser Rendering.
|
|
# Empty = that half fails/skips cleanly with the setup message (never mock rows).
|
|
TREG_TOKEN: "${TREG_TOKEN:-}"
|
|
GOOGLE_VISION_API_KEY: "${GOOGLE_VISION_API_KEY:-}"
|
|
REVERSE_LOOKUP_PUBLIC_BASE_URL: "${REVERSE_LOOKUP_PUBLIC_BASE_URL:-}"
|
|
CLOUDFLARE_ACCOUNT_ID: "${CLOUDFLARE_ACCOUNT_ID:-}"
|
|
CLOUDFLARE_API_TOKEN: "${CLOUDFLARE_API_TOKEN:-}"
|
|
GOOGLE_CLIENT_SECRET: "${GOOGLE_CLIENT_SECRET:-}"
|
|
# OpenCode Go — per-feature model routing (moodboard → deepseek-v4-flash,
|
|
# scraper → minimax-m3). Without this key, route() degrades to Gemini.
|
|
OPENCODE_GO_API_KEY: "${OPENCODE_GO_API_KEY:-}"
|
|
# UAT email anti-spam: only these mailboxes receive SMTP mail from UAT.
|
|
# Production compose does NOT set this — prod sends to all recipients.
|
|
MAIL_ALLOWLIST: "shridhar.sabat@saplind.com,ajay.sable@saplind.com,ajay@sonalapparels.in,dhvani.jagasia@saplind.com,pratik@sonalapparels.in"
|
|
# Optional JSON override of per-feature model routes (see services/llm/routing.py).
|
|
FEATURE_MODEL_ROUTES: "${FEATURE_MODEL_ROUTES:-}"
|
|
# Scraper egress proxy (Bright Data; refs #1780) — empty = direct, walled
|
|
# brands fail-soft with a clear skip reason instead of burning renders.
|
|
SCRAPER_PROXY_URL: "${SCRAPER_PROXY_URL:-}"
|
|
SCRAPER_PROXY_BRANDS: "${SCRAPER_PROXY_BRANDS:-}"
|
|
# Scraping-browser lane (H&M): Playwright connect_over_cdp wss endpoint.
|
|
# Primary + alias, first non-empty wins; empty = lane fail-soft skips.
|
|
BD_SCRAPE_WSS: "${BD_SCRAPE_WSS:-}"
|
|
BD_SCRAPER_WSS: "${BD_SCRAPER_WSS:-}"
|
|
# Bright Data Web Unlocker lane (Sezane + other DataDome-walled brands) —
|
|
# optional; empty = the lane skips with a clear reason. Token + zone are
|
|
# injected at deploy time (.env.uat, never committed).
|
|
BRIGHTDATA_API_KEY: "${BRIGHTDATA_API_KEY:-}"
|
|
BD_UNLOCKER_ZONE: "${BD_UNLOCKER_ZONE:-}"
|
|
# UAT-only: skip the built-in Fabric Library seed at boot (frozen-clean UAT)
|
|
FABRIC_SEED_DISABLED: "1"
|
|
# ── Google Workspace Directory API (org chart) — optional; empty = feature off ──
|
|
# GOOGLE_DIRECTORY_SA_JSON accepts a file path, inline JSON, or base64-encoded
|
|
# service-account key (see backend/services/google_directory_service.py).
|
|
# Never commit a real key value here.
|
|
GOOGLE_DIRECTORY_SA_JSON: "${GOOGLE_DIRECTORY_SA_JSON:-}"
|
|
GOOGLE_DIRECTORY_IMPERSONATED_ADMIN: "${GOOGLE_DIRECTORY_IMPERSONATED_ADMIN:-}"
|
|
# Org-chart-driven approval escalation — dev/uat validation
|
|
ORG_CHART_ENABLED: "${ORG_CHART_ENABLED:-false}"
|
|
# Google Calendar mirror (org calendars) — empty = falls back to
|
|
# GOOGLE_DIRECTORY_SA_JSON above; the same DWD client ID needs the
|
|
# calendar.readonly scope in Admin console for live data.
|
|
GOOGLE_CALENDAR_SA_JSON: "${GOOGLE_CALENDAR_SA_JSON:-}"
|
|
GOOGLE_CALENDAR_SYNC_INTERVAL_MINUTES: "${GOOGLE_CALENDAR_SYNC_INTERVAL_MINUTES:-30}"
|
|
# ── Non-secret, overridable ──
|
|
STITCH_API_HOST: "${STITCH_API_HOST:-https://portal2.stitcherp.com/}"
|
|
WFX_COMPANYID: "${WFX_COMPANYID:-sapl}"
|
|
WFX_Token_Name: "${WFX_Token_Name:-API Acces}"
|
|
|
|
services:
|
|
backend:
|
|
build:
|
|
context: ./backend
|
|
dockerfile: Dockerfile
|
|
depends_on:
|
|
postgres:
|
|
condition: service_healthy
|
|
environment:
|
|
<<: *backend-env
|
|
# Web container: HTTP only. The sync loops run in the `worker` service.
|
|
RUN_SYNC_LOOPS: "false"
|
|
volumes:
|
|
- sapl_uat_data:/app/data
|
|
expose:
|
|
- "8000"
|
|
healthcheck:
|
|
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/api/health', timeout=5)"]
|
|
interval: 30s
|
|
timeout: 5s
|
|
retries: 3
|
|
start_period: 60s
|
|
restart: unless-stopped
|
|
deploy:
|
|
resources:
|
|
limits:
|
|
cpus: '1.0'
|
|
|
|
# Background integration syncs (Stitch TNA 2-min, WFX mirror 30s, Gmail
|
|
# IMAP IDLE + 2-min poll, TNA template bootstrap). Runs sync_worker.py —
|
|
# a long-lived process, no HTTP. RUN_SYNC_LOOPS stays unset → config
|
|
# default true, so the loops start HERE, exactly once.
|
|
worker:
|
|
build:
|
|
context: ./backend
|
|
dockerfile: Dockerfile
|
|
command: ["python", "sync_worker.py"]
|
|
depends_on:
|
|
postgres:
|
|
condition: service_healthy
|
|
environment:
|
|
<<: *backend-env
|
|
volumes:
|
|
- sapl_uat_data:/app/data
|
|
restart: unless-stopped
|
|
deploy:
|
|
resources:
|
|
limits:
|
|
cpus: '0.75'
|
|
|
|
frontend:
|
|
build:
|
|
context: ./frontend
|
|
dockerfile: Dockerfile
|
|
args:
|
|
VITE_APP_VERSION: "${VITE_APP_VERSION:-uat-${GIT_SHA:-dev}}"
|
|
depends_on:
|
|
backend:
|
|
condition: service_healthy
|
|
restart: unless-stopped
|
|
labels:
|
|
# HTTP → HTTPS redirect (DNS: uat.ai-automation-hub.tech)
|
|
- traefik.http.routers.sapl-uat-web.rule=Host(`uat.ai-automation-hub.tech`)
|
|
- traefik.http.routers.sapl-uat-web.entrypoints=web
|
|
- traefik.http.services.sapl-uat-web.loadbalancer.server.port=80
|
|
- traefik.http.routers.sapl-uat-web.service=sapl-uat-web
|
|
- traefik.http.routers.sapl-uat-web.middlewares=redirect-to-https@file
|
|
# HTTPS — custom domain with Let's Encrypt
|
|
- traefik.http.routers.sapl-uat-websecure.rule=Host(`uat.ai-automation-hub.tech`)
|
|
- traefik.http.routers.sapl-uat-websecure.entrypoints=websecure
|
|
- traefik.http.services.sapl-uat-websecure.loadbalancer.server.port=80
|
|
- traefik.http.routers.sapl-uat-websecure.service=sapl-uat-websecure
|
|
- traefik.http.routers.sapl-uat-websecure.tls.certresolver=letsencrypt
|
|
- traefik.enable=true
|
|
- traefik.docker.network=dokploy-network
|
|
networks:
|
|
- dokploy-network
|
|
- default
|
|
healthcheck:
|
|
test: ["CMD-SHELL", "wget -q -O /dev/null http://127.0.0.1/ || exit 1"]
|
|
interval: 30s
|
|
timeout: 5s
|
|
retries: 3
|
|
start_period: 10s
|
|
deploy:
|
|
resources:
|
|
limits:
|
|
cpus: '0.25'
|
|
|
|
postgres:
|
|
image: postgres:16-alpine
|
|
environment:
|
|
POSTGRES_USER: "${POSTGRES_USER:-sapl}"
|
|
POSTGRES_PASSWORD: "${POSTGRES_PASSWORD:?POSTGRES_PASSWORD is required}"
|
|
POSTGRES_DB: "${POSTGRES_DB:-sapl}"
|
|
healthcheck:
|
|
# $$ escapes compose interpolation so the shell expands the container's
|
|
# own POSTGRES_USER/POSTGRES_DB at runtime.
|
|
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
|
|
interval: 10s
|
|
timeout: 5s
|
|
retries: 5
|
|
start_period: 10s
|
|
volumes:
|
|
- sapl_uat_postgres_data:/var/lib/postgresql/data
|
|
restart: unless-stopped
|
|
deploy:
|
|
resources:
|
|
limits:
|
|
cpus: '0.75'
|
|
|
|
volumes:
|
|
# Explicit, project-independent names so local/CI and Dokploy share the same
|
|
# UAT data volumes (app data + postgres). Distinct from prod's sapl_data.
|
|
sapl_uat_data:
|
|
name: sapl_uat_data
|
|
sapl_uat_postgres_data:
|
|
name: sapl_uat_postgres_data
|
|
|
|
networks:
|
|
# Frontend-only attachment to Dokploy's Traefik network. backend, worker and
|
|
# postgres are intentionally NOT listed here — they stay on the private
|
|
# `default` network.
|
|
dokploy-network:
|
|
external: true
|