525 lines
23 KiB
YAML
525 lines
23 KiB
YAML
name: fquiz 镜像构建与部署
|
|
|
|
on:
|
|
push:
|
|
branches:
|
|
- dev
|
|
workflow_dispatch:
|
|
|
|
concurrency:
|
|
group: fquiz-build-and-deploy
|
|
cancel-in-progress: true
|
|
|
|
permissions:
|
|
contents: read
|
|
packages: write
|
|
|
|
env:
|
|
REGISTRY: ghcr.io
|
|
|
|
jobs:
|
|
build-and-push:
|
|
runs-on: ubuntu-latest
|
|
outputs:
|
|
api_image: ${{ steps.vars.outputs.api_image }}
|
|
web_image: ${{ steps.vars.outputs.web_image }}
|
|
image_tag: ${{ steps.vars.outputs.image_tag }}
|
|
steps:
|
|
- name: 拉取代码
|
|
uses: actions/checkout@v4
|
|
|
|
- name: 生成镜像变量
|
|
id: vars
|
|
shell: bash
|
|
run: |
|
|
OWNER_LC="${GITHUB_REPOSITORY_OWNER,,}"
|
|
echo "api_image=${{ env.REGISTRY }}/${OWNER_LC}/fquiz-api" >> "$GITHUB_OUTPUT"
|
|
echo "web_image=${{ env.REGISTRY }}/${OWNER_LC}/fquiz-web" >> "$GITHUB_OUTPUT"
|
|
echo "image_tag=${GITHUB_SHA}" >> "$GITHUB_OUTPUT"
|
|
|
|
- name: 安装 Buildx
|
|
uses: docker/setup-buildx-action@v3
|
|
|
|
- name: 登录 GHCR
|
|
uses: docker/login-action@v3
|
|
with:
|
|
registry: ${{ env.REGISTRY }}
|
|
username: ${{ github.actor }}
|
|
password: ${{ secrets.GITHUB_TOKEN }}
|
|
|
|
- name: 构建并推送 API 镜像
|
|
uses: docker/build-push-action@v6
|
|
with:
|
|
context: ./api
|
|
file: ./api/Dockerfile
|
|
push: true
|
|
build-args: |
|
|
PIP_INDEX_URL=${{ secrets.PIP_INDEX_URL || vars.PIP_INDEX_URL || 'https://pypi.org/simple' }}
|
|
PIP_DEFAULT_TIMEOUT=${{ vars.PIP_DEFAULT_TIMEOUT || '120' }}
|
|
tags: |
|
|
${{ steps.vars.outputs.api_image }}:${{ steps.vars.outputs.image_tag }}
|
|
${{ steps.vars.outputs.api_image }}:latest
|
|
cache-from: type=gha,scope=fquiz-api
|
|
cache-to: type=gha,mode=max,scope=fquiz-api
|
|
|
|
- name: 构建并推送 Web 镜像
|
|
uses: docker/build-push-action@v6
|
|
with:
|
|
context: ./web
|
|
file: ./web/Dockerfile
|
|
push: true
|
|
build-args: |
|
|
NEXT_PUBLIC_API_BASE_URL=${{ vars.NEXT_PUBLIC_API_BASE_URL || 'http://127.0.0.1:8000' }}
|
|
tags: |
|
|
${{ steps.vars.outputs.web_image }}:${{ steps.vars.outputs.image_tag }}
|
|
${{ steps.vars.outputs.web_image }}:latest
|
|
cache-from: type=gha,scope=fquiz-web
|
|
cache-to: type=gha,mode=max,scope=fquiz-web
|
|
|
|
deploy:
|
|
runs-on: ubuntu-latest
|
|
needs: build-and-push
|
|
if: github.ref == 'refs/heads/dev'
|
|
steps:
|
|
- name: 校验部署参数
|
|
env:
|
|
SERVER_HOST: ${{ secrets.SERVER_HOST || vars.SERVER_HOST }}
|
|
SERVER_USER: ${{ secrets.SERVER_USER || vars.SERVER_USER }}
|
|
SERVER_SSH_KEY: ${{ secrets.SERVER_SSH_KEY }}
|
|
SERVER_PASSWORD: ${{ secrets.SERVER_PASSWORD }}
|
|
run: |
|
|
set -euo pipefail
|
|
[ -n "${SERVER_HOST}" ] || { echo "::error::缺少 SERVER_HOST(请在 Secrets 或 Variables 中配置)"; exit 1; }
|
|
[ -n "${SERVER_USER}" ] || { echo "::error::缺少 SERVER_USER(请在 Secrets 或 Variables 中配置)"; exit 1; }
|
|
if [ -z "${SERVER_SSH_KEY}" ] && [ -z "${SERVER_PASSWORD}" ]; then
|
|
echo "::error::缺少登录凭据:请至少配置 SERVER_SSH_KEY 或 SERVER_PASSWORD"
|
|
exit 1
|
|
fi
|
|
|
|
- name: 通过 SSH 拉取并更新容器
|
|
uses: appleboy/ssh-action@v1.2.0
|
|
env:
|
|
DEPLOY_PATH: ${{ secrets.DEPLOY_PATH || vars.DEPLOY_PATH }}
|
|
API_IMAGE: ${{ needs.build-and-push.outputs.api_image }}
|
|
WEB_IMAGE: ${{ needs.build-and-push.outputs.web_image }}
|
|
IMAGE_TAG: ${{ needs.build-and-push.outputs.image_tag }}
|
|
NEXT_PUBLIC_API_BASE_URL: ${{ vars.NEXT_PUBLIC_API_BASE_URL || 'http://127.0.0.1:8000' }}
|
|
FLOWER_BASIC_AUTH: ${{ secrets.FLOWER_BASIC_AUTH || vars.FLOWER_BASIC_AUTH || 'admin:admin' }}
|
|
GHCR_USERNAME: ${{ github.actor }}
|
|
GHCR_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
with:
|
|
host: ${{ secrets.SERVER_HOST || vars.SERVER_HOST }}
|
|
username: ${{ secrets.SERVER_USER || vars.SERVER_USER }}
|
|
port: ${{ secrets.SERVER_PORT || vars.SERVER_PORT || 22 }}
|
|
timeout: 120s
|
|
command_timeout: 45m
|
|
key: ${{ secrets.SERVER_SSH_KEY }}
|
|
password: ${{ secrets.SERVER_PASSWORD }}
|
|
envs: DEPLOY_PATH,API_IMAGE,WEB_IMAGE,IMAGE_TAG,NEXT_PUBLIC_API_BASE_URL,FLOWER_BASIC_AUTH,GHCR_USERNAME,GHCR_TOKEN
|
|
script: |
|
|
set -euo pipefail
|
|
export DOCKER_CLIENT_TIMEOUT="${DOCKER_CLIENT_TIMEOUT:-600}"
|
|
export COMPOSE_HTTP_TIMEOUT="${COMPOSE_HTTP_TIMEOUT:-600}"
|
|
|
|
DEPLOY_DIR="${DEPLOY_PATH:-/opt/fquiz}"
|
|
mkdir -p "${DEPLOY_DIR}"
|
|
cd "${DEPLOY_DIR}"
|
|
|
|
cat > docker-compose.prod.yml <<'YAML'
|
|
services:
|
|
db:
|
|
image: ${POSTGRES_IMAGE:-docker.m.daocloud.io/pgvector/pgvector:pg16}
|
|
container_name: fquiz-db
|
|
environment:
|
|
POSTGRES_DB: ${POSTGRES_DB:-fquiz}
|
|
POSTGRES_USER: ${POSTGRES_USER:-fquiz}
|
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-fquiz}
|
|
ports:
|
|
- "${POSTGRES_PORT:-5434}:5432"
|
|
volumes:
|
|
- fquiz_db_data:/var/lib/postgresql/data
|
|
healthcheck:
|
|
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-fquiz} -d ${POSTGRES_DB:-fquiz}"]
|
|
interval: 10s
|
|
timeout: 5s
|
|
retries: 8
|
|
start_period: 10s
|
|
restart: unless-stopped
|
|
|
|
redis:
|
|
image: ${REDIS_IMAGE:-docker.m.daocloud.io/library/redis:7-alpine}
|
|
container_name: fquiz-redis
|
|
command: redis-server --appendonly yes
|
|
ports:
|
|
- "${REDIS_PORT:-6379}:6379"
|
|
volumes:
|
|
- fquiz_redis_data:/data
|
|
healthcheck:
|
|
test: ["CMD", "redis-cli", "ping"]
|
|
interval: 10s
|
|
timeout: 3s
|
|
retries: 5
|
|
start_period: 5s
|
|
restart: unless-stopped
|
|
|
|
minio:
|
|
image: ${MINIO_IMAGE:-minio/minio:latest}
|
|
container_name: fquiz-minio
|
|
command: server /data --console-address ":9001"
|
|
environment:
|
|
MINIO_ROOT_USER: ${MINIO_ACCESS_KEY:-minioadmin}
|
|
MINIO_ROOT_PASSWORD: ${MINIO_SECRET_KEY:-minioadmin}
|
|
ports:
|
|
- "${MINIO_API_PORT:-9000}:9000"
|
|
- "${MINIO_CONSOLE_PORT:-9001}:9001"
|
|
volumes:
|
|
- fquiz_minio_data:/data
|
|
restart: unless-stopped
|
|
|
|
minio-init:
|
|
image: ${MINIO_MC_IMAGE:-minio/mc:latest}
|
|
container_name: fquiz-minio-init
|
|
depends_on:
|
|
minio:
|
|
condition: service_started
|
|
environment:
|
|
MINIO_ENDPOINT: ${MINIO_ENDPOINT:-http://minio:9000}
|
|
MINIO_ACCESS_KEY: ${MINIO_ACCESS_KEY:-minioadmin}
|
|
MINIO_SECRET_KEY: ${MINIO_SECRET_KEY:-minioadmin}
|
|
MINIO_BUCKET: ${MINIO_BUCKET:-fquiz-files}
|
|
entrypoint: /bin/sh
|
|
command:
|
|
- -c
|
|
- >
|
|
until mc alias set local "$MINIO_ENDPOINT" "$MINIO_ACCESS_KEY" "$MINIO_SECRET_KEY"; do
|
|
sleep 1;
|
|
done;
|
|
mc mb -p "local/$MINIO_BUCKET" || true;
|
|
restart: "no"
|
|
|
|
api:
|
|
image: ${API_IMAGE}
|
|
container_name: fquiz-api
|
|
depends_on:
|
|
db:
|
|
condition: service_healthy
|
|
minio:
|
|
condition: service_started
|
|
minio-init:
|
|
condition: service_completed_successfully
|
|
environment:
|
|
API_HOST: ${API_HOST:-0.0.0.0}
|
|
API_PORT: ${API_PORT:-8000}
|
|
API_CORS_ORIGINS: ${API_CORS_ORIGINS:-http://localhost:3000,http://127.0.0.1:3000}
|
|
API_CORS_ORIGIN_REGEX: ${API_CORS_ORIGIN_REGEX:-}
|
|
DATABASE_URL: ${DATABASE_URL:-postgresql+psycopg://fquiz:fquiz@db:5432/fquiz}
|
|
DB_HOST: ${DB_HOST:-db}
|
|
DB_PORT: ${DB_PORT:-5432}
|
|
DB_NAME: ${DB_NAME:-postgres}
|
|
DB_SCHEMA: ${DB_SCHEMA:-public}
|
|
DB_USERNAME: ${DB_USERNAME:-fquiz}
|
|
DB_PASSWORD: ${DB_PASSWORD:-fquiz}
|
|
USER_USERNAME_COLUMN: ${USER_USERNAME_COLUMN:-username}
|
|
USER_PASSWORD_COLUMN: ${USER_PASSWORD_COLUMN:-password_hash}
|
|
USER_STATUS_COLUMN: ${USER_STATUS_COLUMN:-status}
|
|
FILE_VFS_ROOT: ${FILE_VFS_ROOT:-./data/vfs}
|
|
MINIO_ENABLED: ${MINIO_ENABLED:-true}
|
|
MINIO_ENDPOINT: ${MINIO_ENDPOINT:-http://minio:9000}
|
|
MINIO_ACCESS_KEY: ${MINIO_ACCESS_KEY:-minioadmin}
|
|
MINIO_SECRET_KEY: ${MINIO_SECRET_KEY:-minioadmin}
|
|
MINIO_BUCKET: ${MINIO_BUCKET:-fquiz-files}
|
|
MINIO_REGION: ${MINIO_REGION:-us-east-1}
|
|
JWT_SECRET_KEY: ${JWT_SECRET_KEY:-change-this-in-production}
|
|
ACCESS_TOKEN_EXPIRE_MINUTES: ${ACCESS_TOKEN_EXPIRE_MINUTES:-480}
|
|
REFRESH_TOKEN_EXPIRE_DAYS: ${REFRESH_TOKEN_EXPIRE_DAYS:-30}
|
|
REFRESH_COOKIE_SECURE: ${REFRESH_COOKIE_SECURE:-false}
|
|
REFRESH_COOKIE_SAMESITE: ${REFRESH_COOKIE_SAMESITE:-lax}
|
|
LLM_PROVIDER_API_KEYS: ${LLM_PROVIDER_API_KEYS:-}
|
|
LLM_REQUEST_TIMEOUT_SECONDS: ${LLM_REQUEST_TIMEOUT_SECONDS:-60}
|
|
CHAT_CONTEXT_MESSAGE_LIMIT: ${CHAT_CONTEXT_MESSAGE_LIMIT:-12}
|
|
CHAT_DEFAULT_SYSTEM_PROMPT: ${CHAT_DEFAULT_SYSTEM_PROMPT:-You are a helpful assistant.}
|
|
CELERY_BROKER_URL: ${CELERY_BROKER_URL:-redis://redis:6379/0}
|
|
CELERY_RESULT_BACKEND: ${CELERY_RESULT_BACKEND:-redis://redis:6379/1}
|
|
CELERY_TIMEZONE: ${CELERY_TIMEZONE:-Asia/Shanghai}
|
|
SCHEDULER_EXPIRE_INTERVAL_SECONDS: ${SCHEDULER_EXPIRE_INTERVAL_SECONDS:-60}
|
|
SCHEDULER_API_BASE_URL: ${SCHEDULER_API_BASE_URL:-http://scheduler:19100}
|
|
SCHEDULER_API_TOKEN: ${SCHEDULER_API_TOKEN:-}
|
|
SCHEDULER_DEFAULT_QUEUE: ${SCHEDULER_DEFAULT_QUEUE:-default}
|
|
WINE_BINARY_PATH: ${WINE_BINARY_PATH:-wine}
|
|
WINE_ALLOWED_ROOT: ${WINE_ALLOWED_ROOT:-./data/wine}
|
|
WINE_DEFAULT_TIMEOUT_SECONDS: ${WINE_DEFAULT_TIMEOUT_SECONDS:-300}
|
|
WINE_MAX_TIMEOUT_SECONDS: ${WINE_MAX_TIMEOUT_SECONDS:-1800}
|
|
INITIAL_ADMIN_EMAIL: ${INITIAL_ADMIN_EMAIL:-admin@example.com}
|
|
INITIAL_ADMIN_USERNAME: ${INITIAL_ADMIN_USERNAME:-admin}
|
|
INITIAL_ADMIN_PASSWORD: ${INITIAL_ADMIN_PASSWORD:-change-me-strong-password}
|
|
ports:
|
|
- "${API_PORT:-8000}:8000"
|
|
healthcheck:
|
|
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/health', timeout=2).read()"]
|
|
interval: 10s
|
|
timeout: 3s
|
|
retries: 5
|
|
start_period: 10s
|
|
restart: unless-stopped
|
|
|
|
celery-worker:
|
|
image: ${API_IMAGE}
|
|
container_name: fquiz-celery-worker
|
|
command:
|
|
- celery
|
|
- -A
|
|
- app.core.celery_app.celery_app
|
|
- worker
|
|
- --loglevel=${CELERY_LOG_LEVEL:-INFO}
|
|
- --concurrency=${CELERY_WORKER_CONCURRENCY:-2}
|
|
- --queues=${CELERY_WORKER_QUEUES:-default,celery}
|
|
depends_on:
|
|
api:
|
|
condition: service_healthy
|
|
redis:
|
|
condition: service_healthy
|
|
environment:
|
|
DATABASE_URL: ${DATABASE_URL:-postgresql+psycopg://fquiz:fquiz@db:5432/fquiz}
|
|
DB_HOST: ${DB_HOST:-db}
|
|
DB_PORT: ${DB_PORT:-5432}
|
|
DB_NAME: ${DB_NAME:-postgres}
|
|
DB_SCHEMA: ${DB_SCHEMA:-public}
|
|
DB_USERNAME: ${DB_USERNAME:-fquiz}
|
|
DB_PASSWORD: ${DB_PASSWORD:-fquiz}
|
|
USER_USERNAME_COLUMN: ${USER_USERNAME_COLUMN:-username}
|
|
USER_PASSWORD_COLUMN: ${USER_PASSWORD_COLUMN:-password_hash}
|
|
USER_STATUS_COLUMN: ${USER_STATUS_COLUMN:-status}
|
|
CELERY_BROKER_URL: ${CELERY_BROKER_URL:-redis://redis:6379/0}
|
|
CELERY_RESULT_BACKEND: ${CELERY_RESULT_BACKEND:-redis://redis:6379/1}
|
|
CELERY_TIMEZONE: ${CELERY_TIMEZONE:-Asia/Shanghai}
|
|
SCHEDULER_EXPIRE_INTERVAL_SECONDS: ${SCHEDULER_EXPIRE_INTERVAL_SECONDS:-60}
|
|
SCHEDULER_API_BASE_URL: ${SCHEDULER_API_BASE_URL:-http://scheduler:19100}
|
|
SCHEDULER_API_TOKEN: ${SCHEDULER_API_TOKEN:-}
|
|
SCHEDULER_DEFAULT_QUEUE: ${SCHEDULER_DEFAULT_QUEUE:-default}
|
|
FLOWER_API_BASE_URL: ${FLOWER_API_BASE_URL:-http://flower:5555}
|
|
FLOWER_API_TIMEOUT_SECONDS: ${FLOWER_API_TIMEOUT_SECONDS:-10}
|
|
FLOWER_BASIC_AUTH: ${FLOWER_BASIC_AUTH:-admin:admin}
|
|
WORKER_REGISTRY_TTL_SECONDS: ${WORKER_REGISTRY_TTL_SECONDS:-90}
|
|
restart: unless-stopped
|
|
|
|
celery-beat:
|
|
image: ${API_IMAGE}
|
|
container_name: fquiz-celery-beat
|
|
command:
|
|
- celery
|
|
- -A
|
|
- app.core.celery_app.celery_app
|
|
- beat
|
|
- --loglevel=${CELERY_LOG_LEVEL:-INFO}
|
|
- --schedule=/tmp/celerybeat-schedule
|
|
depends_on:
|
|
api:
|
|
condition: service_healthy
|
|
redis:
|
|
condition: service_healthy
|
|
environment:
|
|
DATABASE_URL: ${DATABASE_URL:-postgresql+psycopg://fquiz:fquiz@db:5432/fquiz}
|
|
DB_HOST: ${DB_HOST:-db}
|
|
DB_PORT: ${DB_PORT:-5432}
|
|
DB_NAME: ${DB_NAME:-postgres}
|
|
DB_SCHEMA: ${DB_SCHEMA:-public}
|
|
DB_USERNAME: ${DB_USERNAME:-fquiz}
|
|
DB_PASSWORD: ${DB_PASSWORD:-fquiz}
|
|
USER_USERNAME_COLUMN: ${USER_USERNAME_COLUMN:-username}
|
|
USER_PASSWORD_COLUMN: ${USER_PASSWORD_COLUMN:-password_hash}
|
|
USER_STATUS_COLUMN: ${USER_STATUS_COLUMN:-status}
|
|
CELERY_BROKER_URL: ${CELERY_BROKER_URL:-redis://redis:6379/0}
|
|
CELERY_RESULT_BACKEND: ${CELERY_RESULT_BACKEND:-redis://redis:6379/1}
|
|
CELERY_TIMEZONE: ${CELERY_TIMEZONE:-Asia/Shanghai}
|
|
SCHEDULER_EXPIRE_INTERVAL_SECONDS: ${SCHEDULER_EXPIRE_INTERVAL_SECONDS:-60}
|
|
SCHEDULER_API_BASE_URL: ${SCHEDULER_API_BASE_URL:-http://scheduler:19100}
|
|
SCHEDULER_API_TOKEN: ${SCHEDULER_API_TOKEN:-}
|
|
SCHEDULER_DEFAULT_QUEUE: ${SCHEDULER_DEFAULT_QUEUE:-default}
|
|
WORKER_REGISTRY_TTL_SECONDS: ${WORKER_REGISTRY_TTL_SECONDS:-90}
|
|
restart: unless-stopped
|
|
|
|
scheduler:
|
|
image: ${API_IMAGE}
|
|
container_name: fquiz-scheduler
|
|
command:
|
|
- uvicorn
|
|
- app.scheduler_main:app
|
|
- --host
|
|
- 0.0.0.0
|
|
- --port
|
|
- "19100"
|
|
depends_on:
|
|
redis:
|
|
condition: service_healthy
|
|
api:
|
|
condition: service_healthy
|
|
environment:
|
|
CELERY_BROKER_URL: ${CELERY_BROKER_URL:-redis://redis:6379/0}
|
|
CELERY_RESULT_BACKEND: ${CELERY_RESULT_BACKEND:-redis://redis:6379/1}
|
|
CELERY_TIMEZONE: ${CELERY_TIMEZONE:-Asia/Shanghai}
|
|
SCHEDULER_API_TOKEN: ${SCHEDULER_API_TOKEN:-}
|
|
SCHEDULER_DEFAULT_QUEUE: ${SCHEDULER_DEFAULT_QUEUE:-default}
|
|
ports:
|
|
- "${SCHEDULER_PORT:-19100}:19100"
|
|
restart: unless-stopped
|
|
|
|
flower:
|
|
image: ${API_IMAGE}
|
|
container_name: fquiz-flower
|
|
command:
|
|
- celery
|
|
- -A
|
|
- app.core.celery_app.celery_app
|
|
- flower
|
|
- --address=0.0.0.0
|
|
- --port=5555
|
|
- --persistent=False
|
|
- --basic-auth=${FLOWER_BASIC_AUTH:-admin:admin}
|
|
depends_on:
|
|
redis:
|
|
condition: service_healthy
|
|
environment:
|
|
CELERY_BROKER_URL: ${CELERY_BROKER_URL:-redis://redis:6379/0}
|
|
CELERY_RESULT_BACKEND: ${CELERY_RESULT_BACKEND:-redis://redis:6379/1}
|
|
CELERY_TIMEZONE: ${CELERY_TIMEZONE:-Asia/Shanghai}
|
|
FLOWER_BASIC_AUTH: ${FLOWER_BASIC_AUTH:-admin:admin}
|
|
ports:
|
|
- "${FLOWER_PORT:-5555}:5555"
|
|
restart: unless-stopped
|
|
|
|
web:
|
|
image: ${WEB_IMAGE}
|
|
container_name: fquiz-web
|
|
depends_on:
|
|
api:
|
|
condition: service_healthy
|
|
environment:
|
|
NEXT_PUBLIC_API_BASE_URL: ${NEXT_PUBLIC_API_BASE_URL:-http://localhost:8000}
|
|
NODE_ENV: production
|
|
ports:
|
|
- "3000:3000"
|
|
restart: unless-stopped
|
|
|
|
volumes:
|
|
fquiz_db_data:
|
|
fquiz_redis_data:
|
|
fquiz_minio_data:
|
|
YAML
|
|
|
|
if [ ! -f .env ]; then
|
|
cat > .env <<'ENV'
|
|
NEXT_PUBLIC_API_BASE_URL=http://127.0.0.1:8000
|
|
API_HOST=0.0.0.0
|
|
API_PORT=8000
|
|
API_CORS_ORIGINS=http://localhost:3000,http://127.0.0.1:3000
|
|
API_CORS_ORIGIN_REGEX=
|
|
DATABASE_URL=postgresql+psycopg://fquiz:fquiz@db:5432/fquiz
|
|
DB_HOST=db
|
|
DB_PORT=5432
|
|
DB_NAME=postgres
|
|
DB_SCHEMA=public
|
|
DB_USERNAME=fquiz
|
|
DB_PASSWORD=fquiz
|
|
USER_USERNAME_COLUMN=username
|
|
USER_PASSWORD_COLUMN=password_hash
|
|
USER_STATUS_COLUMN=status
|
|
FILE_VFS_ROOT=./data/vfs
|
|
MINIO_ENABLED=true
|
|
MINIO_ENDPOINT=http://minio:9000
|
|
MINIO_ACCESS_KEY=minioadmin
|
|
MINIO_SECRET_KEY=minioadmin
|
|
MINIO_BUCKET=fquiz-files
|
|
MINIO_REGION=us-east-1
|
|
JWT_SECRET_KEY=change-this-in-production
|
|
ACCESS_TOKEN_EXPIRE_MINUTES=480
|
|
REFRESH_TOKEN_EXPIRE_DAYS=30
|
|
REFRESH_COOKIE_SECURE=false
|
|
REFRESH_COOKIE_SAMESITE=lax
|
|
LLM_PROVIDER_API_KEYS=
|
|
LLM_REQUEST_TIMEOUT_SECONDS=60
|
|
CHAT_CONTEXT_MESSAGE_LIMIT=12
|
|
CHAT_DEFAULT_SYSTEM_PROMPT=You are a helpful assistant.
|
|
CELERY_BROKER_URL=redis://redis:6379/0
|
|
CELERY_RESULT_BACKEND=redis://redis:6379/1
|
|
CELERY_TIMEZONE=Asia/Shanghai
|
|
CELERY_LOG_LEVEL=INFO
|
|
CELERY_WORKER_CONCURRENCY=2
|
|
CELERY_WORKER_QUEUES=default,celery
|
|
SCHEDULER_EXPIRE_INTERVAL_SECONDS=60
|
|
SCHEDULER_API_BASE_URL=http://scheduler:19100
|
|
SCHEDULER_API_TOKEN=
|
|
SCHEDULER_DEFAULT_QUEUE=default
|
|
SCHEDULER_PORT=19100
|
|
FLOWER_API_BASE_URL=http://flower:5555
|
|
FLOWER_API_TIMEOUT_SECONDS=10
|
|
FLOWER_BASIC_AUTH=admin:admin
|
|
FLOWER_PORT=5555
|
|
WORKER_REGISTRY_TTL_SECONDS=90
|
|
WINE_BINARY_PATH=wine
|
|
WINE_ALLOWED_ROOT=./data/wine
|
|
WINE_DEFAULT_TIMEOUT_SECONDS=300
|
|
WINE_MAX_TIMEOUT_SECONDS=1800
|
|
INITIAL_ADMIN_EMAIL=admin@example.com
|
|
INITIAL_ADMIN_USERNAME=admin
|
|
INITIAL_ADMIN_PASSWORD=change-me-strong-password
|
|
POSTGRES_DB=fquiz
|
|
POSTGRES_USER=fquiz
|
|
POSTGRES_PASSWORD=fquiz
|
|
POSTGRES_PORT=5434
|
|
POSTGRES_IMAGE=docker.m.daocloud.io/pgvector/pgvector:pg16
|
|
REDIS_IMAGE=docker.m.daocloud.io/library/redis:7-alpine
|
|
REDIS_PORT=6379
|
|
MINIO_IMAGE=minio/minio:latest
|
|
MINIO_MC_IMAGE=minio/mc:latest
|
|
MINIO_API_PORT=9000
|
|
MINIO_CONSOLE_PORT=9001
|
|
ENV
|
|
echo "[warn] .env 不存在,已写入默认模板,请尽快改成生产配置。"
|
|
fi
|
|
|
|
cat > .images.env <<ENV
|
|
API_IMAGE=${API_IMAGE}:${IMAGE_TAG}
|
|
WEB_IMAGE=${WEB_IMAGE}:${IMAGE_TAG}
|
|
NEXT_PUBLIC_API_BASE_URL=${NEXT_PUBLIC_API_BASE_URL}
|
|
ENV
|
|
|
|
echo "${GHCR_TOKEN}" | docker login ghcr.io -u "${GHCR_USERNAME}" --password-stdin
|
|
|
|
COMPOSE_CMD="docker compose"
|
|
if ! docker compose version >/dev/null 2>&1; then
|
|
COMPOSE_CMD="docker-compose"
|
|
fi
|
|
|
|
pull_with_retry() {
|
|
local max_retries=3
|
|
local attempt=1
|
|
while true; do
|
|
if ${COMPOSE_CMD} --env-file .env --env-file .images.env -f docker-compose.prod.yml pull; then
|
|
break
|
|
fi
|
|
if [ "${attempt}" -ge "${max_retries}" ]; then
|
|
echo "[error] docker compose pull failed after ${max_retries} attempts."
|
|
return 1
|
|
fi
|
|
local sleep_seconds=$((attempt * 20))
|
|
echo "[warn] docker compose pull failed (attempt ${attempt}/${max_retries}), retrying in ${sleep_seconds}s..."
|
|
sleep "${sleep_seconds}"
|
|
attempt=$((attempt + 1))
|
|
done
|
|
}
|
|
|
|
pull_with_retry
|
|
if ! ${COMPOSE_CMD} --env-file .env --env-file .images.env -f docker-compose.prod.yml up -d --remove-orphans; then
|
|
echo "[error] docker compose up failed, collecting container diagnostics..."
|
|
${COMPOSE_CMD} --env-file .env --env-file .images.env -f docker-compose.prod.yml ps || true
|
|
docker logs --tail 300 fquiz-api || true
|
|
docker logs --tail 200 fquiz-db || true
|
|
docker logs --tail 200 fquiz-redis || true
|
|
docker logs --tail 200 fquiz-celery-worker || true
|
|
docker logs --tail 200 fquiz-celery-beat || true
|
|
docker logs --tail 200 fquiz-scheduler || true
|
|
docker logs --tail 200 fquiz-flower || true
|
|
exit 1
|
|
fi
|
|
${COMPOSE_CMD} --env-file .env --env-file .images.env -f docker-compose.prod.yml ps
|