diff --git a/ai_knowledge_base_v2/.env.example b/ai_knowledge_base_v2/.env.example index 7b1fc9e..9029515 100644 --- a/ai_knowledge_base_v2/.env.example +++ b/ai_knowledge_base_v2/.env.example @@ -10,6 +10,14 @@ FEISHU_SEARCH_URL= # 生产环境必须替换为安全账号密码,不能使用 admin123456 # ============================================ APP_ENV=production +MYSQL_ROOT_PASSWORD=replace-with-strong-root-password +MYSQL_PASSWORD=replace-with-strong-database-password +REDIS_PASSWORD=replace-with-strong-redis-password +DATABASE_URL=mysql+pymysql://ai_kb:URL编码后的数据库密码@mysql:3306/ai_knowledge_base_v2?charset=utf8mb4 +REDIS_URL=redis://:URL编码后的Redis密码@redis:6379/0 +JWT_SECRET_KEY=replace-with-at-least-32-random-characters +# 使用命令生成:python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())" +CONFIG_ENCRYPTION_KEY=replace-with-generated-fernet-key BOOTSTRAP_ADMIN_USERNAME=admin BOOTSTRAP_ADMIN_PASSWORD=replace-with-strong-password BOOTSTRAP_ADMIN_NAME=系统管理员 diff --git a/ai_knowledge_base_v2/apps/admin-web/Dockerfile.prod b/ai_knowledge_base_v2/apps/admin-web/Dockerfile.prod new file mode 100644 index 0000000..eca9a77 --- /dev/null +++ b/ai_knowledge_base_v2/apps/admin-web/Dockerfile.prod @@ -0,0 +1,12 @@ +FROM node:22-alpine AS builder + +WORKDIR /app +COPY package*.json ./ +RUN npm ci +COPY . . +RUN npm run build + +FROM nginx:1.27-alpine +COPY nginx.prod.conf /etc/nginx/conf.d/default.conf +COPY --from=builder /app/dist /usr/share/nginx/html +EXPOSE 80 diff --git a/ai_knowledge_base_v2/apps/admin-web/nginx.prod.conf b/ai_knowledge_base_v2/apps/admin-web/nginx.prod.conf new file mode 100644 index 0000000..1af8a7d --- /dev/null +++ b/ai_knowledge_base_v2/apps/admin-web/nginx.prod.conf @@ -0,0 +1,15 @@ +server { + listen 80; + server_name _; + root /usr/share/nginx/html; + + location / { + try_files $uri $uri/ /index.html; + } + + location ~* \.(?:js|css|png|jpg|jpeg|gif|svg|ico|woff2?)$ { + expires 7d; + add_header Cache-Control "public, immutable"; + try_files $uri =404; + } +} diff --git a/ai_knowledge_base_v2/apps/backend/.env.example b/ai_knowledge_base_v2/apps/backend/.env.example index 221c902..aa9ceb8 100644 --- a/ai_knowledge_base_v2/apps/backend/.env.example +++ b/ai_knowledge_base_v2/apps/backend/.env.example @@ -12,6 +12,7 @@ DB_POOL_RECYCLE=1800 AUTO_CREATE_TABLES=false JWT_SECRET_KEY=change-me-in-production +CONFIG_ENCRYPTION_KEY= JWT_ALGORITHM=HS256 ACCESS_TOKEN_EXPIRE_MINUTES=43200 diff --git a/ai_knowledge_base_v2/apps/user-client/Dockerfile.prod b/ai_knowledge_base_v2/apps/user-client/Dockerfile.prod new file mode 100644 index 0000000..eca9a77 --- /dev/null +++ b/ai_knowledge_base_v2/apps/user-client/Dockerfile.prod @@ -0,0 +1,12 @@ +FROM node:22-alpine AS builder + +WORKDIR /app +COPY package*.json ./ +RUN npm ci +COPY . . +RUN npm run build + +FROM nginx:1.27-alpine +COPY nginx.prod.conf /etc/nginx/conf.d/default.conf +COPY --from=builder /app/dist /usr/share/nginx/html +EXPOSE 80 diff --git a/ai_knowledge_base_v2/apps/user-client/nginx.prod.conf b/ai_knowledge_base_v2/apps/user-client/nginx.prod.conf new file mode 100644 index 0000000..1af8a7d --- /dev/null +++ b/ai_knowledge_base_v2/apps/user-client/nginx.prod.conf @@ -0,0 +1,15 @@ +server { + listen 80; + server_name _; + root /usr/share/nginx/html; + + location / { + try_files $uri $uri/ /index.html; + } + + location ~* \.(?:js|css|png|jpg|jpeg|gif|svg|ico|woff2?)$ { + expires 7d; + add_header Cache-Control "public, immutable"; + try_files $uri =404; + } +} diff --git a/ai_knowledge_base_v2/deploy/gateway.prod.conf b/ai_knowledge_base_v2/deploy/gateway.prod.conf new file mode 100644 index 0000000..4d136d0 --- /dev/null +++ b/ai_knowledge_base_v2/deploy/gateway.prod.conf @@ -0,0 +1,45 @@ +map $http_upgrade $connection_upgrade { + default upgrade; + '' close; +} + +server { + listen 80; + server_name _; + client_max_body_size 20m; + + location = /login { + return 302 /login/; + } + + location ^~ /api/ { + proxy_pass http://backend:8100; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + proxy_buffering off; + proxy_read_timeout 3600s; + } + + location ^~ /login/ { + proxy_pass http://admin-web/; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location / { + proxy_pass http://user-client; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } +} diff --git a/ai_knowledge_base_v2/docker-compose.prod.yml b/ai_knowledge_base_v2/docker-compose.prod.yml new file mode 100644 index 0000000..59a7a9c --- /dev/null +++ b/ai_knowledge_base_v2/docker-compose.prod.yml @@ -0,0 +1,88 @@ +services: + mysql: + image: mysql:8.4 + restart: unless-stopped + environment: + MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:?必须配置 MYSQL_ROOT_PASSWORD} + MYSQL_DATABASE: ai_knowledge_base_v2 + MYSQL_USER: ai_kb + MYSQL_PASSWORD: ${MYSQL_PASSWORD:?必须配置 MYSQL_PASSWORD} + TZ: Asia/Shanghai + command: ["--character-set-server=utf8mb4", "--collation-server=utf8mb4_unicode_ci"] + volumes: + - ai_kb_v2_mysql_data:/var/lib/mysql + healthcheck: + test: ["CMD-SHELL", "mysqladmin ping -h 127.0.0.1 -uroot -p$$MYSQL_ROOT_PASSWORD --silent"] + interval: 10s + timeout: 5s + retries: 20 + + redis: + image: redis:7-alpine + restart: unless-stopped + environment: + REDIS_PASSWORD: ${REDIS_PASSWORD:?必须配置 REDIS_PASSWORD} + command: ["redis-server", "--appendonly", "yes", "--requirepass", "${REDIS_PASSWORD:?必须配置 REDIS_PASSWORD}"] + volumes: + - ai_kb_v2_redis_data:/data + healthcheck: + test: ["CMD-SHELL", "redis-cli -a $$REDIS_PASSWORD ping | grep PONG"] + interval: 10s + timeout: 5s + retries: 20 + + backend: + build: + context: ./apps/backend + restart: unless-stopped + environment: + APP_ENV: production + DEBUG: "false" + DATABASE_URL: ${DATABASE_URL:?必须配置经过 URL 编码的 DATABASE_URL} + REDIS_URL: ${REDIS_URL:?必须配置经过 URL 编码的 REDIS_URL} + BACKEND_WORKERS: ${BACKEND_WORKERS:-4} + DB_POOL_SIZE: ${DB_POOL_SIZE:-20} + DB_MAX_OVERFLOW: ${DB_MAX_OVERFLOW:-20} + JWT_SECRET_KEY: ${JWT_SECRET_KEY:?必须配置 JWT_SECRET_KEY} + CONFIG_ENCRYPTION_KEY: ${CONFIG_ENCRYPTION_KEY:?必须配置 CONFIG_ENCRYPTION_KEY} + MOCK_SMS_ENABLED: "false" + MOCK_RAG_ENABLED: "false" + MOCK_MODEL_ENABLED: "false" + FEISHU_MOCK_ENABLED: "false" + BOOTSTRAP_ADMIN_USERNAME: ${BOOTSTRAP_ADMIN_USERNAME:?必须配置 BOOTSTRAP_ADMIN_USERNAME} + BOOTSTRAP_ADMIN_PASSWORD: ${BOOTSTRAP_ADMIN_PASSWORD:?必须配置 BOOTSTRAP_ADMIN_PASSWORD} + BOOTSTRAP_ADMIN_NAME: ${BOOTSTRAP_ADMIN_NAME:-系统管理员} + AUTO_CREATE_TABLES: "false" + depends_on: + mysql: + condition: service_healthy + redis: + condition: service_healthy + + user-client: + build: + context: ./apps/user-client + dockerfile: Dockerfile.prod + restart: unless-stopped + + admin-web: + build: + context: ./apps/admin-web + dockerfile: Dockerfile.prod + restart: unless-stopped + + gateway: + image: nginx:1.27-alpine + restart: unless-stopped + ports: + - "${GATEWAY_PORT:-8080}:80" + volumes: + - ./deploy/gateway.prod.conf:/etc/nginx/conf.d/default.conf:ro + depends_on: + - backend + - user-client + - admin-web + +volumes: + ai_kb_v2_mysql_data: + ai_kb_v2_redis_data: