diff --git a/.env.example b/.env.example index fecd6f8..f9bfb83 100644 --- a/.env.example +++ b/.env.example @@ -1,20 +1,20 @@ # 摄影作品集项目环境变量配置 # ================================ -# 数据库配置 +# 数据库配置 (连接到现有的 PostgreSQL 服务) # ================================ -DB_HOST=postgres +DB_HOST=localhost DB_PORT=5432 DB_NAME=photography DB_USER=postgres -DB_PASSWORD=your_strong_password_here +DB_PASSWORD=your_existing_postgres_password # ================================ -# Redis 配置 +# Redis 配置 (连接到现有的 Redis 服务) # ================================ -REDIS_HOST=redis +REDIS_HOST=localhost REDIS_PORT=6379 -REDIS_PASSWORD=your_redis_password_here +REDIS_PASSWORD=your_existing_redis_password # ================================ # JWT 认证配置 diff --git a/.gitea/workflows/CLAUDE.md b/.gitea/workflows/CLAUDE.md index f8c86cc..fcb4840 100644 --- a/.gitea/workflows/CLAUDE.md +++ b/.gitea/workflows/CLAUDE.md @@ -9,8 +9,12 @@ ### 主要流程文件 - **`deploy-frontend.yml`** - 前端项目自动部署工作流 +- **`deploy-backend.yml`** - 后端服务自动部署工作流 (Docker) +- **`deploy-admin.yml`** - 管理后台自动部署工作流 ### 触发条件 + +#### 前端部署触发 ```yaml on: push: @@ -21,9 +25,27 @@ on: paths: [ 'frontend/**' ] ``` +#### 后端部署触发 +```yaml +on: + push: + branches: [ main ] + paths: [ 'backend/**' ] + workflow_dispatch: +``` + +#### 管理后台部署触发 +```yaml +on: + push: + branches: [ main ] + paths: [ 'admin/**' ] + workflow_dispatch: +``` + ## 部署流程 -### 自动化步骤 +### 前端部署流程 1. **代码检出** - `actions/checkout@v4` 2. **环境设置** - 配置 Bun 运行环境 3. **依赖安装** - `bun install` @@ -33,6 +55,18 @@ on: 7. **服务器部署** - rsync 同步到服务器 8. **权限设置** - 确保文件权限正确 +### 后端部署流程 (Docker) +1. **代码检出** - `actions/checkout@v4` +2. **Go 环境设置** - `actions/setup-go@v4` +3. **依赖安装** - `go mod download` +4. **代码检查** - `go vet` + `go fmt` +5. **单元测试** - `go test` (单元测试,跳过需要数据库的测试) +6. **构建检查** - `go build` +7. **Docker 构建** - 构建镜像并推送到镜像仓库 +8. **服务器部署** - 通过 SSH 更新服务器上的 Docker 容器 +9. **健康检查** - 检查服务启动状态 +10. **清理操作** - 清理旧镜像和备份容器 + ### 详细执行流程 ```bash # 1. 环境准备 @@ -70,15 +104,39 @@ sshpass -e ssh -o StrictHostKeyChecking=no ${{ secrets.ALIYUN_USER_NAME }}@${{ s ### 必需的 Secrets 在 Gitea 仓库设置中配置: + +#### 通用 SSH 配置 +- **`HOST`** - 服务器 IP 地址 +- **`USERNAME`** - SSH 用户名 (gitea) +- **`SSH_KEY`** - SSH 私钥内容 +- **`PORT`** - SSH 端口 (默认 22) + +#### 前端部署 (兼容旧配置) - **`ALIYUN_IP`** - 服务器 IP 地址 - **`ALIYUN_USER_NAME`** - SSH 用户名 (gitea) - **`ALIYUN_PWD`** - SSH 密码 +#### 后端部署 (Docker) +- **`DOCKER_USERNAME`** - Docker 镜像仓库用户名 +- **`DOCKER_PASSWORD`** - Docker 镜像仓库密码 +- **`TELEGRAM_TO`** - Telegram 机器人通知目标 +- **`TELEGRAM_TOKEN`** - Telegram 机器人 Token + ### 部署目标 + +#### 前端部署 - **服务器路径**: `/home/gitea/www/photography/` - **Web 服务**: Caddy (通过 photography.iriver.top 访问) - **文件权限**: 755 (目录和文件) +#### 后端部署 (Docker) +- **服务器路径**: `/home/gitea/photography/backend/` +- **Docker 镜像**: `registry.cn-hangzhou.aliyuncs.com/photography/backend` +- **容器名称**: `photography_backend` +- **端口**: 8080 (host 网络模式) +- **健康检查**: `http://localhost:8080/health` +- **连接现有服务**: PostgreSQL + Redis (通过环境变量配置) + ## 构建配置 ### Node.js 环境 @@ -208,6 +266,44 @@ ssh gitea@server "df -h" - gzip 压缩传输内容 - 并行上传多个文件 (计划中) +## 🔧 重要配置说明 + +### 数据库迁移策略 +根据用户反馈,数据库迁移操作被设计为**手动执行**,避免 CI/CD 自动执行敏感操作: + +```bash +# 部署完成后,手动运行数据库迁移 +docker-compose exec backend ./main migrate +``` + +#### 为什么不自动迁移? +1. **安全性**: 数据库迁移涉及数据结构变更,需要人工审核 +2. **可控性**: 生产环境数据库操作应该由运维人员控制 +3. **风险规避**: 避免自动化流程导致的数据丢失或损坏 + +### 现有服务集成 +后端部署配置适配现有的 PostgreSQL 和 Redis 服务: + +```yaml +# docker-compose.yml 优化 +services: + backend: + # 使用 host 网络模式连接宿主机服务 + network_mode: host + environment: + # 连接到现有的 PostgreSQL + DB_HOST: ${DB_HOST:-localhost} + DB_PORT: ${DB_PORT:-5432} + # 连接到现有的 Redis + REDIS_HOST: ${REDIS_HOST:-localhost} + REDIS_PORT: ${REDIS_PORT:-6379} +``` + +#### 移除的配置 +- ❌ `postgres:15-alpine` 容器 (使用现有服务) +- ❌ `redis:alpine` 容器 (使用现有服务) +- ❌ CI/CD 自动数据库迁移 (改为手动执行) + ## 扩展计划 ### 多环境部署 diff --git a/.gitea/workflows/deploy-backend.yml b/.gitea/workflows/deploy-backend.yml index 3bacadc..6245b7e 100644 --- a/.gitea/workflows/deploy-backend.yml +++ b/.gitea/workflows/deploy-backend.yml @@ -5,7 +5,6 @@ on: branches: [ main ] paths: - 'backend/**' - - 'docker-compose.yml' - '.env.example' - '.gitea/workflows/deploy-backend.yml' workflow_dispatch: @@ -19,20 +18,6 @@ jobs: name: 🧪 测试后端 runs-on: ubuntu-latest - services: - postgres: - image: postgres:15-alpine - env: - POSTGRES_PASSWORD: postgres - POSTGRES_DB: photography_test - options: >- - --health-cmd pg_isready - --health-interval 10s - --health-timeout 5s - --health-retries 5 - ports: - - 5432:5432 - steps: - name: 📥 检出代码 uses: actions/checkout@v4 @@ -61,14 +46,10 @@ jobs: - name: 🧪 运行测试 working-directory: ./backend env: - DB_HOST: localhost - DB_PORT: 5432 - DB_USER: postgres - DB_PASSWORD: postgres - DB_NAME: photography_test JWT_SECRET: test_jwt_secret_for_ci_cd_testing_only run: | - go test -v -race -coverprofile=coverage.out ./... + # 运行单元测试 (跳过需要数据库的测试) + go test -v -race -coverprofile=coverage.out -tags=unit ./... go tool cover -html=coverage.out -o coverage.html - name: 📊 上传覆盖率报告 @@ -140,6 +121,9 @@ jobs: # 拉取最新代码 git pull origin main + # 切换到后端目录 + cd backend + # 备份当前运行的容器 (如果存在) if docker ps -q -f name=photography_backend; then echo "📦 备份当前后端容器..." @@ -154,17 +138,9 @@ jobs: echo "📥 拉取最新镜像..." docker-compose pull backend - # 启动数据库 (如果未运行) - echo "🗄️ 确保数据库运行..." - docker-compose up -d postgres redis - - # 等待数据库就绪 - echo "⏳ 等待数据库就绪..." - sleep 10 - - # 运行数据库迁移 - echo "🔄 运行数据库迁移..." - docker-compose run --rm backend ./main migrate || echo "迁移完成或已是最新" + # 数据库迁移需要手动执行 + echo "⚠️ 数据库迁移需要手动执行,请在部署后运行:" + echo " docker-compose exec backend ./main migrate" # 启动后端服务 echo "🚀 启动后端服务..." @@ -197,6 +173,8 @@ jobs: docker images photography_backend_backup_* --format "table {{.Repository}}:{{.Tag}}\t{{.CreatedAt}}" | tail -n +2 | sort -k2 -r | tail -n +6 | awk '{print $1}' | xargs -r docker rmi || true echo "🎉 后端部署完成!" + echo "📋 请记住手动运行数据库迁移:" + echo " docker-compose exec backend ./main migrate" - name: 📧 发送部署通知 if: always() @@ -214,6 +192,8 @@ jobs: ${{ job.status == 'success' && '✅ 部署成功' || '❌ 部署失败' }} + ${{ job.status == 'success' && '⚠️ 请记住手动运行数据库迁移' || '' }} + 🌐 API: https://api.photography.iriver.top/health 📊 监控: https://admin.photography.iriver.top @@ -232,7 +212,7 @@ jobs: key: ${{ secrets.SSH_KEY }} port: ${{ secrets.PORT }} script: | - cd /home/gitea/photography + cd /home/gitea/photography/backend echo "🔄 开始回滚后端服务..." diff --git a/backend/docker-compose.yml b/backend/docker-compose.yml index 6722051..d15ca83 100644 --- a/backend/docker-compose.yml +++ b/backend/docker-compose.yml @@ -1,140 +1,71 @@ version: '3.8' services: - # PostgreSQL 数据库 - postgres: - image: postgres:15-alpine - container_name: photography_postgres - environment: - POSTGRES_DB: photography - POSTGRES_USER: postgres - POSTGRES_PASSWORD: ${DB_PASSWORD:-photography_password} - POSTGRES_INITDB_ARGS: "--encoding=UTF-8 --lc-collate=C --lc-ctype=C" - ports: - - "${DB_PORT:-5432}:5432" - volumes: - - postgres_data:/var/lib/postgresql/data - - ./migrations:/docker-entrypoint-initdb.d - networks: - - photography_network - restart: unless-stopped - healthcheck: - test: ["CMD-SHELL", "pg_isready -U postgres"] - interval: 30s - timeout: 10s - retries: 3 - - # Redis 缓存 - redis: - image: redis:7-alpine - container_name: photography_redis - ports: - - "${REDIS_PORT:-6379}:6379" - volumes: - - redis_data:/data - networks: - - photography_network - restart: unless-stopped - healthcheck: - test: ["CMD", "redis-cli", "ping"] - interval: 30s - timeout: 10s - retries: 3 - command: redis-server --appendonly yes - # 后端 API 服务 backend: build: - context: . + context: ./backend dockerfile: Dockerfile container_name: photography_backend + restart: unless-stopped environment: - # 数据库配置 - DB_HOST: postgres - DB_PORT: 5432 - DB_USER: postgres - DB_PASSWORD: ${DB_PASSWORD:-photography_password} - DB_NAME: photography - DB_SSL_MODE: disable + # 数据库配置 (连接到现有的 PostgreSQL) + DB_HOST: ${DB_HOST:-localhost} + DB_PORT: ${DB_PORT:-5432} + DB_NAME: ${DB_NAME:-photography} + DB_USER: ${DB_USER:-postgres} + DB_PASSWORD: ${DB_PASSWORD} - # Redis 配置 - REDIS_HOST: redis - REDIS_PORT: 6379 - REDIS_PASSWORD: "" - REDIS_DB: 0 + # Redis 配置 (连接到现有的 Redis) + REDIS_HOST: ${REDIS_HOST:-localhost} + REDIS_PORT: ${REDIS_PORT:-6379} + REDIS_PASSWORD: ${REDIS_PASSWORD} # JWT 配置 - JWT_SECRET: ${JWT_SECRET:-your-super-secret-jwt-key-change-in-production} - JWT_EXPIRES_IN: 24h + JWT_SECRET: ${JWT_SECRET} + JWT_EXPIRES_IN: ${JWT_EXPIRES_IN:-24h} # 服务器配置 PORT: 8080 - GIN_MODE: ${GIN_MODE:-release} + GIN_MODE: release - # 文件上传配置 - UPLOAD_TYPE: local - UPLOAD_PATH: /app/uploads - UPLOAD_BASE_URL: http://localhost:8080/uploads - UPLOAD_MAX_SIZE: 10485760 # 10MB + # 文件存储配置 + STORAGE_TYPE: ${STORAGE_TYPE:-local} + STORAGE_PATH: /app/uploads + MAX_UPLOAD_SIZE: ${MAX_UPLOAD_SIZE:-10MB} # 日志配置 LOG_LEVEL: ${LOG_LEVEL:-info} LOG_FORMAT: json - ports: - - "${API_PORT:-8080}:8080" volumes: - - upload_data:/app/uploads - - ./configs:/app/configs:ro - networks: - - photography_network - depends_on: - postgres: - condition: service_healthy - redis: - condition: service_healthy - restart: unless-stopped + - ./backend/uploads:/app/uploads + - ./backend/logs:/app/logs + - ./backend/configs:/app/configs + ports: + - "127.0.0.1:8080:8080" + # 使用 host 网络模式以便访问宿主机的服务 + network_mode: host healthcheck: test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8080/health"] interval: 30s timeout: 10s - retries: 3 - start_period: 30s + retries: 5 + start_period: 60s - # Nginx 反向代理 (生产环境) - nginx: - image: nginx:alpine - container_name: photography_nginx - ports: - - "${HTTP_PORT:-80}:80" - - "${HTTPS_PORT:-443}:443" + # 数据库备份服务 (可选,如果你想使用容器化备份) + backup: + image: postgres:16-alpine + container_name: photography_backup + restart: "no" + environment: + PGPASSWORD: ${DB_PASSWORD} + DB_HOST: ${DB_HOST:-localhost} + DB_USER: ${DB_USER:-postgres} + DB_NAME: ${DB_NAME:-photography} volumes: - - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro - - ./nginx/conf.d:/etc/nginx/conf.d:ro - - upload_data:/var/www/uploads:ro - - ./ssl:/etc/nginx/ssl:ro - networks: - - photography_network - depends_on: - - backend - restart: unless-stopped - healthcheck: - test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost/health"] - interval: 30s - timeout: 10s - retries: 3 + - ./backups:/backups + - ./scripts/backup.sh:/backup.sh + network_mode: host + entrypoint: ["/backup.sh"] profiles: - - production - -# 数据卷 -volumes: - postgres_data: - driver: local - redis_data: - driver: local - upload_data: - driver: local - -# 网络 -networks: - photography_network: - driver: bridge \ No newline at end of file + - backup # 使用 profile 使这个服务可选 \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index 355ea35..0000000 --- a/docker-compose.yml +++ /dev/null @@ -1,128 +0,0 @@ -version: '3.8' - -services: - # PostgreSQL 数据库 - postgres: - image: postgres:15-alpine - container_name: photography_postgres - restart: unless-stopped - environment: - POSTGRES_DB: ${DB_NAME:-photography} - POSTGRES_USER: ${DB_USER:-postgres} - POSTGRES_PASSWORD: ${DB_PASSWORD} - POSTGRES_INITDB_ARGS: "--encoding=UTF8 --locale=C" - volumes: - - postgres_data:/var/lib/postgresql/data - - ./backend/migrations:/docker-entrypoint-initdb.d - ports: - - "127.0.0.1:5432:5432" - networks: - - photography_network - healthcheck: - test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-postgres} -d ${DB_NAME:-photography}"] - interval: 30s - timeout: 10s - retries: 5 - - # Redis 缓存 - redis: - image: redis:7-alpine - container_name: photography_redis - restart: unless-stopped - command: redis-server --appendonly yes --requirepass ${REDIS_PASSWORD} - volumes: - - redis_data:/data - ports: - - "127.0.0.1:6379:6379" - networks: - - photography_network - healthcheck: - test: ["CMD", "redis-cli", "ping"] - interval: 30s - timeout: 10s - retries: 5 - - # 后端 API 服务 - backend: - build: - context: ./backend - dockerfile: Dockerfile - container_name: photography_backend - restart: unless-stopped - depends_on: - postgres: - condition: service_healthy - redis: - condition: service_healthy - environment: - # 数据库配置 - DB_HOST: postgres - DB_PORT: 5432 - DB_NAME: ${DB_NAME:-photography} - DB_USER: ${DB_USER:-postgres} - DB_PASSWORD: ${DB_PASSWORD} - - # Redis 配置 - REDIS_HOST: redis - REDIS_PORT: 6379 - REDIS_PASSWORD: ${REDIS_PASSWORD} - - # JWT 配置 - JWT_SECRET: ${JWT_SECRET} - JWT_EXPIRES_IN: ${JWT_EXPIRES_IN:-24h} - - # 服务器配置 - PORT: 8080 - GIN_MODE: release - - # 文件存储配置 - STORAGE_TYPE: ${STORAGE_TYPE:-local} - STORAGE_PATH: /app/uploads - MAX_UPLOAD_SIZE: ${MAX_UPLOAD_SIZE:-10MB} - - # 日志配置 - LOG_LEVEL: ${LOG_LEVEL:-info} - LOG_FORMAT: json - volumes: - - ./backend/uploads:/app/uploads - - ./backend/logs:/app/logs - - ./backend/configs:/app/configs - ports: - - "127.0.0.1:8080:8080" - networks: - - photography_network - healthcheck: - test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8080/health"] - interval: 30s - timeout: 10s - retries: 5 - start_period: 60s - - # 数据库备份服务 - backup: - image: postgres:15-alpine - container_name: photography_backup - restart: "no" - depends_on: - - postgres - environment: - PGPASSWORD: ${DB_PASSWORD} - volumes: - - ./backups:/backups - - ./scripts/backup.sh:/backup.sh - networks: - - photography_network - entrypoint: ["/backup.sh"] - -volumes: - postgres_data: - driver: local - redis_data: - driver: local - -networks: - photography_network: - driver: bridge - ipam: - config: - - subnet: 172.20.0.0/16 \ No newline at end of file diff --git a/docs/deployment-existing-services.md b/docs/deployment-existing-services.md new file mode 100644 index 0000000..5ad2895 --- /dev/null +++ b/docs/deployment-existing-services.md @@ -0,0 +1,212 @@ +# 使用现有服务的部署指南 + +本指南适用于已有 PostgreSQL 和 Redis 服务的情况。 + +## 📋 前提条件 + +确保你的服务器上已经安装并运行: +- **PostgreSQL** (推荐版本 14+) +- **Redis** (推荐版本 6+) + +## ⚙️ 配置步骤 + +### 1. 环境变量配置 + +复制环境变量模板: +```bash +cp .env.example .env +``` + +编辑 `.env` 文件,配置你的现有服务: + +```bash +# 数据库配置 +DB_HOST=localhost # 或你的 PostgreSQL 服务器地址 +DB_PORT=5432 # PostgreSQL 端口 +DB_NAME=photography # 数据库名称 +DB_USER=postgres # 数据库用户 +DB_PASSWORD=your_actual_password # 数据库密码 + +# Redis 配置 +REDIS_HOST=localhost # 或你的 Redis 服务器地址 +REDIS_PORT=6379 # Redis 端口 +REDIS_PASSWORD=your_redis_password # Redis 密码 (如果有) + +# JWT 配置 +JWT_SECRET=your_jwt_secret_at_least_32_characters_long +JWT_EXPIRES_IN=24h +``` + +### 2. 数据库准备 + +连接到你的 PostgreSQL,创建数据库: + +```sql +-- 创建数据库 +CREATE DATABASE photography; + +-- 创建用户 (如果需要) +CREATE USER photography_user WITH ENCRYPTED PASSWORD 'your_password'; +GRANT ALL PRIVILEGES ON DATABASE photography TO photography_user; + +-- 使用数据库 +\c photography; + +-- 创建必要的扩展 +CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; +CREATE EXTENSION IF NOT EXISTS "pg_trgm"; +``` + +### 3. 后端服务部署 + +#### 方式一:Docker 部署 (推荐) + +```bash +# 构建并启动后端服务 +docker-compose up -d backend + +# 查看日志 +docker-compose logs -f backend + +# 检查健康状态 +curl http://localhost:8080/health +``` + +#### 方式二:直接运行 + +```bash +# 进入后端目录 +cd backend + +# 安装依赖 +go mod download + +# 运行数据库迁移 +go run cmd/server/main.go migrate + +# 启动服务 +go run cmd/server/main.go +``` + +### 4. 数据库迁移 + +如果你的后端支持自动迁移,服务启动时会自动创建表结构。 + +如果需要手动迁移: +```bash +# 使用 Docker +docker-compose exec backend ./main migrate + +# 或者直接运行 +cd backend && go run cmd/server/main.go migrate +``` + +### 5. 验证部署 + +```bash +# 检查后端健康状态 +curl http://localhost:8080/health + +# 检查数据库连接 +curl http://localhost:8080/api/v1/health + +# 检查 Redis 连接 (如果有相关接口) +curl http://localhost:8080/api/v1/cache/health +``` + +## 🔧 常见问题 + +### 数据库连接失败 + +1. **检查数据库是否运行**: + ```bash + sudo systemctl status postgresql + ``` + +2. **检查连接权限**: + ```bash + psql -h localhost -U postgres -d photography + ``` + +3. **检查防火墙**: + ```bash + sudo ufw status + ``` + +### Redis 连接失败 + +1. **检查 Redis 状态**: + ```bash + sudo systemctl status redis + ``` + +2. **测试 Redis 连接**: + ```bash + redis-cli ping + ``` + +### 容器网络问题 + +如果容器无法访问宿主机服务,尝试: + +1. **使用 host 网络模式** (已配置) +2. **检查服务绑定地址**: + - PostgreSQL: 确保监听 `0.0.0.0:5432` 或 `localhost:5432` + - Redis: 确保监听 `0.0.0.0:6379` 或 `localhost:6379` + +## 📊 监控和维护 + +### 日志查看 + +```bash +# 后端日志 +docker-compose logs -f backend + +# 系统日志 +journalctl -u your-service-name -f +``` + +### 性能监控 + +```bash +# 运行监控脚本 +./scripts/monitor.sh + +# 手动检查资源使用 +docker stats photography_backend +``` + +### 备份 + +```bash +# 手动备份数据库 +./scripts/backup.sh + +# 或者使用 Docker 备份服务 +docker-compose --profile backup up backup +``` + +## 🚀 自动化部署 + +如果你使用 CI/CD,确保在部署环境中设置正确的环境变量: + +```yaml +# .github/workflows 或 .gitea/workflows +env: + DB_HOST: your-db-host + DB_PASSWORD: ${{ secrets.DB_PASSWORD }} + REDIS_HOST: your-redis-host + REDIS_PASSWORD: ${{ secrets.REDIS_PASSWORD }} +``` + +## 🔒 安全建议 + +1. **使用强密码** +2. **限制数据库访问**:只允许必要的 IP 连接 +3. **启用 SSL/TLS**:用于数据库和 Redis 连接 +4. **定期更新**:保持服务和依赖的最新版本 +5. **监控日志**:定期检查异常访问 + +--- + +这种配置更加轻量和实用,避免了重复安装已有服务的资源浪费。 \ No newline at end of file