diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index ff15606..1f6876c 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -122,218 +122,138 @@ jobs: 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' + cat > deploy/pro-deploy/compose.yml <<'YAML' services: db: - image: ${POSTGRES_IMAGE:-docker.m.daocloud.io/pgvector/pgvector:pg16} - container_name: fquiz-db + image: ${POSTGRES_IMAGE} + container_name: ${COMPOSE_PROJECT_NAME}-db environment: - POSTGRES_DB: ${POSTGRES_DB:-fquiz} - POSTGRES_USER: ${POSTGRES_USER:-fquiz} - POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-fquiz} + POSTGRES_DB: ${POSTGRES_DB} + POSTGRES_USER: ${POSTGRES_USER} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} ports: - - "${POSTGRES_PORT:-5434}:5432" + - "${POSTGRES_PORT}: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 + - prod_db_data:/var/lib/postgresql/data restart: unless-stopped redis: - image: ${REDIS_IMAGE:-docker.m.daocloud.io/library/redis:7-alpine} - container_name: fquiz-redis + image: ${REDIS_IMAGE} + container_name: ${COMPOSE_PROJECT_NAME}-redis command: redis-server --appendonly yes ports: - - "${REDIS_PORT:-6379}:6379" + - "${REDIS_PORT}:6379" volumes: - - fquiz_redis_data:/data - healthcheck: - test: ["CMD", "redis-cli", "ping"] - interval: 10s - timeout: 3s - retries: 5 - start_period: 5s + - prod_redis_data:/data restart: unless-stopped minio: - image: ${MINIO_IMAGE:-minio/minio:latest} - container_name: fquiz-minio + image: ${MINIO_IMAGE} + container_name: ${COMPOSE_PROJECT_NAME}-minio command: server /data --console-address ":9001" environment: - MINIO_ROOT_USER: ${MINIO_ACCESS_KEY:-minioadmin} - MINIO_ROOT_PASSWORD: ${MINIO_SECRET_KEY:-minioadmin} + MINIO_ROOT_USER: ${MINIO_ACCESS_KEY} + MINIO_ROOT_PASSWORD: ${MINIO_SECRET_KEY} ports: - - "${MINIO_API_PORT:-9000}:9000" - - "${MINIO_CONSOLE_PORT:-9001}:9001" + - "${MINIO_API_PORT}:9000" + - "${MINIO_CONSOLE_PORT}:9001" volumes: - - fquiz_minio_data:/data + - prod_minio_data:/data restart: unless-stopped minio-init: - image: ${MINIO_MC_IMAGE:-minio/mc:latest} - container_name: fquiz-minio-init + image: ${MINIO_MC_IMAGE} + container_name: ${COMPOSE_PROJECT_NAME}-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} + - minio entrypoint: /bin/sh command: - -c - > - until mc alias set local "$$MINIO_ENDPOINT" "$$MINIO_ACCESS_KEY" "$$MINIO_SECRET_KEY"; do + until mc alias set local "$${MINIO_ENDPOINT}" "$${MINIO_ACCESS_KEY}" "$${MINIO_SECRET_KEY}"; do sleep 1; done; - mc mb -p "local/$$MINIO_BUCKET" || true; + mc mb -p "local/$${MINIO_BUCKET}" || true; + environment: + MINIO_ENDPOINT: ${MINIO_ENDPOINT} + MINIO_ACCESS_KEY: ${MINIO_ACCESS_KEY} + MINIO_SECRET_KEY: ${MINIO_SECRET_KEY} + MINIO_BUCKET: ${MINIO_BUCKET} restart: "no" api: image: ${API_IMAGE} - container_name: fquiz-api + container_name: ${COMPOSE_PROJECT_NAME}-api depends_on: - db: - condition: service_healthy - minio: - condition: service_started - minio-init: - condition: service_completed_successfully + - db + - redis + - minio + - minio-init + env_file: + - ./.env.prod 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} - 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 + API_HOST: 0.0.0.0 + API_PORT: 8000 + DB_HOST: db + DB_PORT: 5432 + MINIO_ENDPOINT: http://minio:9000 + CELERY_BROKER_URL: redis://redis:6379/0 + CELERY_RESULT_BACKEND: redis://redis:6379/1 + expose: + - "8000" restart: unless-stopped celery-worker: image: ${API_IMAGE} - container_name: fquiz-celery-worker + container_name: ${COMPOSE_PROJECT_NAME}-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} + - --loglevel=${CELERY_LOG_LEVEL} + - --concurrency=${CELERY_WORKER_CONCURRENCY} + - --queues=${CELERY_WORKER_QUEUES} depends_on: - api: - condition: service_healthy - redis: - condition: service_healthy + - api + - redis + env_file: + - ./.env.prod 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} - 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} + DB_HOST: db + DB_PORT: 5432 + CELERY_BROKER_URL: redis://redis:6379/0 + CELERY_RESULT_BACKEND: redis://redis:6379/1 + FLOWER_API_BASE_URL: http://flower:5555 restart: unless-stopped celery-beat: image: ${API_IMAGE} - container_name: fquiz-celery-beat + container_name: ${COMPOSE_PROJECT_NAME}-celery-beat command: - celery - -A - app.core.celery_app.celery_app - beat - - --loglevel=${CELERY_LOG_LEVEL:-INFO} + - --loglevel=${CELERY_LOG_LEVEL} - --schedule=/tmp/celerybeat-schedule depends_on: - api: - condition: service_healthy - redis: - condition: service_healthy + - api + - redis + env_file: + - ./.env.prod 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} - WORKER_REGISTRY_TTL_SECONDS: ${WORKER_REGISTRY_TTL_SECONDS:-90} + DB_HOST: db + DB_PORT: 5432 + CELERY_BROKER_URL: redis://redis:6379/0 + CELERY_RESULT_BACKEND: redis://redis:6379/1 restart: unless-stopped flower: image: ${API_IMAGE} - container_name: fquiz-flower - profiles: - - monitoring + container_name: ${COMPOSE_PROJECT_NAME}-flower command: - celery - -A @@ -342,67 +262,86 @@ jobs: - --address=0.0.0.0 - --port=5555 - --persistent=False - - --basic-auth=${FLOWER_BASIC_AUTH:-admin:admin} + - --basic-auth=${FLOWER_BASIC_AUTH} depends_on: - redis: - condition: service_healthy + - redis 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" + CELERY_BROKER_URL: redis://redis:6379/0 + CELERY_RESULT_BACKEND: redis://redis:6379/1 + CELERY_TIMEZONE: ${CELERY_TIMEZONE} + FLOWER_BASIC_AUTH: ${FLOWER_BASIC_AUTH} + expose: + - "5555" restart: unless-stopped web: image: ${WEB_IMAGE} - container_name: fquiz-web + container_name: ${COMPOSE_PROJECT_NAME}-web depends_on: - api: - condition: service_healthy + - api environment: - NEXT_PUBLIC_API_BASE_URL: ${NEXT_PUBLIC_API_BASE_URL:-http://localhost:8000} + NEXT_PUBLIC_API_BASE_URL: ${NEXT_PUBLIC_API_BASE_URL} NODE_ENV: production - ports: - - "${WEB_PORT:-3000}:3000" + expose: + - "3000" restart: unless-stopped volumes: - fquiz_db_data: - fquiz_redis_data: - fquiz_minio_data: + prod_db_data: + prod_redis_data: + prod_minio_data: YAML - if [ ! -f .env ]; then - cat > .env <<'ENV' - NEXT_PUBLIC_API_BASE_URL=http://127.0.0.1:8000 - WEB_PORT=3000 - 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 + if [ ! -f deploy/pro-deploy/.env ]; then + cat > deploy/pro-deploy/.env <<'ENV' + COMPOSE_PROJECT_NAME=fquiz-prod + API_IMAGE=ghcr.io/chengkml/fquiz-api:latest + WEB_IMAGE=ghcr.io/chengkml/fquiz-web:latest + POSTGRES_IMAGE=docker.m.daocloud.io/pgvector/pgvector:pg16 + REDIS_IMAGE=docker.m.daocloud.io/library/redis:7-alpine + MINIO_IMAGE=minio/minio:latest + MINIO_MC_IMAGE=minio/mc:latest + POSTGRES_PORT=5434 + REDIS_PORT=6379 + MINIO_API_PORT=9000 + MINIO_CONSOLE_PORT=9001 + NEXT_PUBLIC_API_BASE_URL=https://quiz.example.com/api + CELERY_LOG_LEVEL=INFO + CELERY_WORKER_CONCURRENCY=4 + CELERY_WORKER_QUEUES=default,celery + CELERY_TIMEZONE=Asia/Shanghai + FLOWER_BASIC_AUTH=admin:change_me + POSTGRES_DB=fquiz + POSTGRES_USER=fquiz + MINIO_ENDPOINT=http://minio:9000 + MINIO_BUCKET=fquiz-files + ENV + echo "[warn] deploy/pro-deploy/.env 不存在,已写入默认模板,请尽快改成生产配置。" + fi + + if [ ! -f deploy/pro-deploy/.env.prod ]; then + cat > deploy/pro-deploy/.env.prod <<'ENV' + DATABASE_URL=postgresql+psycopg://fquiz:replace_strong_password@db:5432/fquiz DB_HOST=db DB_PORT=5432 DB_NAME=postgres DB_SCHEMA=public DB_USERNAME=fquiz - DB_PASSWORD=fquiz + DB_PASSWORD=replace_strong_password 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_ACCESS_KEY=replace_strong_access_key + MINIO_SECRET_KEY=replace_strong_secret_key MINIO_BUCKET=fquiz-files MINIO_REGION=us-east-1 - JWT_SECRET_KEY=change-this-in-production + JWT_SECRET_KEY=replace_strong_jwt_secret ACCESS_TOKEN_EXPIRE_MINUTES=480 REFRESH_TOKEN_EXPIRE_DAYS=30 - REFRESH_COOKIE_SECURE=false + REFRESH_COOKIE_SECURE=true REFRESH_COOKIE_SAMESITE=lax LLM_PROVIDER_API_KEYS= LLM_REQUEST_TIMEOUT_SECONDS=60 @@ -411,60 +350,33 @@ jobs: 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 FLOWER_API_BASE_URL=http://flower:5555 FLOWER_API_TIMEOUT_SECONDS=10 - FLOWER_BASIC_AUTH=admin:admin - FLOWER_PORT=5555 + FLOWER_BASIC_AUTH=admin:change_me WORKER_REGISTRY_TTL_SECONDS=90 + CELERY_WORKER_QUEUES=default,celery 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 + INITIAL_ADMIN_PASSWORD=replace_strong_admin_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 + POSTGRES_PASSWORD=replace_strong_password + API_CORS_ORIGINS=https://quiz.example.com + API_CORS_ORIGIN_REGEX= ENV - echo "[warn] .env 不存在,已写入默认模板,请尽快改成生产配置。" + echo "[warn] deploy/pro-deploy/.env.prod 不存在,已写入默认模板,请尽快改成生产配置。" fi - ensure_web_port_available() { - local target_web_port=3000 - if grep -q '^WEB_PORT=' .env; then - sed -i "s/^WEB_PORT=.*/WEB_PORT=${target_web_port}/" .env - else - echo "WEB_PORT=${target_web_port}" >> .env - fi - - local conflicting_web_containers - conflicting_web_containers="$(docker ps --filter "publish=${target_web_port}" --format '{{.Names}}' | grep -Ev '^fquiz-web$' || true)" - if [ -n "${conflicting_web_containers}" ]; then - echo "[error] WEB 端口 ${target_web_port} 已被其他容器占用,停止部署。" - echo "[error] 冲突容器: ${conflicting_web_containers//$'\n'/, }" - return 1 - fi - } - - ensure_web_port_available - cat > .images.env < `GLOBAL: __global__` - - 在 `.env` 配置 `LLM_PROVIDER_API_KEYS`(示例:`openai=sk-xxx`) -- 默认镜像源已配置为 `docker.m.daocloud.io`,并默认使用 `pgvector` 镜像;如你网络环境可直连 Docker Hub,可在 `.env` 中覆盖 `POSTGRES_IMAGE / PYTHON_BASE_IMAGE / NODE_BASE_IMAGE`。 + - 在 `deploy/dev-deploy/.env.dev` 配置 `LLM_PROVIDER_API_KEYS`(示例:`openai=sk-xxx`) +- 默认镜像源已配置为 `docker.m.daocloud.io`,并默认使用 `pgvector` 镜像;如你网络环境可直连 Docker Hub,可在 `deploy/dev-deploy/.env` 中覆盖 `POSTGRES_IMAGE / PYTHON_BASE_IMAGE / NODE_BASE_IMAGE`。 diff --git a/deploy/dev-deploy/.env.dev b/deploy/dev-deploy/.env.dev new file mode 100644 index 0000000..5dcf59d --- /dev/null +++ b/deploy/dev-deploy/.env.dev @@ -0,0 +1,56 @@ +DATABASE_URL=postgresql+psycopg://fquiz:replace_me@db:5432/fquiz +DB_HOST=db +DB_PORT=5432 +DB_NAME=postgres +DB_SCHEMA=public +DB_USERNAME=fquiz +DB_PASSWORD=replace_me + +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=replace_me +MINIO_SECRET_KEY=replace_me +MINIO_BUCKET=fquiz-files +MINIO_REGION=us-east-1 + +JWT_SECRET_KEY=replace_me +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 +SCHEDULER_EXPIRE_INTERVAL_SECONDS=60 +FLOWER_API_BASE_URL=http://flower:5556 +FLOWER_API_TIMEOUT_SECONDS=10 +FLOWER_BASIC_AUTH=admin:admin +WORKER_REGISTRY_TTL_SECONDS=90 +CELERY_WORKER_QUEUES=default,celery + +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=replace_me + +POSTGRES_DB=fquiz +POSTGRES_USER=fquiz +POSTGRES_PASSWORD=replace_me + +API_CORS_ORIGINS=http://localhost:3000,http://127.0.0.1:3000 +API_CORS_ORIGIN_REGEX= diff --git a/deploy/dev-deploy/compose.yml b/deploy/dev-deploy/compose.yml new file mode 100644 index 0000000..655081b --- /dev/null +++ b/deploy/dev-deploy/compose.yml @@ -0,0 +1,200 @@ +services: + db: + image: ${POSTGRES_IMAGE} + container_name: ${COMPOSE_PROJECT_NAME}-db + environment: + POSTGRES_DB: ${POSTGRES_DB} + POSTGRES_USER: ${POSTGRES_USER} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + PGDATA: /var/lib/postgresql/data/pgdata + ports: + - "${POSTGRES_PORT}:5432" + volumes: + - ./data/postgres:/var/lib/postgresql/data + restart: unless-stopped + + redis: + image: ${REDIS_IMAGE} + container_name: ${COMPOSE_PROJECT_NAME}-redis + ports: + - "${REDIS_PORT}:6379" + volumes: + - ./data/redis:/data + command: redis-server --appendonly yes + restart: unless-stopped + + minio: + image: ${MINIO_IMAGE} + container_name: ${COMPOSE_PROJECT_NAME}-minio + command: server /data --console-address ":9001" + environment: + MINIO_ROOT_USER: ${MINIO_ACCESS_KEY} + MINIO_ROOT_PASSWORD: ${MINIO_SECRET_KEY} + ports: + - "${MINIO_API_PORT}:9000" + - "${MINIO_CONSOLE_PORT}:9001" + volumes: + - ./data/minio:/data + restart: unless-stopped + + minio-init: + image: ${MINIO_MC_IMAGE} + container_name: ${COMPOSE_PROJECT_NAME}-minio-init + depends_on: + - minio + 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; + environment: + MINIO_ENDPOINT: ${MINIO_ENDPOINT} + MINIO_ACCESS_KEY: ${MINIO_ACCESS_KEY} + MINIO_SECRET_KEY: ${MINIO_SECRET_KEY} + MINIO_BUCKET: ${MINIO_BUCKET} + restart: "no" + + api: + build: + context: ../../api + dockerfile: Dockerfile + args: + PYTHON_BASE_IMAGE: ${PYTHON_BASE_IMAGE} + PIP_INDEX_URL: ${PIP_INDEX_URL} + PIP_DEFAULT_TIMEOUT: ${PIP_DEFAULT_TIMEOUT} + PIP_RETRIES: ${PIP_RETRIES} + container_name: ${COMPOSE_PROJECT_NAME}-api + depends_on: + - db + - redis + - minio + - minio-init + env_file: + - ./.env.dev + environment: + API_HOST: 0.0.0.0 + API_PORT: 8000 + DB_HOST: db + DB_PORT: 5432 + MINIO_ENDPOINT: http://minio:9000 + CELERY_BROKER_URL: redis://redis:6379/0 + CELERY_RESULT_BACKEND: redis://redis:6379/1 + ports: + - "${API_PORT}:8000" + volumes: + - ./data/app:/app/data + restart: unless-stopped + + celery-worker: + build: + context: ../../api + dockerfile: Dockerfile + args: + PYTHON_BASE_IMAGE: ${PYTHON_BASE_IMAGE} + PIP_INDEX_URL: ${PIP_INDEX_URL} + PIP_DEFAULT_TIMEOUT: ${PIP_DEFAULT_TIMEOUT} + PIP_RETRIES: ${PIP_RETRIES} + container_name: ${COMPOSE_PROJECT_NAME}-celery-worker + command: + - celery + - -A + - app.core.celery_app.celery_app + - worker + - --loglevel=${CELERY_LOG_LEVEL} + - --concurrency=${CELERY_WORKER_CONCURRENCY} + - --queues=${CELERY_WORKER_QUEUES} + depends_on: + - api + - redis + env_file: + - ./.env.dev + environment: + DB_HOST: db + DB_PORT: 5432 + CELERY_BROKER_URL: redis://redis:6379/0 + CELERY_RESULT_BACKEND: redis://redis:6379/1 + FLOWER_API_BASE_URL: http://flower:5555 + volumes: + - ./data/app:/app/data + restart: unless-stopped + + celery-beat: + build: + context: ../../api + dockerfile: Dockerfile + args: + PYTHON_BASE_IMAGE: ${PYTHON_BASE_IMAGE} + PIP_INDEX_URL: ${PIP_INDEX_URL} + PIP_DEFAULT_TIMEOUT: ${PIP_DEFAULT_TIMEOUT} + PIP_RETRIES: ${PIP_RETRIES} + container_name: ${COMPOSE_PROJECT_NAME}-celery-beat + command: + - celery + - -A + - app.core.celery_app.celery_app + - beat + - --loglevel=${CELERY_LOG_LEVEL} + - --schedule=/app/data/celery/beat-schedule + depends_on: + - api + - redis + env_file: + - ./.env.dev + environment: + DB_HOST: db + DB_PORT: 5432 + CELERY_BROKER_URL: redis://redis:6379/0 + CELERY_RESULT_BACKEND: redis://redis:6379/1 + volumes: + - ./data/app:/app/data + restart: unless-stopped + + flower: + build: + context: ../../api + dockerfile: Dockerfile + args: + PYTHON_BASE_IMAGE: ${PYTHON_BASE_IMAGE} + PIP_INDEX_URL: ${PIP_INDEX_URL} + PIP_DEFAULT_TIMEOUT: ${PIP_DEFAULT_TIMEOUT} + PIP_RETRIES: ${PIP_RETRIES} + container_name: ${COMPOSE_PROJECT_NAME}-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} + depends_on: + - redis + environment: + CELERY_BROKER_URL: redis://redis:6379/0 + CELERY_RESULT_BACKEND: redis://redis:6379/1 + CELERY_TIMEZONE: ${CELERY_TIMEZONE} + FLOWER_BASIC_AUTH: ${FLOWER_BASIC_AUTH} + ports: + - "${FLOWER_PORT}:5555" + restart: unless-stopped + + web: + build: + context: ../../web + dockerfile: Dockerfile + args: + NODE_BASE_IMAGE: ${NODE_BASE_IMAGE} + NEXT_PUBLIC_API_BASE_URL: ${NEXT_PUBLIC_API_BASE_URL} + container_name: ${COMPOSE_PROJECT_NAME}-web + depends_on: + - api + environment: + NEXT_PUBLIC_API_BASE_URL: ${NEXT_PUBLIC_API_BASE_URL} + NODE_ENV: production + ports: + - "3000:3000" + restart: unless-stopped diff --git a/deploy/dev-deploy/data/.gitkeep b/deploy/dev-deploy/data/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/deploy/dev-deploy/data/app/.gitkeep b/deploy/dev-deploy/data/app/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/deploy/dev-deploy/data/app/celery/.gitkeep b/deploy/dev-deploy/data/app/celery/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/deploy/dev-deploy/data/minio/.gitkeep b/deploy/dev-deploy/data/minio/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/deploy/dev-deploy/data/postgres/.gitkeep b/deploy/dev-deploy/data/postgres/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/deploy/dev-deploy/data/redis/.gitkeep b/deploy/dev-deploy/data/redis/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/deploy/pro-deploy/.env.prod b/deploy/pro-deploy/.env.prod new file mode 100644 index 0000000..19b3b61 --- /dev/null +++ b/deploy/pro-deploy/.env.prod @@ -0,0 +1,56 @@ +DATABASE_URL=postgresql+psycopg://fquiz:replace_strong_password@db:5432/fquiz +DB_HOST=db +DB_PORT=5432 +DB_NAME=postgres +DB_SCHEMA=public +DB_USERNAME=fquiz +DB_PASSWORD=replace_strong_password + +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=replace_strong_access_key +MINIO_SECRET_KEY=replace_strong_secret_key +MINIO_BUCKET=fquiz-files +MINIO_REGION=us-east-1 + +JWT_SECRET_KEY=replace_strong_jwt_secret +ACCESS_TOKEN_EXPIRE_MINUTES=480 +REFRESH_TOKEN_EXPIRE_DAYS=30 +REFRESH_COOKIE_SECURE=true +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 +SCHEDULER_EXPIRE_INTERVAL_SECONDS=60 +FLOWER_API_BASE_URL=http://flower:5555 +FLOWER_API_TIMEOUT_SECONDS=10 +FLOWER_BASIC_AUTH=admin:change_me +WORKER_REGISTRY_TTL_SECONDS=90 +CELERY_WORKER_QUEUES=default,celery + +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=replace_strong_admin_password + +POSTGRES_DB=fquiz +POSTGRES_USER=fquiz +POSTGRES_PASSWORD=replace_strong_password + +API_CORS_ORIGINS=https://quiz.example.com +API_CORS_ORIGIN_REGEX= diff --git a/deploy/pro-deploy/compose.yml b/deploy/pro-deploy/compose.yml new file mode 100644 index 0000000..dca1ddf --- /dev/null +++ b/deploy/pro-deploy/compose.yml @@ -0,0 +1,167 @@ +services: + db: + image: ${POSTGRES_IMAGE} + container_name: ${COMPOSE_PROJECT_NAME}-db + environment: + POSTGRES_DB: ${POSTGRES_DB} + POSTGRES_USER: ${POSTGRES_USER} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + PGDATA: /var/lib/postgresql/data/pgdata + ports: + - "${POSTGRES_PORT}:5432" + volumes: + - ./data/postgres:/var/lib/postgresql/data + restart: unless-stopped + + redis: + image: ${REDIS_IMAGE} + container_name: ${COMPOSE_PROJECT_NAME}-redis + command: redis-server --appendonly yes + ports: + - "${REDIS_PORT}:6379" + volumes: + - ./data/redis:/data + restart: unless-stopped + + minio: + image: ${MINIO_IMAGE} + container_name: ${COMPOSE_PROJECT_NAME}-minio + command: server /data --console-address ":9001" + environment: + MINIO_ROOT_USER: ${MINIO_ACCESS_KEY} + MINIO_ROOT_PASSWORD: ${MINIO_SECRET_KEY} + ports: + - "${MINIO_API_PORT}:9000" + - "${MINIO_CONSOLE_PORT}:9001" + volumes: + - ./data/minio:/data + restart: unless-stopped + + minio-init: + image: ${MINIO_MC_IMAGE} + container_name: ${COMPOSE_PROJECT_NAME}-minio-init + depends_on: + - minio + 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; + environment: + MINIO_ENDPOINT: ${MINIO_ENDPOINT} + MINIO_ACCESS_KEY: ${MINIO_ACCESS_KEY} + MINIO_SECRET_KEY: ${MINIO_SECRET_KEY} + MINIO_BUCKET: ${MINIO_BUCKET} + restart: "no" + + api: + image: ${API_IMAGE} + container_name: ${COMPOSE_PROJECT_NAME}-api + depends_on: + - db + - redis + - minio + - minio-init + env_file: + - ./.env.prod + environment: + API_HOST: 0.0.0.0 + API_PORT: 8000 + DB_HOST: db + DB_PORT: 5432 + MINIO_ENDPOINT: http://minio:9000 + CELERY_BROKER_URL: redis://redis:6379/0 + CELERY_RESULT_BACKEND: redis://redis:6379/1 + expose: + - "8000" + volumes: + - ./data/app:/app/data + restart: unless-stopped + + celery-worker: + image: ${API_IMAGE} + container_name: ${COMPOSE_PROJECT_NAME}-celery-worker + command: + - celery + - -A + - app.core.celery_app.celery_app + - worker + - --loglevel=${CELERY_LOG_LEVEL} + - --concurrency=${CELERY_WORKER_CONCURRENCY} + - --queues=${CELERY_WORKER_QUEUES} + depends_on: + - api + - redis + env_file: + - ./.env.prod + environment: + DB_HOST: db + DB_PORT: 5432 + CELERY_BROKER_URL: redis://redis:6379/0 + CELERY_RESULT_BACKEND: redis://redis:6379/1 + FLOWER_API_BASE_URL: http://flower:5555 + volumes: + - ./data/app:/app/data + restart: unless-stopped + + celery-beat: + image: ${API_IMAGE} + container_name: ${COMPOSE_PROJECT_NAME}-celery-beat + command: + - celery + - -A + - app.core.celery_app.celery_app + - beat + - --loglevel=${CELERY_LOG_LEVEL} + - --schedule=/app/data/celery/beat-schedule + depends_on: + - api + - redis + env_file: + - ./.env.prod + environment: + DB_HOST: db + DB_PORT: 5432 + CELERY_BROKER_URL: redis://redis:6379/0 + CELERY_RESULT_BACKEND: redis://redis:6379/1 + volumes: + - ./data/app:/app/data + restart: unless-stopped + + flower: + image: ${API_IMAGE} + container_name: ${COMPOSE_PROJECT_NAME}-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} + depends_on: + - redis + environment: + CELERY_BROKER_URL: redis://redis:6379/0 + CELERY_RESULT_BACKEND: redis://redis:6379/1 + CELERY_TIMEZONE: ${CELERY_TIMEZONE} + FLOWER_BASIC_AUTH: ${FLOWER_BASIC_AUTH} + expose: + - "5555" + restart: unless-stopped + + web: + image: ${WEB_IMAGE} + container_name: ${COMPOSE_PROJECT_NAME}-web + depends_on: + - api + environment: + NEXT_PUBLIC_API_BASE_URL: ${NEXT_PUBLIC_API_BASE_URL} + NODE_ENV: production + expose: + - "3000" + restart: unless-stopped diff --git a/deploy/pro-deploy/data/.gitkeep b/deploy/pro-deploy/data/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/deploy/pro-deploy/data/app/.gitkeep b/deploy/pro-deploy/data/app/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/deploy/pro-deploy/data/app/celery/.gitkeep b/deploy/pro-deploy/data/app/celery/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/deploy/pro-deploy/data/minio/.gitkeep b/deploy/pro-deploy/data/minio/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/deploy/pro-deploy/data/postgres/.gitkeep b/deploy/pro-deploy/data/postgres/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/deploy/pro-deploy/data/redis/.gitkeep b/deploy/pro-deploy/data/redis/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index 6e9b241..0000000 --- a/docker-compose.yml +++ /dev/null @@ -1,287 +0,0 @@ -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: - build: - context: ./api - dockerfile: Dockerfile - args: - PYTHON_BASE_IMAGE: ${PYTHON_BASE_IMAGE:-docker.m.daocloud.io/library/python:3.11-slim} - PIP_INDEX_URL: ${PIP_INDEX_URL:-https://pypi.org/simple} - PIP_DEFAULT_TIMEOUT: ${PIP_DEFAULT_TIMEOUT:-300} - PIP_RETRIES: ${PIP_RETRIES:-20} - 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:-} - 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} - 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: - build: - context: ./api - dockerfile: Dockerfile - args: - PYTHON_BASE_IMAGE: ${PYTHON_BASE_IMAGE:-docker.m.daocloud.io/library/python:3.11-slim} - PIP_INDEX_URL: ${PIP_INDEX_URL:-https://pypi.org/simple} - PIP_DEFAULT_TIMEOUT: ${PIP_DEFAULT_TIMEOUT:-300} - PIP_RETRIES: ${PIP_RETRIES:-20} - 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:-} - 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} - 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:-} - WORKER_REGISTRY_TTL_SECONDS: ${WORKER_REGISTRY_TTL_SECONDS:-90} - restart: unless-stopped - - celery-beat: - build: - context: ./api - dockerfile: Dockerfile - args: - PYTHON_BASE_IMAGE: ${PYTHON_BASE_IMAGE:-docker.m.daocloud.io/library/python:3.11-slim} - PIP_INDEX_URL: ${PIP_INDEX_URL:-https://pypi.org/simple} - PIP_DEFAULT_TIMEOUT: ${PIP_DEFAULT_TIMEOUT:-300} - PIP_RETRIES: ${PIP_RETRIES:-20} - 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:-} - 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} - WORKER_REGISTRY_TTL_SECONDS: ${WORKER_REGISTRY_TTL_SECONDS:-90} - restart: unless-stopped - - flower: - build: - context: ./api - dockerfile: Dockerfile - args: - PYTHON_BASE_IMAGE: ${PYTHON_BASE_IMAGE:-docker.m.daocloud.io/library/python:3.11-slim} - PIP_INDEX_URL: ${PIP_INDEX_URL:-https://pypi.org/simple} - PIP_DEFAULT_TIMEOUT: ${PIP_DEFAULT_TIMEOUT:-300} - PIP_RETRIES: ${PIP_RETRIES:-20} - 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: - build: - context: ./web - dockerfile: Dockerfile - args: - NODE_BASE_IMAGE: ${NODE_BASE_IMAGE:-docker.m.daocloud.io/library/node:22-alpine} - NEXT_PUBLIC_API_BASE_URL: ${NEXT_PUBLIC_API_BASE_URL:-http://localhost:8000} - 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: diff --git a/memory/2026-05-02.md b/memory/2026-05-02.md index 16e6ef7..52d4ebb 100644 --- a/memory/2026-05-02.md +++ b/memory/2026-05-02.md @@ -102,42 +102,91 @@ - 行为变化:不再支持 `dispatchMode=scheduler_api` 与独立 scheduler HTTP 网关调用。 - 保持不变:默认任务链路(API 直连 Celery)与 Flower 监控链路。 -## Work Log - 浏览器标签图标替换为高压电塔(2026-05-02) +## Work Log - 切换部署入口到 deploy 目录并删除根 docker-compose.yml(2026-05-02) - 背景: - - 需求要求将浏览器标签图标替换为工程内 `高压电塔.png`。 + - 用户要求将部署结构统一到 `deploy/dev-deploy` 与 `deploy/pro-deploy`,并删除根目录 `docker-compose.yml`。 -- 本次改动(最小闭环): - - 文件:`web/src/app/favicon.ico` - - 使用仓库根目录 `高压电塔.png`(200x200)重生成为 ICO,并覆盖现有 `favicon.ico`。 - - 页面标题与其他元数据未改动。 +- 本次改动: + - 新增目录结构: + - `deploy/dev-deploy/{compose.yml,.env,.env.dev}` + - `deploy/pro-deploy/{compose.yml,.env,.env.prod}` + - 本地开发入口切换: + - `package.json` 的 `docker:up/down/logs` 改为显式使用 `deploy/dev-deploy/compose.yml` + `deploy/dev-deploy/.env`。 + - `README.md` Docker 部署说明改为基于 `deploy/dev-deploy`。 + - `AGENTS.md` 根脚本说明改为指向 `deploy/dev-deploy`。 + - 部署流水线切换: + - `.github/workflows/main.yml` 改为使用 `deploy/pro-deploy` 结构进行生产部署(不再依赖根 compose)。 + - 长期记忆口径更新: + - `MEMORY.md` 中 Docker 命令口径改为 `deploy/dev-deploy` 方案。 + - 删除: + - 根目录 `docker-compose.yml`。 - 验证: - - `file web/src/app/favicon.ico` - - 结果:`MS Windows icon resource - 1 icon, 200x200 withPNG image data`。 - - `git diff -- web/src/app/favicon.ico` - - 结果:仅该二进制图标文件发生变更。 + - 目录校验:`find deploy -maxdepth 3 -type f | sort` 命中 dev/pro 全套文件。 + - 入口校验:`rg` 检查脚本/文档,已无根 compose 作为默认入口。 - 风险与影响: - - 影响面仅前端浏览器标签图标资源。 - - 可能受浏览器 favicon 缓存影响,首次需强刷后看到新图标。 + - 旧习惯直接执行 `docker compose up -d`(仓库根目录)将失效,必须改为显式 `-f deploy/.../compose.yml`。 + - workflow 远端部署改为写入并使用 `deploy/pro-deploy`,对旧服务器目录结构有一次性迁移要求。 -## Work Log - 修复退出登录闪烁(2026-05-02) +## Work Log - 移除 deploy 中的 nginx 服务(2026-05-02) - 背景: - - 当前退出登录会先把前端登录态置空,再触发页面跳转。 - - 在后台页会先短暂渲染“请先登录”占位,再跳回登录页,造成肉眼可见闪烁。 + - 用户确认原工程不需要 nginx 服务,仅需保留 deploy 双目录与 compose/env 结构。 -- 本次改动(最小闭环): - - 文件:`web/src/components/auth-provider.tsx` - - 调整 `logout` 的 `finally` 收尾顺序: - - 浏览器环境下优先执行 `window.location.replace("/")` 并直接返回; - - 非浏览器环境才执行 `clearAuth()`。 - - 效果:避免在跳转前先渲染未登录中间态页面。 +- 本次改动: + - 删除 `deploy/dev-deploy/compose.yml` 与 `deploy/pro-deploy/compose.yml` 中的 `nginx` 服务定义。 + - 删除 `deploy/dev-deploy/.env` 与 `deploy/pro-deploy/.env` 中 `NGINX_*` 变量。 + - 删除 `deploy/dev-deploy/nginx/` 与 `deploy/pro-deploy/nginx/` 目录。 + - 更新 `.github/workflows/main.yml`,移除 nginx 相关生成、变量与日志采集逻辑。 + +- 验证: + - `docker compose --env-file deploy/dev-deploy/.env -f deploy/dev-deploy/compose.yml config` -> 通过。 + - `docker compose --env-file deploy/pro-deploy/.env -f deploy/pro-deploy/compose.yml config` -> 通过。 - 风险与影响: - - 影响面仅前端退出流程。 - - 行为变化为“直接跳登录页”,不会改变后端登出接口调用逻辑。 + - 部署后不再提供内置反向代理与 HTTPS 终止能力,如需网关需由外部 LB/Nginx/Ingress 承接。 -- 验证建议: - - 已登录状态下从任意后台页面点击“退出登录”,预期直接到登录页,不再出现“请先登录”闪屏。 +## Work Log - deploy 目录统一托管组件配置与数据挂载(2026-05-02) + +- 背景: + - 用户目标是将各组件配置与数据文件集中挂载到 `deploy` 目录,便于统一管理;`nginx` 仅为示例并非必需。 + +- 本次改动: + - `deploy/dev-deploy/compose.yml` 与 `deploy/pro-deploy/compose.yml` 改为目录挂载: + - DB:`./data/postgres -> /var/lib/postgresql/data` + - Redis:`./data/redis -> /data` + - MinIO:`./data/minio -> /data` + - API/Worker/Beat:`./data/app -> /app/data` + - Celery Beat 调度文件持久化: + - `--schedule=/app/data/celery/beat-schedule` + - 删除命名卷定义,改为显式 bind mount。 + - 新增目录骨架(含 `.gitkeep`): + - `deploy/dev-deploy/data/{postgres,redis,minio,app/celery}` + - `deploy/pro-deploy/data/{postgres,redis,minio,app/celery}` + +- 验证: + - `docker compose --env-file deploy/dev-deploy/.env -f deploy/dev-deploy/compose.yml config` -> 通过。 + - `docker compose --env-file deploy/pro-deploy/.env -f deploy/pro-deploy/compose.yml config` -> 通过。 + +- 风险与影响: + - 宿主机目录权限需允许容器读写(尤其 PostgreSQL/Redis/MinIO)。 + - 生产环境若采用只读部署目录,需单独放开 `deploy/*/data/**` 写权限。 + + +## Work Log - dev-deploy 环境注入文件更名(2026-05-02) + +- 背景: + - `dev-deploy` 使用 `.env.prod` 命名语义不清晰。 + +- 本次改动: + - 将 `deploy/dev-deploy/.env.prod` 重命名为 `deploy/dev-deploy/.env.dev`。 + - 将 `deploy/dev-deploy/compose.yml` 中 `env_file` 引用同步改为 `.env.dev`。 + - 将 `README.md` 中相关命令与说明同步改为 `.env.dev`。 + +- 验证: + - `docker compose --env-file deploy/dev-deploy/.env -f deploy/dev-deploy/compose.yml config` -> 通过。 + +- 风险与影响: + - 本地若仍保留旧文件名 `.env.prod`,将不再被 dev compose 自动读取。 diff --git a/package.json b/package.json index ebbcbeb..e389b86 100644 --- a/package.json +++ b/package.json @@ -11,8 +11,8 @@ "build:web": "npm --workspace web run build", "lint:web": "npm --workspace web run lint", "dev:api": "python3 -m uvicorn api.app.main:app --reload --host 0.0.0.0 --port 8000", - "docker:up": "docker compose up --build -d", - "docker:down": "docker compose down", - "docker:logs": "docker compose logs -f" + "docker:up": "docker compose --env-file deploy/dev-deploy/.env -f deploy/dev-deploy/compose.yml up --build -d", + "docker:down": "docker compose --env-file deploy/dev-deploy/.env -f deploy/dev-deploy/compose.yml down", + "docker:logs": "docker compose --env-file deploy/dev-deploy/.env -f deploy/dev-deploy/compose.yml logs -f" } }