diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml new file mode 100644 index 0000000..886fb3b --- /dev/null +++ b/.github/workflows/ci-cd.yml @@ -0,0 +1,104 @@ +name: 部署 Next.js 站点到 Gitea + +# 定义触发工作流程的条件 +on: + push: + # 当推送到 "main" 分支时触发工作流程 + branches: ["main"] + # 手动触发工作流程 + workflow_dispatch: + +#permissions: +# contents: read +# pages: write +# id-token: write + +# 设置工作流程中所需的权限 +permissions: + # 只读权限用于读取仓库内容 + contents: read + +#concurrency: +# group: "pages" +# cancel-in-progress: false + +# 设置工作流的并发控制 +concurrency: + group: "deploy" # 定义并发组名称为 "deploy" + cancel-in-progress: false # 不取消正在运行的工作流 + +jobs: + deploy: + # 指定在最新的 Ubuntu 环境下运行 + runs-on: ubuntu-latest + # 主要执行的步骤 + steps: + # 第一步:检出(checkout)代码 + - name: 检出代码 + # 使用 GitHub 官方的 actions/checkout@v4 + #uses: actions/checkout@v4 + uses: https://git.aoun.ltd/actions/checkout@v4 + + # 第二步:将代码通过 SCP 传输到群晖服务器上 + - name: 🚚 将项目文件复制到目标服务器 + # 使用 appleboy 的 SCP 动作传输文件到远程服务器 + #uses: appleboy/scp-action@v0.1.7 + uses: https://git.aoun.ltd/298977887/scp-action@v0.1.7 + with: + host: ${{ secrets.SYNOLOGY_HOST }} + username: ${{ secrets.SYNOLOGY_USERNAME }} + key: ${{ secrets.SSH_PRIVATE_KEY }} + port: ${{ secrets.SYNOLOGY_SSH_PORT }} + source: "." + target: "/volume2/docker/saas00001/" + + - name: 🛠️ 在群晖上构建并运行Docker镜像 + #uses: appleboy/ssh-action@master + # 使用 appleboy 的 SSH 动作来连接服务器并运行命令 + #uses: appleboy/ssh-action@v1.0.3 + #使用自定义的 SSH 动作 + uses: https://git.aoun.ltd/298977887/ssh-action@v1.0.3 + with: + host: ${{ secrets.SYNOLOGY_HOST }} + username: ${{ secrets.SYNOLOGY_USERNAME }} + key: ${{ secrets.SSH_PRIVATE_KEY }} + port: ${{ secrets.SYNOLOGY_SSH_PORT }} + script: | + # 通过 set -e 使脚本在遇到错误时立即停止 + set -e + echo "🚀 开始执行部署..." + #cd /volume1/docker/saas01 + cd /volume2/docker/saas01 + echo "🔧 正在构建临时镜像..." + #/usr/local/bin/docker build -t saas01-temp . + if /usr/local/bin/docker build -t saas01-temp .; then + echo "✅ 镜像构建成功,开始更新容器..." + echo "🔧 正在停止旧容器..." + /usr/local/bin/docker stop saas01 || true + echo "🔧 正在删除旧容器..." + /usr/local/bin/docker rm saas01 || true + echo "🔧 正在删除旧镜像..." + /usr/local/bin/docker rmi saas01 || true + echo "🔧 重命名新镜像..." + /usr/local/bin/docker tag saas01-temp saas01 + /usr/local/bin/docker rmi saas01-temp + echo "🚀 正在运行新容器..." + #/usr/local/bin/docker run -d -p 3300:3000 --name saas01 -e TZ=Asia/Shanghai saas01 + # 启动新的容器,使用指定的环境变量和端口映射,并设置自动重启功能 + # 容器名称为 saas01,镜像名称为 saas01 + # 自动重启容器,除非手动停止 + # 将本地 3300 端口映射到容器的 3000 端口 + # 设置时区为上海 + # 设置 API 地址,最后一行没有反斜杠 + # 使用 saas01 镜像运行容器 + /usr/local/bin/docker run -d \ + --name saas01 \ + --restart unless-stopped \ + -p 3300:3000 \ + -e TZ=Asia/Shanghai \ + -e NEXT_PUBLIC_API=https://emoji.aoun.ltd/ \ + saas01 + echo "🎉 部署完成!" + else + echo "❌ 镜像构建失败!,保留旧容器运行。" + fi \ No newline at end of file diff --git a/docker-compose.production.yml b/docker-compose.production.yml new file mode 100644 index 0000000..a3d5b75 --- /dev/null +++ b/docker-compose.production.yml @@ -0,0 +1,67 @@ +version: '3.8' + +services: + # Next.js 应用服务 + nextjs-app: + image: ${DOCKER_IMAGE:-user/saas-app:latest} + container_name: nextjs-saas-app + ports: + - "3000:3000" + environment: + - NODE_ENV=production + - DB_HOST=${DB_HOST:-mysql} + - DB_USER=${DB_USER:-root} + - DB_PASSWORD=${DB_PASSWORD:-aiwoQwo520..} + - DB_NAME=${DB_NAME:-saas_db} + - DB_PORT=${DB_PORT:-3306} + depends_on: + - mysql + restart: unless-stopped + volumes: + - ./uploads:/app/uploads + networks: + - saas-network + + # MySQL 数据库服务 + mysql: + image: mysql:8.0 + container_name: my-mysql + restart: unless-stopped + environment: + - MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD:-aiwoQwo520..} + - MYSQL_DATABASE=${MYSQL_DATABASE:-saas_db} + - MYSQL_USER=${MYSQL_USER:-saas_user} + - MYSQL_PASSWORD=${MYSQL_PASSWORD:-saas_password} + ports: + - "3306:3306" + volumes: + - mysql_data:/var/lib/mysql + - ./scripts:/docker-entrypoint-initdb.d + networks: + - saas-network + + # Nginx 反向代理 + nginx: + image: nginx:alpine + container_name: nginx-proxy + ports: + - "80:80" + - "443:443" + volumes: + - ./nginx/conf.d:/etc/nginx/conf.d + - ./nginx/ssl:/etc/nginx/ssl + - ./nginx/logs:/var/log/nginx + depends_on: + - nextjs-app + networks: + - saas-network + restart: unless-stopped + +# 定义持久化卷 +volumes: + mysql_data: + +# 定义网络 +networks: + saas-network: + driver: bridge \ No newline at end of file diff --git a/nginx/conf.d/default.conf b/nginx/conf.d/default.conf new file mode 100644 index 0000000..5c72e6b --- /dev/null +++ b/nginx/conf.d/default.conf @@ -0,0 +1,86 @@ +server { + listen 80; + server_name localhost; + + # 重定向HTTP请求到HTTPS + return 301 https://$host$request_uri; +} + +server { + listen 443 ssl; + server_name localhost; + + # SSL证书配置 + ssl_certificate /etc/nginx/ssl/fullchain.pem; + ssl_certificate_key /etc/nginx/ssl/privkey.pem; + ssl_protocols TLSv1.2 TLSv1.3; + ssl_ciphers HIGH:!aNULL:!MD5; + ssl_prefer_server_ciphers on; + + # 安全相关的HTTP头 + add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; + add_header X-Content-Type-Options "nosniff"; + add_header X-Frame-Options "SAMEORIGIN"; + add_header X-XSS-Protection "1; mode=block"; + + # 负载限制配置 + client_max_body_size 100M; + client_body_buffer_size 128k; + + # 代理缓存设置 + proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=STATIC:10m inactive=7d use_temp_path=off; + proxy_cache_key "$scheme$request_method$host$request_uri"; + + # 静态资源处理 + location /_next/static { + proxy_cache STATIC; + proxy_pass http://nextjs-app:3000/_next/static; + 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; + + # 缓存设置 + expires 30d; + add_header Cache-Control "public, max-age=2592000, immutable"; + } + + # 上传文件目录 + location /uploads { + proxy_pass http://nextjs-app:3000/uploads; + 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; + } + + # API路由 + location /api { + proxy_pass http://nextjs-app:3000/api; + 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; + } + + # 所有其他请求 + location / { + proxy_pass http://nextjs-app: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; + } + + # 日志配置 + access_log /var/log/nginx/access.log; + error_log /var/log/nginx/error.log; +} \ No newline at end of file