refactor: migrate deploy layout and stabilize compose runtime
This commit is contained in:
+138
-226
@@ -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 <<ENV
|
||||
API_IMAGE=${API_IMAGE}:${IMAGE_TAG}
|
||||
WEB_IMAGE=${WEB_IMAGE}:${IMAGE_TAG}
|
||||
NEXT_PUBLIC_API_BASE_URL=${NEXT_PUBLIC_API_BASE_URL}
|
||||
FLOWER_BASIC_AUTH=${FLOWER_BASIC_AUTH}
|
||||
ENV
|
||||
|
||||
echo "${GHCR_TOKEN}" | docker login ghcr.io -u "${GHCR_USERNAME}" --password-stdin
|
||||
@@ -478,7 +390,7 @@ jobs:
|
||||
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
|
||||
if ${COMPOSE_CMD} --env-file deploy/pro-deploy/.env --env-file deploy/pro-deploy/.env.prod --env-file .images.env -f deploy/pro-deploy/compose.yml pull; then
|
||||
break
|
||||
fi
|
||||
if [ "${attempt}" -ge "${max_retries}" ]; then
|
||||
@@ -493,15 +405,15 @@ jobs:
|
||||
}
|
||||
|
||||
pull_with_retry
|
||||
if ! ${COMPOSE_CMD} --env-file .env --env-file .images.env -f docker-compose.prod.yml up -d --remove-orphans; then
|
||||
if ! ${COMPOSE_CMD} --env-file deploy/pro-deploy/.env --env-file deploy/pro-deploy/.env.prod --env-file .images.env -f deploy/pro-deploy/compose.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-flower || true
|
||||
${COMPOSE_CMD} --env-file deploy/pro-deploy/.env --env-file deploy/pro-deploy/.env.prod --env-file .images.env -f deploy/pro-deploy/compose.yml ps || true
|
||||
docker logs --tail 300 fquiz-prod-api || true
|
||||
docker logs --tail 200 fquiz-prod-db || true
|
||||
docker logs --tail 200 fquiz-prod-redis || true
|
||||
docker logs --tail 200 fquiz-prod-celery-worker || true
|
||||
docker logs --tail 200 fquiz-prod-celery-beat || true
|
||||
docker logs --tail 200 fquiz-prod-flower || true
|
||||
exit 1
|
||||
fi
|
||||
${COMPOSE_CMD} --env-file .env --env-file .images.env -f docker-compose.prod.yml ps
|
||||
${COMPOSE_CMD} --env-file deploy/pro-deploy/.env --env-file deploy/pro-deploy/.env.prod --env-file .images.env -f deploy/pro-deploy/compose.yml ps
|
||||
|
||||
+26
@@ -29,3 +29,29 @@ __pycache__/
|
||||
.env.*.local
|
||||
api/.env
|
||||
web/.env.local
|
||||
|
||||
# Deploy runtime data (container-managed)
|
||||
deploy/dev-deploy/data/**
|
||||
!deploy/dev-deploy/data/app/
|
||||
!deploy/dev-deploy/data/app/celery/
|
||||
!deploy/dev-deploy/data/minio/
|
||||
!deploy/dev-deploy/data/postgres/
|
||||
!deploy/dev-deploy/data/redis/
|
||||
!deploy/dev-deploy/data/.gitkeep
|
||||
!deploy/dev-deploy/data/app/.gitkeep
|
||||
!deploy/dev-deploy/data/app/celery/.gitkeep
|
||||
!deploy/dev-deploy/data/minio/.gitkeep
|
||||
!deploy/dev-deploy/data/postgres/.gitkeep
|
||||
!deploy/dev-deploy/data/redis/.gitkeep
|
||||
deploy/pro-deploy/data/**
|
||||
!deploy/pro-deploy/data/app/
|
||||
!deploy/pro-deploy/data/app/celery/
|
||||
!deploy/pro-deploy/data/minio/
|
||||
!deploy/pro-deploy/data/postgres/
|
||||
!deploy/pro-deploy/data/redis/
|
||||
!deploy/pro-deploy/data/.gitkeep
|
||||
!deploy/pro-deploy/data/app/.gitkeep
|
||||
!deploy/pro-deploy/data/app/celery/.gitkeep
|
||||
!deploy/pro-deploy/data/minio/.gitkeep
|
||||
!deploy/pro-deploy/data/postgres/.gitkeep
|
||||
!deploy/pro-deploy/data/redis/.gitkeep
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
- 前端:`web/`,`Next.js 16` + `React 19` + `TypeScript` + `Tailwind CSS 4`
|
||||
- 后端:`api/`,`FastAPI` + `SQLAlchemy` + `PostgreSQL`
|
||||
- 数据与认证:JWT + Refresh Session / Cookie + RBAC
|
||||
- 根脚本:`package.json` 统一调度 `web` / `api` / `docker compose`
|
||||
- 根脚本:`package.json` 统一调度 `web` / `api` / `deploy/dev-deploy` 的 `docker compose`
|
||||
|
||||
## 2. 工作原则
|
||||
|
||||
|
||||
@@ -50,8 +50,8 @@
|
||||
- 宿主机 DB 暴露端口统一走 `POSTGRES_PORT`(默认 `5434`),用于规避与宿主机已有 PostgreSQL(常见 `5432`)冲突;容器内连接仍保持 `db:5432`。
|
||||
- CORS 来源控制采用“双轨配置”:`API_CORS_ORIGINS`(精确列表)+ `API_CORS_ORIGIN_REGEX`(正则,可选);`API_CORS_ORIGINS` 支持 `*` 和通配符域名并在后端转换为 `allow_origin_regex`。
|
||||
- GitHub Actions 使用 `appleboy/ssh-action` 部署时,慢网环境需显式设置 `command_timeout`(建议 `45m`)并为 `docker compose pull` 增加重试,避免出现 `Run Command Timeout` 直接中断发布。
|
||||
- `docker compose up -d` 不会重建 `build` 类型服务镜像;本项目 `web` 无源码挂载且运行 Next.js 生产构建产物,前端代码变更后需执行 `docker compose up --build -d web`(必要时先 `docker compose build --no-cache web`)。
|
||||
- `api` 构建若在拉取 `docker.m.daocloud.io/library/python:3.11-slim` 时出现 manifest `EOF`,优先重试 `docker compose build api`;若持续失败,可在 `.env` 覆盖 `PYTHON_BASE_IMAGE=python:3.11-slim` 走 Docker Hub 兜底。
|
||||
- `docker compose up -d` 不会重建 `build` 类型服务镜像;开发链路默认使用 `deploy/dev-deploy/compose.yml`,前端代码变更后需执行 `docker compose --env-file deploy/dev-deploy/.env -f deploy/dev-deploy/compose.yml up --build -d web`(必要时先 `docker compose --env-file deploy/dev-deploy/.env -f deploy/dev-deploy/compose.yml build --no-cache web`)。
|
||||
- `api` 构建若在拉取 `docker.m.daocloud.io/library/python:3.11-slim` 时出现 manifest `EOF`,优先重试 `docker compose --env-file deploy/dev-deploy/.env -f deploy/dev-deploy/compose.yml build api`;若持续失败,可在 `deploy/dev-deploy/.env` 覆盖 `PYTHON_BASE_IMAGE=python:3.11-slim` 走 Docker Hub 兜底。
|
||||
|
||||
## 前端视觉口径(2026-04-12)
|
||||
|
||||
@@ -272,14 +272,14 @@
|
||||
|
||||
## 发布验收口径(2026-04-26)
|
||||
|
||||
- 发布链路默认执行:
|
||||
- `docker compose build`
|
||||
- `docker compose up -d`
|
||||
- 发布链路默认执行(开发链路):
|
||||
- `docker compose --env-file deploy/dev-deploy/.env -f deploy/dev-deploy/compose.yml build`
|
||||
- `docker compose --env-file deploy/dev-deploy/.env -f deploy/dev-deploy/compose.yml up -d`
|
||||
- 最小运行态验收:
|
||||
- `docker compose ps`(关键服务 `api/web/celery-worker/celery-beat/db/redis/minio` 为 Up,关键依赖健康)。
|
||||
- `docker compose --env-file deploy/dev-deploy/.env -f deploy/dev-deploy/compose.yml ps`(关键服务 `api/web/celery-worker/celery-beat/db/redis/minio` 为 Up,关键依赖健康)。
|
||||
- `curl -fsS http://127.0.0.1:8000/health` 返回 API 健康 JSON。
|
||||
- `curl -I -fsS http://127.0.0.1:3000/` 返回 `HTTP/1.1 200 OK`。
|
||||
- 结合 `docker compose logs --tail` 抽样检查 `api/web/celery-worker/celery-beat` 启动日志是否正常。
|
||||
- 结合 `docker compose --env-file deploy/dev-deploy/.env -f deploy/dev-deploy/compose.yml logs --tail` 抽样检查 `api/web/celery-worker/celery-beat` 启动日志是否正常。
|
||||
|
||||
## 前端组件栈口径(2026-04-22)
|
||||
|
||||
@@ -627,10 +627,10 @@
|
||||
## 发布执行口径(2026-04-25)
|
||||
|
||||
- 本项目本地发布更新容器的标准链路保持为:
|
||||
- `docker compose build`
|
||||
- `docker compose up -d`
|
||||
- `docker compose --env-file deploy/dev-deploy/.env -f deploy/dev-deploy/compose.yml build`
|
||||
- `docker compose --env-file deploy/dev-deploy/.env -f deploy/dev-deploy/compose.yml up -d`
|
||||
- 发布后至少执行以下验收:
|
||||
- `docker compose ps`(容器状态/健康)
|
||||
- `docker compose --env-file deploy/dev-deploy/.env -f deploy/dev-deploy/compose.yml ps`(容器状态/健康)
|
||||
- `curl -fsS http://127.0.0.1:8000/health`(API 健康)
|
||||
- `curl -I -fsS http://127.0.0.1:3000/`(前端可达)
|
||||
- 2026-04-25 二次重发验证通过:按上述链路重跑后,`api/web/db` 均可正常拉起并通过健康检查。
|
||||
@@ -805,7 +805,7 @@
|
||||
|
||||
## 文件管理 MinIO 接入口径(2026-04-25)
|
||||
|
||||
- `docker-compose.yml` 新增 `minio` 与 `minio-init` 服务:
|
||||
- `deploy/dev-deploy/compose.yml` 与 `deploy/pro-deploy/compose.yml` 均包含 `minio` 与 `minio-init` 服务:
|
||||
- `minio` 提供 S3 兼容对象存储(`9000` API,`9001` Console)。
|
||||
- `minio-init` 使用 `minio/mc` 自动创建 `MINIO_BUCKET`,避免首用报 `NoSuchBucket`。
|
||||
- `api` 服务通过环境变量接入 MinIO:
|
||||
|
||||
@@ -86,29 +86,27 @@
|
||||
|
||||
## Docker Compose 部署
|
||||
|
||||
- 开发部署目录:`deploy/dev-deploy`
|
||||
- 生产部署目录:`deploy/pro-deploy`
|
||||
|
||||
1. 准备环境变量:
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
cp deploy/dev-deploy/.env deploy/dev-deploy/.env.local
|
||||
cp deploy/dev-deploy/.env.dev deploy/dev-deploy/.env.dev.local
|
||||
```
|
||||
|
||||
2. 构建并启动容器:
|
||||
|
||||
```bash
|
||||
docker compose up --build -d
|
||||
```
|
||||
|
||||
如需同时启动本地 PostgreSQL 容器(`db`),使用:
|
||||
|
||||
```bash
|
||||
docker compose --profile local-db up --build -d
|
||||
docker compose --env-file deploy/dev-deploy/.env -f deploy/dev-deploy/compose.yml up --build -d
|
||||
```
|
||||
|
||||
3. 查看运行状态和日志:
|
||||
|
||||
```bash
|
||||
docker compose ps
|
||||
docker compose logs -f
|
||||
docker compose --env-file deploy/dev-deploy/.env -f deploy/dev-deploy/compose.yml ps
|
||||
docker compose --env-file deploy/dev-deploy/.env -f deploy/dev-deploy/compose.yml logs -f
|
||||
```
|
||||
|
||||
4. 访问服务:
|
||||
@@ -116,16 +114,16 @@
|
||||
- 前端:`http://localhost:3000`
|
||||
- 后端:`http://localhost:8000/health`
|
||||
- PostgreSQL:默认连接外部库(`DB_HOST/DB_PORT/DB_NAME/DB_SCHEMA/DB_USERNAME/DB_PASSWORD`)
|
||||
- 本地 PostgreSQL(可选):启用 `local-db` profile 后使用 `localhost:5434`(可通过 `POSTGRES_PORT` 覆盖)
|
||||
- 本地 PostgreSQL:`localhost:5434`(可通过 `deploy/dev-deploy/.env` 的 `POSTGRES_PORT` 覆盖)
|
||||
|
||||
5. 停止并清理:
|
||||
|
||||
```bash
|
||||
docker compose down
|
||||
docker compose --env-file deploy/dev-deploy/.env -f deploy/dev-deploy/compose.yml down
|
||||
```
|
||||
|
||||
说明:
|
||||
- `NEXT_PUBLIC_API_BASE_URL` 在 Next.js 中是构建期注入;如果修改该值,需要重新执行 `docker compose up --build`。
|
||||
- `NEXT_PUBLIC_API_BASE_URL` 在 Next.js 中是构建期注入;如果修改该值,需要重新执行 `docker compose --env-file deploy/dev-deploy/.env -f deploy/dev-deploy/compose.yml up --build`。
|
||||
- 若未显式设置 `DATABASE_URL`,API 会使用 `DB_*` 变量自动组装 PostgreSQL 连接,并将 `DB_SCHEMA` 作为 `search_path`(等价 JDBC 的 `currentSchema` 语义)。
|
||||
- 若出现跨域(CORS)错误,请在 `.env` 配置:
|
||||
- `API_CORS_ORIGINS`:精确来源列表(逗号分隔),如 `https://admin.example.com,http://localhost:3000`
|
||||
@@ -133,5 +131,5 @@
|
||||
- 支持在 `API_CORS_ORIGINS` 中使用通配符(如 `https://*.example.com`)或 `*`(仅建议开发调试)
|
||||
- AI 聊天依赖模型路由与 Provider Key:
|
||||
- 路由优先级:`CAPABILITY: chat.default` -> `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`。
|
||||
|
||||
@@ -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=
|
||||
@@ -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
|
||||
@@ -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=
|
||||
@@ -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
|
||||
@@ -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:
|
||||
+74
-25
@@ -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 自动读取。
|
||||
|
||||
+3
-3
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user