# 生产部署:蓝绿双槽 + 常驻代理,支持零停机发布 # # 服务器目录(必须同级): # /opt/fashion/backend ← 本文件所在(后端仓,部署入口) # /opt/fashion/test ← 前端仓(默认按 ../test 取,可用 .env 的 WEB_DIR 覆盖) # # 浏览器 ──▶ proxy(nginx:80,常驻不重建) # ├─ /api ──▶ backend_a 或 backend_b # ├─ / ──▶ web_a 或 web_b # ├─ 18090(容器内) SSR 取数 → 当前活跃后端 # ├─ 127.0.0.1:18091 前端构建期取 SSG # └─ 127.0.0.1:18092 管理后台(SSH 隧道) # # 首次部署:cp .env.example .env → 填密钥 → ./init-db.sh → ./deploy.sh all # 日常发布:./deploy.sh(只更新后端) / ./deploy.sh all / ./deploy.sh --rollback # # a/b 两个槽共用同一份镜像标签(RELEASE=时间戳-提交号,不可变), # deploy.sh 只重建空闲槽,换完 nginx -s reload 即切换。 x-backend-base: &backend-base image: fashion-backend:${RELEASE:-latest} restart: unless-stopped environment: SERVER_PORT: 8090 SSG_PORT: 8091 BACKSTAGE_PORT: 8092 GIN_MODE: release HASHID_SECRET: ${HASHID_SECRET:?HASHID_SECRET 未设置:复制 .env.example 为 .env 并填写} JWT_SECRET: ${JWT_SECRET:?JWT_SECRET 未设置} SSG_TOKEN: ${SSG_TOKEN:-} CORS_ALLOW_ORIGINS: ${CORS_ALLOW_ORIGINS:-https://xisoa.com,https://www.xisoa.com} RATE_LIMIT_ENABLED: ${RATE_LIMIT_ENABLED:-true} RATE_LIMIT_RPS: ${RATE_LIMIT_RPS:-20} RATE_LIMIT_BURST: ${RATE_LIMIT_BURST:-40} DB_HOST: db DB_PORT: 5432 DB_USER: ${DB_USER:-fashion} DB_PASSWORD: ${DB_PASSWORD:?DB_PASSWORD 未设置} DB_NAME: ${DB_NAME:-fashion} INGEST_SECRET: ${INGEST_SECRET:?INGEST_SECRET 未设置} INGEST_TTL: ${INGEST_TTL:-300} INGEST_DOWNLOAD_PROXY: ${INGEST_DOWNLOAD_PROXY:-} INGEST_IMAGE_CONCURRENCY: ${INGEST_IMAGE_CONCURRENCY:-4} S4_ENABLED: ${S4_ENABLED:-true} S4_AK: ${S4_AK:?S4_AK 未设置} S4_SK: ${S4_SK:?S4_SK 未设置} S4_BUCKET: ${S4_BUCKET:-toomstudio} S4_ENDPOINT: ${S4_ENDPOINT:-https://s3.bitiful.net} S4_REGION: ${S4_REGION:-cn-east-1} S4_BASE_URL: ${S4_BASE_URL:-} VISION_ENABLED: ${VISION_ENABLED:-false} VISION_ENDPOINT: ${VISION_ENDPOINT:-http://127.0.0.1:8900} UPLOAD_DIR: /app/uploads volumes: - uploads:/app/uploads depends_on: db: condition: service_healthy healthcheck: test: ["CMD", "wget", "-qO-", "http://127.0.0.1:8090/api/health"] interval: 10s timeout: 5s retries: 6 start_period: 15s x-web-base: &web-base image: fashion-web:${RELEASE:-latest} restart: unless-stopped environment: # 容器内的 localhost 是它自己,SSR 取数统一走代理(永远指向活跃后端槽) BASE_API_SSR: http://proxy:18090 healthcheck: test: ["CMD", "nc", "-z", "127.0.0.1", "3000"] interval: 10s timeout: 3s retries: 5 start_period: 15s services: # ---- 数据库:PostgreSQL 16 + pgvector ---- db: image: pgvector/pgvector:pg16 restart: unless-stopped environment: POSTGRES_USER: ${DB_USER:-fashion} POSTGRES_PASSWORD: ${DB_PASSWORD:?DB_PASSWORD 未设置} POSTGRES_DB: ${DB_NAME:-fashion} TZ: Asia/Shanghai volumes: - db_data:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-fashion} -d ${DB_NAME:-fashion}"] interval: 10s timeout: 5s retries: 10 # ---- 常驻代理:整个发布过程中不重建,切换只靠 reload ---- proxy: image: nginx:1.27-alpine restart: unless-stopped ports: - "${HTTP_PORT:-80}:80" - "127.0.0.1:18091:18091" # 前端构建期取 SSG - "127.0.0.1:18092:18092" # 管理后台(SSH 隧道) volumes: - ./nginx/default.conf:/etc/nginx/conf.d/default.conf:ro # 切换点放在 .deploy/(已被 gitignore),发布时改写它不会弄脏工作区 - ./.deploy/nginx:/etc/nginx/active:ro healthcheck: test: ["CMD", "wget", "-qO-", "http://127.0.0.1/healthz"] interval: 15s timeout: 3s retries: 5 start_period: 5s depends_on: backend_a: condition: service_healthy web_a: condition: service_started # ---- 后端双槽 ---- backend_a: <<: *backend-base build: context: . args: GOPROXY: ${GOPROXY:-https://goproxy.cn,direct} backend_b: <<: *backend-base # ---- 前端双槽(只跑 Astro node,nginx 在 proxy 里)---- web_a: <<: *web-base build: context: ${WEB_DIR:-../test} args: # 走宿主回环的代理 18091,由代理转发到活跃后端槽 BASE_API_SSG: http://127.0.0.1:18091 SSG_TOKEN: ${SSG_TOKEN:-} network: host web_b: <<: *web-base volumes: db_data: uploads: