# SciLit 生产环境 Docker Compose # # 使用方式: # cp .env.example .env # 填入实际值 # docker compose -f docker-compose.prod.yml --env-file .env up -d # # HTTPS 终止建议使用上游反向代理(Caddy / Nginx / ALB), # 前端 Nginx 容器本身仅提供 HTTP 服务。 version: "3.9" networks: scilit: driver: bridge services: postgres: image: pgvector/pgvector:pg16 networks: - scilit environment: POSTGRES_DB: scilit POSTGRES_USER: scilit POSTGRES_PASSWORD: ${PG_PASSWORD} ports: [] volumes: - pgdata:/var/lib/postgresql/data restart: unless-stopped healthcheck: test: ["CMD-SHELL", "pg_isready -U scilit"] interval: 10s timeout: 5s retries: 5 logging: driver: "json-file" options: max-size: "10m" max-file: "3" redis: image: redis:7-alpine networks: - scilit command: redis-server --requirepass ${REDIS_PASSWORD} ports: [] volumes: - redisdata:/data restart: unless-stopped healthcheck: test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "ping"] interval: 10s timeout: 5s retries: 5 logging: driver: "json-file" options: max-size: "10m" max-file: "3" elasticsearch: image: elasticsearch:8.11.0 networks: - scilit environment: discovery.type: single-node xpack.security.enabled: "false" "ES_JAVA_OPTS": "-Xms1g -Xmx1g" bootstrap.memory_lock: "true" ports: [] volumes: - esdata:/usr/share/elasticsearch/data restart: unless-stopped ulimits: memlock: soft: -1 hard: -1 healthcheck: test: ["CMD-SHELL", "curl -s http://localhost:9200/_cluster/health | grep -qE 'green|yellow'"] interval: 15s timeout: 10s retries: 5 start_period: 30s logging: driver: "json-file" options: max-size: "10m" max-file: "3" minio: image: minio/minio:latest command: server /data --console-address ":9001" networks: - scilit environment: MINIO_ROOT_USER: ${MINIO_ROOT_USER:-minioadmin} MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD:-minioadmin} ports: [] volumes: - miniodata:/data restart: unless-stopped healthcheck: test: ["CMD-SHELL", "curl -sf http://localhost:9000/minio/health/live || exit 1"] interval: 15s timeout: 5s retries: 5 start_period: 10s logging: driver: "json-file" options: max-size: "10m" max-file: "3" migrate: build: ./backend networks: - scilit command: alembic -c alembic/alembic.ini upgrade head environment: DATABASE_URL: postgresql+asyncpg://scilit:${PG_PASSWORD}@postgres:5432/scilit JWT_SECRET: ${JWT_SECRET} SPECIALTY: ${SPECIALTY:-oncology} DEBUG: "false" depends_on: postgres: condition: service_healthy restart: "no" backend: build: ./backend networks: - scilit # ENTRYPOINT 自动执行迁移,CMD 启动 uvicorn(4 workers + proxy-headers) # 无需额外 command environment: DATABASE_URL: postgresql+asyncpg://scilit:${PG_PASSWORD}@postgres:5432/scilit REDIS_URL: redis://default:${REDIS_PASSWORD}@redis:6379 ES_URL: http://elasticsearch:9200 S3_ENDPOINT: ${S3_ENDPOINT} S3_ACCESS_KEY: ${S3_ACCESS_KEY} S3_SECRET_KEY: ${S3_SECRET_KEY} S3_BUCKET: ${S3_BUCKET:-scilit-files} COS_SECRET_ID: ${COS_SECRET_ID:-} COS_SECRET_KEY: ${COS_SECRET_KEY:-} COS_REGION: ${COS_REGION:-ap-guangzhou} COS_BUCKET: ${COS_BUCKET:-scilit-files} JWT_SECRET: ${JWT_SECRET} SPECIALTY: ${SPECIALTY:-oncology} DEBUG: "false" SMTP_HOST: ${SMTP_HOST} SMTP_PORT: ${SMTP_PORT:-587} SMTP_USER: ${SMTP_USER} SMTP_PASSWORD: ${SMTP_PASSWORD} SMTP_FROM: ${SMTP_FROM} SENTRY_DSN: ${SENTRY_DSN} AI_API_KEY: ${AI_API_KEY} AI_BASE_URL: ${AI_BASE_URL} AI_MODEL: ${AI_MODEL} PUBMED_API_KEY: ${PUBMED_API_KEY} CORS_ORIGINS: ${CORS_ORIGINS} PUBLIC_BASE_URL: ${PUBLIC_BASE_URL} ports: - "127.0.0.1:8000:8000" depends_on: postgres: condition: service_healthy redis: condition: service_healthy elasticsearch: condition: service_started minio: condition: service_healthy migrate: condition: service_completed_successfully healthcheck: test: ["CMD-SHELL", "curl -sf http://localhost:8000/health || exit 1"] interval: 15s timeout: 5s retries: 3 start_period: 20s restart: unless-stopped logging: driver: "json-file" options: max-size: "10m" max-file: "3" worker: build: ./backend networks: - scilit # 通过 CMD 覆写默认的 uvicorn 启动,改为 ARQ worker(异步任务消费) # 迁移由独立的 migrate 服务完成(depends_on 确保顺序) command: arq app.tasks.worker.WorkerSettings healthcheck: test: ["CMD-SHELL", "grep -q arq /proc/1/cmdline"] interval: 30s timeout: 5s start_period: 30s retries: 3 environment: DATABASE_URL: postgresql+asyncpg://scilit:${PG_PASSWORD}@postgres:5432/scilit REDIS_URL: redis://default:${REDIS_PASSWORD}@redis:6379 ES_URL: http://elasticsearch:9200 S3_ENDPOINT: ${S3_ENDPOINT} S3_ACCESS_KEY: ${S3_ACCESS_KEY} S3_SECRET_KEY: ${S3_SECRET_KEY} S3_BUCKET: ${S3_BUCKET:-scilit-files} COS_SECRET_ID: ${COS_SECRET_ID:-} COS_SECRET_KEY: ${COS_SECRET_KEY:-} COS_REGION: ${COS_REGION:-ap-guangzhou} COS_BUCKET: ${COS_BUCKET:-scilit-files} JWT_SECRET: ${JWT_SECRET} SPECIALTY: ${SPECIALTY:-oncology} DEBUG: "false" SMTP_HOST: ${SMTP_HOST} SMTP_PORT: ${SMTP_PORT:-587} SMTP_USER: ${SMTP_USER} SMTP_PASSWORD: ${SMTP_PASSWORD} SMTP_FROM: ${SMTP_FROM} AI_API_KEY: ${AI_API_KEY} AI_BASE_URL: ${AI_BASE_URL} AI_MODEL: ${AI_MODEL} PUBMED_API_KEY: ${PUBMED_API_KEY} depends_on: postgres: condition: service_healthy redis: condition: service_healthy minio: condition: service_healthy migrate: condition: service_completed_successfully restart: unless-stopped logging: driver: "json-file" options: max-size: "10m" max-file: "3" frontend: build: context: ./frontend dockerfile: Dockerfile.prod networks: - scilit environment: VITE_API_BASE: /api/v1 ports: - "80:80" volumes: - /var/log/scilit/nginx:/var/log/nginx depends_on: backend: condition: service_started restart: unless-stopped logging: driver: "json-file" options: max-size: "10m" max-file: "3" volumes: pgdata: redisdata: esdata: miniodata: