diff --git a/.env.production b/.env.production new file mode 100644 index 0000000..6fcee67 --- /dev/null +++ b/.env.production @@ -0,0 +1,24 @@ +# OmniVerse Production Environment Configuration + +# Application +NODE_ENV=production + +# News Crawler Service +PORT=3001 +API_KEY=6b212515bedb521bc700e125a8d43ae53907a5160296abdab2064aaa3ed86526 +ENABLE_SCHEDULER=true +CRAWL_INTERVAL_MINUTES=30 +MAX_NEWS_AGE_DAYS=7 + +# Database +DATABASE_PATH=./data/news.db + +# Logging +LOG_LEVEL=info +LOG_FILE=./logs/app.log + +# Cache +CACHE_TTL_SECONDS=300 + +# CORS (允许的前端域名) +CORS_ORIGIN=https://www.omniverseai.net,https://omniverseai.net,http://localhost:3000 diff --git a/.env.production.example b/.env.production.example new file mode 100644 index 0000000..714199a --- /dev/null +++ b/.env.production.example @@ -0,0 +1,24 @@ +# OmniVerse Production Environment Configuration + +# Application +NODE_ENV=production + +# News Crawler Service +PORT=3001 +API_KEY=your-secure-api-key-here-change-this +ENABLE_SCHEDULER=true +CRAWL_INTERVAL_MINUTES=30 +MAX_NEWS_AGE_DAYS=7 + +# Database +DATABASE_PATH=./data/news.db + +# Logging +LOG_LEVEL=info +LOG_FILE=./logs/app.log + +# Cache +CACHE_TTL_SECONDS=300 + +# CORS (允许的前端域名) +CORS_ORIGIN=https://www.omniverseai.net,https://omniverseai.net,http://localhost:3000 diff --git a/.github/workflows/deploy-production.yml b/.github/workflows/deploy-production.yml new file mode 100644 index 0000000..3699fb3 --- /dev/null +++ b/.github/workflows/deploy-production.yml @@ -0,0 +1,96 @@ +name: Deploy Website to Production + +on: + push: + branches: + - main + paths-ignore: + - 'README.md' + - 'docs/**' + - '.gitignore' + workflow_dispatch: + +env: + HARBOR_REGISTRY: harbor.omniverseai.net + HARBOR_PROJECT: omniverse + IMAGE_NAME: website + KUBECONFIG: /etc/rancher/k3s/k3s.yaml + +jobs: + build-and-deploy: + runs-on: [self-hosted, Linux, X64] + + steps: + # ======================================== + # 1. Checkout Code + # ======================================== + - name: Checkout code + uses: actions/checkout@v4 + + # ======================================== + # 2. Set up Docker Buildx + # ======================================== + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + # ======================================== + # 3. Login to Harbor + # ======================================== + - name: Login to Harbor + run: | + echo "${{ secrets.HARBOR_PASSWORD }}" | docker login ${{ env.HARBOR_REGISTRY }} -u ${{ secrets.HARBOR_USERNAME }} --password-stdin + + # ======================================== + # 4. Build and Push Docker Image + # ======================================== + - name: Build and push Docker image + run: | + IMAGE_TAG="${{ env.HARBOR_REGISTRY }}/${{ env.HARBOR_PROJECT }}/${{ env.IMAGE_NAME }}:latest" + IMAGE_SHA="${{ env.HARBOR_REGISTRY }}/${{ env.HARBOR_PROJECT }}/${{ env.IMAGE_NAME }}:${{ github.sha }}" + + docker build -f Dockerfile.prod -t $IMAGE_TAG -t $IMAGE_SHA . + docker push $IMAGE_TAG + docker push $IMAGE_SHA + + echo "Built and pushed: $IMAGE_TAG" + echo "Built and pushed: $IMAGE_SHA" + + # ======================================== + # 5. Deploy to k3s + # ======================================== + - name: Deploy to k3s + env: + KUBECONFIG: ${{ env.KUBECONFIG }} + run: | + # Apply Kubernetes manifests + kubectl apply -f k8s/production.yaml + + # Force pull latest image and restart + kubectl set image deployment/website website=${{ env.HARBOR_REGISTRY }}/${{ env.HARBOR_PROJECT }}/${{ env.IMAGE_NAME }}:${{ github.sha }} -n prod + kubectl rollout restart deployment/website -n prod + + # Wait for rollout to complete + kubectl rollout status deployment/website -n prod --timeout=300s + + # ======================================== + # 6. Verify Deployment + # ======================================== + - name: Verify deployment + run: | + echo "===================================" + echo "Website Deployment Summary" + echo "===================================" + echo "" + echo "Image: ${{ env.HARBOR_REGISTRY }}/${{ env.HARBOR_PROJECT }}/${{ env.IMAGE_NAME }}:latest" + echo "Commit: ${{ github.sha }}" + echo "Namespace: prod" + echo "" + echo "Pods:" + kubectl get pods -n prod -l app=website + echo "" + echo "Service:" + kubectl get svc -n prod website-svc + echo "" + echo "Ingress:" + kubectl get ingress -n prod website-ingress + echo "===================================" diff --git a/CLOUDFLARE_FIX.md b/CLOUDFLARE_FIX.md new file mode 100644 index 0000000..2382c57 --- /dev/null +++ b/CLOUDFLARE_FIX.md @@ -0,0 +1,95 @@ +# 网站空白页面问题修复指南 + +## 问题诊断 + +您的网站已成功部署在服务器上,但通过 https://www.omniverseai.net 访问时看到空白页面。 + +经过检查: +- ✅ 服务器正常运行 +- ✅ HTML 文件正确返回 +- ✅ JS/CSS 资源文件可以正常访问 +- ⚠️ **问题原因:Cloudflare CDN 缓存** + +## 解决方案 + +### 方案一:清除 Cloudflare 缓存(推荐) + +1. 登录 Cloudflare 控制台:https://dash.cloudflare.com +2. 选择域名 `omniverseai.net` +3. 进入 **"缓存" (Caching)** 菜单 +4. 点击 **"清除所有文件" (Purge Everything)** +5. 确认清除 +6. 等待 30 秒后,刷新浏览器(按 Ctrl+Shift+R 强制刷新) + +### 方案二:临时禁用 Cloudflare 代理 + +如果清除缓存后仍有问题,可以临时禁用 Cloudflare: + +1. 登录 Cloudflare 控制台 +2. 进入 **DNS 设置** +3. 找到 `www` 和 `@` 记录 +4. 点击橙色云图标,将其变为灰色(DNS Only 模式) +5. 等待 DNS 传播(约 5-10 分钟) +6. 刷新浏览器测试 + +### 方案三:配置 Cloudflare 缓存规则 + +为了避免未来出现类似问题: + +1. 进入 Cloudflare 的 **"规则" (Rules)** → **"页面规则" (Page Rules)** +2. 创建新规则: + - URL: `www.omniverseai.net/*` + - 设置: + - 缓存级别:Standard + - Browser Cache TTL: 4 hours + - Always Online: ON +3. 保存规则 + +## 验证步骤 + +1. 打开浏览器开发者工具(F12) +2. 切换到 "Console" 标签 +3. 访问 https://www.omniverseai.net +4. 检查是否有错误信息 +5. 切换到 "Network" 标签 +6. 确认 JS 和 CSS 文件是否成功加载(状态码 200) + +## 本地测试(绕过 Cloudflare) + +如果需要直接测试服务器(绕过 Cloudflare): + +```bash +# 在您的本地计算机添加 hosts 文件 +# Windows: C:\Windows\System32\drivers\etc\hosts +# Mac/Linux: /etc/hosts + +154.38.168.199 www.omniverseai.net +``` + +然后访问 http://www.omniverseai.net(注意是 HTTP) + +## 检查 Cloudflare SSL 设置 + +确保 Cloudflare SSL/TLS 设置正确: + +1. 进入 **SSL/TLS** 设置 +2. 加密模式应设置为: + - **Flexible**(如果服务器只有 HTTP) + - **Full**(如果服务器有自签名证书) + - **Full (strict)**(如果服务器有有效的 SSL 证书) + +**当前服务器配置**:仅 HTTP,因此建议使用 **Flexible** 模式 + +## 技术细节 + +- 服务器 IP: 154.38.168.199 +- HTTP 端口: 80 +- HTTPS 端口: 443(通过 Cloudflare 代理) +- 容器状态: 所有服务正常运行 + +## 需要帮助? + +如果以上方法都不能解决问题,请提供: +1. 浏览器控制台的错误截图 +2. Network 标签的资源加载状态 +3. 当前的 Cloudflare SSL/TLS 设置模式 diff --git a/Dockerfile b/Dockerfile index 5f37562..910f751 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,64 +1,41 @@ -# Node.js 应用 Dockerfile - 自动优化版本 -# 由 docker-builder 生成 +# OmniVerse Web Application Dockerfile +# Production build for US server -# 构建阶段 +# Build stage FROM node:18-alpine AS builder -# 清除代理配置,避免网络冲突 -ENV HTTP_PROXY="" -ENV HTTPS_PROXY="" -ENV http_proxy="" -ENV https_proxy="" - WORKDIR /app -# 使用国内镜像源加速 -RUN npm config delete proxy && \ - npm config delete https-proxy && \ - npm config set registry https://registry.npmmirror.com && \ - npm config set cache /tmp/.npm --global - -# 复制 package 文件 +# Copy package files COPY package*.json ./ -# 安装依赖 +# Install dependencies RUN npm install --frozen-lockfile -# 复制源代码 +# Copy source code COPY . . # Build the application -RUN npm run build -- --base=/ +RUN npm run build -# 生产阶段 +# Production stage FROM node:18-alpine AS production -# 清除代理配置 -ENV HTTP_PROXY="" -ENV HTTPS_PROXY="" -ENV http_proxy="" -ENV https_proxy="" - WORKDIR /app -# 配置 npm 使用国内镜像源 -RUN npm config delete proxy && \ - npm config delete https-proxy && \ - npm config set registry https://registry.npmmirror.com - -# 只安装运行时工具 +# Install serve for static file serving RUN npm install -g serve -# 复制构建产物 +# Copy build artifacts COPY --from=builder /app/dist ./dist COPY --from=builder /app/package.json ./ -# 暴露端口 +# Expose port EXPOSE 3000 -# 健康检查 +# Health check HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \ CMD wget --no-verbose --tries=1 --spider http://localhost:3000 || exit 1 -# 启动应用 +# Start application CMD ["serve", "-s", "dist", "-l", "3000"] diff --git a/Dockerfile.prod b/Dockerfile.prod new file mode 100644 index 0000000..a3cbb97 --- /dev/null +++ b/Dockerfile.prod @@ -0,0 +1,40 @@ +# ============================================================================ +# OmniVerse Website Production Dockerfile +# Multi-stage build for optimized image size +# ============================================================================ + +# Stage 1: Build +FROM node:18-alpine AS builder + +WORKDIR /app + +# Copy package files +COPY package*.json ./ + +# Install dependencies +RUN npm ci --only=production --ignore-scripts || npm install + +# Copy source code +COPY . . + +# Build application +RUN npm run build + +# Stage 2: Production +FROM nginx:1.25-alpine + +# Copy built files +COPY --from=builder /app/dist /usr/share/nginx/html + +# Copy nginx configuration +COPY nginx.conf /etc/nginx/conf.d/default.conf + +# Expose port +EXPOSE 80 + +# Health check +HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \ + CMD wget --no-verbose --tries=1 --spider http://localhost/health || exit 1 + +# Start nginx +CMD ["nginx", "-g", "daemon off;"] diff --git a/PRODUCTION_DEPLOY.md b/PRODUCTION_DEPLOY.md new file mode 100644 index 0000000..14002ef --- /dev/null +++ b/PRODUCTION_DEPLOY.md @@ -0,0 +1,298 @@ +# OmniVerse 官网生产环境部署指南 + +## 部署架构 + +``` +Internet (80/443) + ↓ +Nginx Reverse Proxy (Docker) + ↓ + ├─→ Frontend App (Port 3000, Docker) + └─→ News Crawler API (Port 3001, Docker) +``` + +## 服务组件 + +1. **Nginx** - 反向代理和SSL终止 (80/443端口) +2. **Web Frontend** - React应用 (内部3000端口) +3. **News Crawler** - 新闻爬虫服务 (内部3001端口) +4. **Certbot** - SSL证书自动续期 + +## 部署前准备 + +### 1. 检查域名DNS解析 + +确保域名已正确解析到服务器: +```bash +nslookup www.omniverseai.net +nslookup omniverseai.net +``` + +### 2. 检查必要端口 + +确保以下端口可用: +```bash +# 检查端口占用 +netstat -tlnp | grep -E ':(80|443) ' + +# 如有其他服务占用,需要先停止 +``` + +### 3. 安装Docker和Docker Compose + +```bash +# 检查Docker版本 +docker --version +docker-compose --version + +# 如未安装,请先安装Docker +``` + +### 4. 配置环境变量 + +编辑 `.env.production` 文件,修改必要的配置: +```bash +nano .env.production +``` + +重要配置项: +- `API_KEY`: 修改为强密码 +- `CORS_ORIGIN`: 确认域名正确 + +## 部署步骤 + +### 第一步:初始部署(HTTP) + +首次部署使用HTTP模式,用于SSL证书申请: + +```bash +# 运行部署脚本 +./deploy-prod.sh +``` + +部署脚本会: +1. 停止旧容器 +2. 构建Docker镜像 +3. 启动所有服务 +4. 显示服务状态 + +### 第二步:验证HTTP访问 + +访问 http://www.omniverseai.net 确认网站可以正常访问。 + +```bash +# 检查服务状态 +docker-compose -f docker-compose.prod.yml ps + +# 查看日志 +docker-compose -f docker-compose.prod.yml logs -f web +docker-compose -f docker-compose.prod.yml logs -f news-crawler +docker-compose -f docker-compose.prod.yml logs -f nginx +``` + +### 第三步:申请SSL证书 + +确认HTTP访问正常后,运行SSL初始化脚本: + +```bash +# 申请证书(首次) +./init-ssl.sh +``` + +### 第四步:启用HTTPS + +证书申请成功后,需要更新Nginx配置: + +1. 编辑 `nginx/default.conf`: +```bash +nano nginx/default.conf +``` + +2. 进行以下修改: + - 在HTTP服务器块中,注释掉临时的 `proxy_pass` 配置 + - 取消注释 HTTP到HTTPS重定向配置 + - 取消注释整个HTTPS服务器配置块 + +3. 重启Nginx: +```bash +docker-compose -f docker-compose.prod.yml restart nginx +``` + +4. 验证HTTPS访问: + 访问 https://www.omniverseai.net + +## 管理命令 + +### 查看服务状态 +```bash +docker-compose -f docker-compose.prod.yml ps +``` + +### 查看日志 +```bash +# 查看所有服务日志 +docker-compose -f docker-compose.prod.yml logs -f + +# 查看特定服务日志 +docker-compose -f docker-compose.prod.yml logs -f web +docker-compose -f docker-compose.prod.yml logs -f nginx +docker-compose -f docker-compose.prod.yml logs -f news-crawler +``` + +### 重启服务 +```bash +# 重启所有服务 +docker-compose -f docker-compose.prod.yml restart + +# 重启特定服务 +docker-compose -f docker-compose.prod.yml restart web +docker-compose -f docker-compose.prod.yml restart nginx +``` + +### 停止服务 +```bash +docker-compose -f docker-compose.prod.yml down +``` + +### 更新部署 +```bash +# 拉取最新代码 +git pull + +# 重新部署 +./deploy-prod.sh +``` + +### SSL证书续期 + +Certbot容器会自动续期证书(每12小时检查一次)。也可以手动续期: + +```bash +docker-compose -f docker-compose.prod.yml run --rm certbot renew +docker-compose -f docker-compose.prod.yml restart nginx +``` + +## 监控和维护 + +### 健康检查 +```bash +# Web服务健康检查 +curl http://localhost:3000 + +# News爬虫服务健康检查 +curl http://localhost:3001/health + +# Nginx状态 +curl -I http://www.omniverseai.net +``` + +### 数据备份 + +新闻数据库和日志存储在Docker volumes中: +```bash +# 查看volumes +docker volume ls | grep omniverse + +# 备份新闻数据库 +docker run --rm -v website_news_data:/data -v $(pwd):/backup alpine tar czf /backup/news_data_backup.tar.gz -C /data . + +# 备份日志 +docker run --rm -v website_news_logs:/logs -v $(pwd):/backup alpine tar czf /backup/news_logs_backup.tar.gz -C /logs . +``` + +### 清理旧数据 + +新闻爬虫服务会自动清理7天前的旧新闻(可在 `.env.production` 中配置)。 + +## 故障排查 + +### 网站无法访问 + +1. 检查服务是否运行: +```bash +docker-compose -f docker-compose.prod.yml ps +``` + +2. 检查Nginx日志: +```bash +docker-compose -f docker-compose.prod.yml logs nginx +``` + +3. 检查端口是否监听: +```bash +netstat -tlnp | grep -E ':(80|443) ' +``` + +### SSL证书问题 + +1. 检查证书文件: +```bash +ls -la ./certbot/conf/live/www.omniverseai.net/ +``` + +2. 测试证书: +```bash +openssl s_client -connect www.omniverseai.net:443 -servername www.omniverseai.net +``` + +3. 手动续期: +```bash +docker-compose -f docker-compose.prod.yml run --rm certbot renew --dry-run +``` + +### 新闻服务问题 + +1. 检查服务日志: +```bash +docker-compose -f docker-compose.prod.yml logs news-crawler +``` + +2. 手动触发爬虫: +```bash +curl -X POST http://localhost:3001/api/crawl \ + -H "X-API-Key: your-api-key" +``` + +3. 检查数据库: +```bash +docker exec -it omniverse-news-crawler ls -la /app/data +``` + +## 性能优化 + +### Nginx缓存 + +可以在 `nginx/default.conf` 中添加缓存配置: +```nginx +proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=my_cache:10m max_size=1g inactive=60m; +proxy_cache my_cache; +``` + +### 资源限制 + +可以在 `docker-compose.prod.yml` 中添加资源限制: +```yaml +services: + web: + deploy: + resources: + limits: + cpus: '1' + memory: 512M +``` + +## 安全建议 + +1. 定期更新Docker镜像 +2. 使用强密码作为API_KEY +3. 启用防火墙,只开放80和443端口 +4. 定期备份数据 +5. 监控日志中的异常访问 +6. 配置fail2ban防止暴力攻击 + +## 联系支持 + +如遇到问题,请查看: +- 应用日志:`docker-compose logs` +- Nginx日志:`/var/log/nginx/`(在容器内) +- 新闻服务日志:`./news-crawler-service/logs/` diff --git a/deploy-prod.sh b/deploy-prod.sh new file mode 100755 index 0000000..f654e55 --- /dev/null +++ b/deploy-prod.sh @@ -0,0 +1,78 @@ +#!/bin/bash + +# 生产环境部署脚本 +# 用于构建和启动OmniVerse官网 + +set -e + +echo "==========================================" +echo " OmniVerse 官网生产环境部署" +echo "==========================================" +echo "" + +# 检查必要文件 +if [ ! -f ".env.production" ]; then + echo "错误:找不到 .env.production 文件" + echo "请复制 .env.production.example 并配置环境变量" + exit 1 +fi + +# 停止旧容器 +echo "停止旧容器..." +docker-compose -f docker-compose.prod.yml down + +# 清理旧镜像(可选) +read -p "是否清理旧的Docker镜像?(y/N) " -n 1 -r +echo +if [[ $REPLY =~ ^[Yy]$ ]]; then + echo "清理旧镜像..." + docker system prune -f +fi + +# 构建镜像 +echo "" +echo "构建Docker镜像..." +docker-compose -f docker-compose.prod.yml build --no-cache + +# 启动服务 +echo "" +echo "启动服务..." +docker-compose -f docker-compose.prod.yml up -d + +# 显示服务状态 +echo "" +echo "等待服务启动..." +sleep 10 + +echo "" +echo "服务状态:" +docker-compose -f docker-compose.prod.yml ps + +# 检查服务健康状态 +echo "" +echo "检查服务健康状态..." +sleep 5 + +echo "" +echo "Web服务健康检查:" +docker inspect --format='{{json .State.Health}}' omniverse-web | python3 -m json.tool || echo "健康检查未就绪" + +echo "" +echo "News爬虫服务健康检查:" +docker inspect --format='{{json .State.Health}}' omniverse-news-crawler | python3 -m json.tool || echo "健康检查未就绪" + +echo "" +echo "==========================================" +echo " 部署完成!" +echo "==========================================" +echo "" +echo "访问地址:" +echo " HTTP: http://www.omniverseai.net" +echo " HTTPS: https://www.omniverseai.net (需要先申请SSL证书)" +echo "" +echo "查看日志:" +echo " docker-compose -f docker-compose.prod.yml logs -f" +echo "" +echo "如需申请SSL证书,请运行:" +echo " ./init-ssl.sh" +echo "" diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml new file mode 100644 index 0000000..b7ccf35 --- /dev/null +++ b/docker-compose.prod.yml @@ -0,0 +1,85 @@ +version: '3.8' + +services: + # Nginx反向代理 + nginx: + image: nginx:alpine + container_name: omniverse-nginx + ports: + - "80:80" + - "443:443" + volumes: + - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro + - ./nginx/default.conf:/etc/nginx/conf.d/default.conf:ro + - ./certbot/conf:/etc/letsencrypt:ro + - ./certbot/www:/var/www/certbot:ro + - nginx_logs:/var/log/nginx + depends_on: + - web + - news-crawler + restart: unless-stopped + networks: + - omniverse-network + + # 前端应用 + web: + build: + context: . + dockerfile: Dockerfile + container_name: omniverse-web + environment: + - NODE_ENV=production + restart: unless-stopped + healthcheck: + test: ["CMD", "wget", "-q", "--spider", "http://localhost:3000"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + networks: + - omniverse-network + + # 新闻爬虫服务 + news-crawler: + build: + context: ./news-crawler-service + dockerfile: Dockerfile + container_name: omniverse-news-crawler + environment: + - NODE_ENV=production + - PORT=3001 + volumes: + - news_data:/app/data + - news_logs:/app/logs + restart: unless-stopped + healthcheck: + test: ["CMD", "wget", "-q", "--spider", "http://localhost:3001/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + networks: + - omniverse-network + + # Certbot用于SSL证书申请和续期 + certbot: + image: certbot/certbot:latest + container_name: omniverse-certbot + volumes: + - ./certbot/conf:/etc/letsencrypt + - ./certbot/www:/var/www/certbot + entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 12h & wait $${!}; done;'" + networks: + - omniverse-network + +networks: + omniverse-network: + driver: bridge + +volumes: + news_data: + driver: local + news_logs: + driver: local + nginx_logs: + driver: local diff --git a/init-ssl.sh b/init-ssl.sh new file mode 100755 index 0000000..1d0a3fa --- /dev/null +++ b/init-ssl.sh @@ -0,0 +1,49 @@ +#!/bin/bash + +# SSL证书初始化脚本 +# 用于首次申请Let's Encrypt证书 + +set -e + +DOMAIN="www.omniverseai.net" +EMAIL="admin@omniverseai.net" # 请修改为您的实际邮箱 + +echo "正在为域名 ${DOMAIN} 申请SSL证书..." + +# 创建必要的目录 +mkdir -p ./certbot/conf +mkdir -p ./certbot/www + +# 启动nginx和certbot服务 +docker-compose -f docker-compose.prod.yml up -d nginx + +# 等待nginx启动 +echo "等待Nginx启动..." +sleep 5 + +# 申请证书 +echo "申请SSL证书..." +docker-compose -f docker-compose.prod.yml run --rm certbot certonly \ + --webroot \ + --webroot-path=/var/www/certbot \ + --email ${EMAIL} \ + --agree-tos \ + --no-eff-email \ + -d ${DOMAIN} \ + -d omniverseai.net + +if [ $? -eq 0 ]; then + echo "SSL证书申请成功!" + echo "" + echo "接下来的步骤:" + echo "1. 编辑 nginx/default.conf" + echo "2. 取消注释HTTPS服务器配置块" + echo "3. 注释或删除HTTP配置中的临时proxy_pass" + echo "4. 取消注释HTTP到HTTPS重定向" + echo "5. 运行: docker-compose -f docker-compose.prod.yml restart nginx" +else + echo "SSL证书申请失败,请检查:" + echo "1. 域名DNS是否正确解析到本服务器" + echo "2. 80端口是否可以从外网访问" + echo "3. 邮箱地址是否正确" +fi diff --git a/k8s/production.yaml b/k8s/production.yaml new file mode 100644 index 0000000..52707c2 --- /dev/null +++ b/k8s/production.yaml @@ -0,0 +1,124 @@ +# ============================================================================ +# OmniVerse Website - Kubernetes Deployment +# Namespace: prod +# Domain: www.omniverseai.net +# ============================================================================ + +--- +# Deployment +apiVersion: apps/v1 +kind: Deployment +metadata: + name: website + namespace: prod + labels: + app: website + environment: production +spec: + replicas: 2 + strategy: + type: RollingUpdate + rollingUpdate: + maxSurge: 1 + maxUnavailable: 0 + selector: + matchLabels: + app: website + template: + metadata: + labels: + app: website + environment: production + spec: + containers: + - name: website + image: harbor.omniverseai.net/omniverse/website:latest + imagePullPolicy: Always + ports: + - name: http + containerPort: 80 + protocol: TCP + + livenessProbe: + httpGet: + path: /health + port: http + initialDelaySeconds: 10 + periodSeconds: 10 + timeoutSeconds: 3 + failureThreshold: 3 + + readinessProbe: + httpGet: + path: /health + port: http + initialDelaySeconds: 5 + periodSeconds: 5 + timeoutSeconds: 3 + failureThreshold: 3 + + resources: + requests: + cpu: 100m + memory: 128Mi + limits: + cpu: 500m + memory: 512Mi + + imagePullSecrets: + - name: harbor-secret + +--- +# Service +apiVersion: v1 +kind: Service +metadata: + name: website-svc + namespace: prod + labels: + app: website +spec: + type: ClusterIP + selector: + app: website + ports: + - name: http + port: 80 + targetPort: 80 + protocol: TCP + +--- +# Ingress +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: website-ingress + namespace: prod + annotations: + nginx.ingress.kubernetes.io/rewrite-target: / + nginx.ingress.kubernetes.io/proxy-body-size: "50m" + nginx.ingress.kubernetes.io/ssl-redirect: "false" +spec: + ingressClassName: nginx + rules: + - host: www.omniverseai.net + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: website-svc + port: + number: 80 + + - host: omniverseai.net + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: website-svc + port: + number: 80 diff --git a/news-crawler-service/Dockerfile b/news-crawler-service/Dockerfile new file mode 100644 index 0000000..b192d75 --- /dev/null +++ b/news-crawler-service/Dockerfile @@ -0,0 +1,30 @@ +# OmniVerse News Crawler Service Dockerfile +FROM node:20-alpine + +# Set working directory +WORKDIR /app + +# Install build dependencies +RUN apk add --no-cache python3 make g++ + +# Copy package files +COPY package*.json ./ + +# Install dependencies +RUN npm install --production + +# Copy source code +COPY . . + +# Create data and log directories +RUN mkdir -p /app/data /app/logs + +# Expose port +EXPOSE 3001 + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \ + CMD wget --no-verbose --tries=1 --spider http://localhost:3001/health || exit 1 + +# Start application +CMD ["npm", "start"] diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 0000000..e8d8c08 --- /dev/null +++ b/nginx.conf @@ -0,0 +1,36 @@ +server { + listen 80; + server_name _; + + root /usr/share/nginx/html; + index index.html; + + # Gzip compression + gzip on; + gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript; + gzip_vary on; + gzip_min_length 1024; + + # Security headers + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-Content-Type-Options "nosniff" always; + add_header X-XSS-Protection "1; mode=block" always; + + # SPA fallback + location / { + try_files $uri $uri/ /index.html; + } + + # Cache static assets + location ~* \.(jpg|jpeg|png|gif|ico|css|js|svg|woff|woff2|ttf|eot)$ { + expires 1y; + add_header Cache-Control "public, immutable"; + } + + # Health check endpoint + location /health { + access_log off; + return 200 "OK\n"; + add_header Content-Type text/plain; + } +} diff --git a/nginx/default.conf b/nginx/default.conf new file mode 100644 index 0000000..af93fe4 --- /dev/null +++ b/nginx/default.conf @@ -0,0 +1,84 @@ +# OmniVerse官网 - HTTP配置(用于SSL证书申请和重定向) +server { + listen 80; + server_name www.omniverseai.net omniverseai.net; + + # Let's Encrypt验证目录 + location /.well-known/acme-challenge/ { + root /var/www/certbot; + } + + # 其他请求重定向到HTTPS(证书配置后启用) + location / { + # 临时允许HTTP访问,用于测试 + proxy_pass http://web:3000; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + # 支持 Cloudflare HTTPS 代理 + proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto; + proxy_cache_bypass $http_upgrade; + } + + # News API代理 + location /api/news { + proxy_pass http://news-crawler:3001; + 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; + } +} + +# HTTPS配置(获得证书后启用) +# server { +# listen 443 ssl http2; +# server_name www.omniverseai.net omniverseai.net; +# +# # SSL证书配置 +# ssl_certificate /etc/letsencrypt/live/www.omniverseai.net/fullchain.pem; +# ssl_certificate_key /etc/letsencrypt/live/www.omniverseai.net/privkey.pem; +# ssl_trusted_certificate /etc/letsencrypt/live/www.omniverseai.net/chain.pem; +# +# # SSL安全配置 +# ssl_protocols TLSv1.2 TLSv1.3; +# ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384'; +# ssl_prefer_server_ciphers off; +# ssl_session_cache shared:SSL:10m; +# ssl_session_timeout 10m; +# ssl_stapling on; +# ssl_stapling_verify on; +# +# # 安全头 +# add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; +# add_header X-Frame-Options "SAMEORIGIN" always; +# add_header X-Content-Type-Options "nosniff" always; +# add_header X-XSS-Protection "1; mode=block" always; +# +# # 前端应用 +# location / { +# proxy_pass http://web:3000; +# proxy_http_version 1.1; +# proxy_set_header Upgrade $http_upgrade; +# proxy_set_header Connection 'upgrade'; +# 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_cache_bypass $http_upgrade; +# } +# +# # News API代理 +# location /api/news { +# proxy_pass http://news-crawler:3001; +# 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/nginx/nginx.conf b/nginx/nginx.conf new file mode 100644 index 0000000..cd4a8d6 --- /dev/null +++ b/nginx/nginx.conf @@ -0,0 +1,40 @@ +# Nginx配置 - OmniVerse官网生产环境 + +# 主配置 +user nginx; +worker_processes auto; +error_log /var/log/nginx/error.log warn; +pid /var/run/nginx.pid; + +events { + worker_connections 1024; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + + access_log /var/log/nginx/access.log main; + + sendfile on; + tcp_nopush on; + tcp_nodelay on; + keepalive_timeout 65; + types_hash_max_size 2048; + client_max_body_size 20M; + + # Gzip压缩 + gzip on; + gzip_vary on; + gzip_min_length 1024; + gzip_types text/plain text/css text/xml text/javascript + application/x-javascript application/xml+rss + application/javascript application/json; + + # 包含站点配置 + include /etc/nginx/conf.d/*.conf; +} diff --git a/src/components/Navigation.tsx b/src/components/Navigation.tsx index 624d7ab..9edc05f 100644 --- a/src/components/Navigation.tsx +++ b/src/components/Navigation.tsx @@ -181,31 +181,30 @@ const Navigation: React.FC = ({ onOpenAuthModal }) => { : 'bg-transparent' }`} > -
-
+
+
{/* Logo */}
-
- +
+
-

+

{t('company.name')}

-

{t('company.tagline')}

{/* Desktop Navigation */} -
+
{navigationItems.map((item) => (
= ({ onOpenAuthModal }) => { {item.href.startsWith('/blog') || item.href.startsWith('/articles') || item.href.startsWith('/docs') || item.href.startsWith('/industry-news') ? ( = ({ onOpenAuthModal }) => { ) : item.href.startsWith('/about') || item.href.startsWith('/team') || item.href.startsWith('/history') || item.href.startsWith('/careers') || item.href.startsWith('/news') || item.href.startsWith('/investors') || item.href.startsWith('/csr') || item.href.startsWith('/contact') ? ( = ({ onOpenAuthModal }) => { href={item.href} onClick={(e) => handleSmoothScroll(e, item.id)} whileHover={{ y: -2 }} - className={`flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-all duration-300 ${ + className={`flex items-center gap-2 px-3 xl:px-4 py-2.5 rounded-lg text-sm font-medium whitespace-nowrap transition-all duration-300 ${ activeDropdown === item.id ? 'text-cyan-400 bg-cyan-500/10' : 'text-gray-300 hover:text-cyan-400 hover:bg-gray-800/50' @@ -369,7 +368,7 @@ const Navigation: React.FC = ({ onOpenAuthModal }) => {
{/* Right Side Actions */} -
+
{/* Search Button */} = ({ onOpenAuthModal }) => { {isAuthenticated ? ( ) : ( -
- onOpenAuthModal('login')} - className={`px-4 py-2 border rounded-lg transition-all duration-300 ${ - effectiveTheme === 'dark' - ? 'text-cyan-400 border-cyan-500/50 hover:bg-cyan-500/10' - : 'text-cyan-600 border-cyan-500/50 hover:bg-cyan-50' - }`} - > - {t('nav.login')} - - onOpenAuthModal('register')} - className="px-4 py-2 bg-gradient-to-r from-cyan-500 to-green-500 text-black font-bold rounded-lg hover:shadow-lg hover:shadow-cyan-500/30 transition-all duration-300" - > - {t('nav.getStarted')} - -
+ onOpenAuthModal('login')} + className={`px-5 py-2.5 border rounded-lg text-sm font-medium transition-all duration-300 ${ + effectiveTheme === 'dark' + ? 'text-cyan-400 border-cyan-500/50 hover:bg-cyan-500/10' + : 'text-cyan-600 border-cyan-500/50 hover:bg-cyan-50' + }`} + > + {t('nav.login')} + )}
@@ -448,7 +437,7 @@ const Navigation: React.FC = ({ onOpenAuthModal }) => { whileHover={{ scale: 1.05 }} whileTap={{ scale: 0.95 }} onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)} - className="lg:hidden p-2 text-gray-300 hover:text-cyan-400 transition-colors" + className="xl:hidden p-2 text-gray-300 hover:text-cyan-400 transition-colors" > {isMobileMenuOpen ? : } @@ -465,9 +454,9 @@ const Navigation: React.FC = ({ onOpenAuthModal }) => { animate={{ opacity: 1, x: 0 }} exit={{ opacity: 0, x: '100%' }} transition={{ type: 'tween', duration: 0.3 }} - className="fixed inset-0 z-40 bg-gray-900/95 backdrop-blur-xl lg:hidden" + className="fixed inset-0 z-40 bg-gray-900/95 backdrop-blur-xl xl:hidden" > -
+
{/* Search Bar */}
= ({ onOpenAuthModal }) => {
) : ( -
- -
)} diff --git a/src/main.tsx b/src/main.tsx index f17eacc..9d84daa 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -9,7 +9,7 @@ import { SearchProvider } from './contexts/SearchContext' import { AuthProvider } from './contexts/AuthContext' import { FavoritesProvider } from './contexts/FavoritesContext' -const basename = import.meta.env.PROD ? '/website' : ''; +const basename = ''; createRoot(document.getElementById('root')!).render( diff --git a/vite.config.ts b/vite.config.ts index 5e3d98f..c5a6fa1 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -4,7 +4,7 @@ import react from '@vitejs/plugin-react' // https://vite.dev/config/ export default defineConfig({ plugins: [react()], - base: process.env.NODE_ENV === 'production' ? '/website/' : '/', + base: '/', server: { port: 5174, host: '0.0.0.0'