# Drone CI/CD 通用部署指南 基于 **Drone CI + 私有 Docker Registry + Docker Compose** 的自动化部署方案。适用于任何基于 Docker 镜像部署的项目。 本指南基于抖音评论管理系统的实际落地经验编写,涵盖从零搭建到生产可用的全流程。 --- ## 目录 - [前提条件](#前提条件) - [架构概览](#架构概览) - [第一步:基础设施准备(一次性)](#第一步基础设施准备一次性) - [第二步:项目接入 Drone CI](#第二步项目接入-drone-ci) - [第三步:编写 .drone.yml](#第三步编写-droneyml) - [第四步:编写部署脚本](#第四步编写部署脚本) - [第五步:生产服务器配置](#第五步生产服务器配置) - [第六步:配置 Drone 仓库设置](#第六步配置-drone-仓库设置) - [第七步:验证](#第七步验证) - [回滚方案](#回滚方案) - [多项目管理](#多项目管理) - [踩坑清单](#踩坑清单) - [故障排查速查表](#故障排查速查表) - [附录:完整模板文件](#附录完整模板文件) --- ## 前提条件 | 组件 | 要求 | |------|------| | Gitea 服务器 | 已有,可通过 HTTPS 访问 | | Drone CI 服务器 | Ubuntu x64,已安装 Docker,已部署 Drone Server + Runner | | 生产服务器 | Ubuntu x64,已安装 Docker + Docker Compose | | 项目 | 包含 Dockerfile,可通过 `docker compose` 部署 | 如果 Drone CI 尚未部署,参见 [附录 A:Drone CI 部署](#附录-a-drone-ci-部署)。 --- ## 架构概览 ```text 触发方式: A) 创建 Git Tag(如 v1.0.0)→ 自动构建部署指定版本 B) Drone Cron 定时任务 → 自动构建部署 latest 流水线: Drone CI 拉取代码 | +-- 构建镜像 1 (如 backend) → 推送到 Registry +-- 构建镜像 2 (如 frontend) → 推送到 Registry +-- 构建镜像 N ... | v SSH 到生产服务器 → 执行部署脚本 | +-- docker compose pull (拉取新镜像) +-- docker compose up -d (滚动更新) +-- 健康检查 +-- 数据库迁移(如有) | v 发送通知(企业微信/钉钉/飞书,可选) ``` 三台服务器各司其职: | 角色 | 职责 | |------|------| | Gitea 服务器 | 代码仓库,通过 Webhook 触发 Drone | | Drone CI 服务器 | 执行构建 + 运行私有 Docker Registry | | 生产服务器 | 运行 Docker Compose 部署的生产服务 | --- ## 第一步:基础设施准备(一次性) 以下操作只需做一次,所有项目共享。 ### 1.1 启动私有 Docker Registry 在 **Drone CI 服务器**上执行: ```bash docker run -d --name registry \ -p 5000:5000 \ -v /opt/registry-data:/var/lib/registry \ --restart always \ registry:2 ``` 验证: ```bash curl http://localhost:5000/v2/_catalog # 预期输出: {"repositories":[]} ``` ### 1.2 配置 insecure-registries 由于 Registry 未配置 TLS,需要在 **所有需要访问 Registry 的服务器** 上配置 insecure-registries。 编辑 `/etc/docker/daemon.json`: ```json { "insecure-registries": [":5000"] } ``` 然后重启 Docker: ```bash sudo systemctl restart docker ``` > **重要**: > - `insecure-registries` 的值 **不要** 带 `http://` 前缀,直接写 `host:port` > - 如果已有 `registry-mirrors` 等其他配置,合并到同一个 JSON 文件中,不要覆盖 > - Drone CI 服务器和生产服务器 **都需要** 配置 **示例**(假设 Drone CI 服务器 IP 为 `192.168.1.100`,域名为 `ci.example.com`): ```json { "registry-mirrors": [ "https://docker.1panel.live" ], "insecure-registries": [ "ci.example.com:5000", "192.168.1.100:5000" ] } ``` ### 1.3 配置 SSH 免密登录 在 **Drone CI 服务器**上生成部署专用密钥: ```bash ssh-keygen -t ed25519 -C "drone-ci-deploy" -f ~/.ssh/drone_deploy -N "" ``` 将公钥添加到 **生产服务器**: ```bash # 如果 SSH 端口是 22 ssh-copy-id -i ~/.ssh/drone_deploy.pub @ # 如果 SSH 端口不是 22(如 3141) ssh-copy-id -i ~/.ssh/drone_deploy.pub -p @ ``` 验证: ```bash ssh -i ~/.ssh/drone_deploy -p @ "echo ok" ``` ### 1.4 确保生产服务器用户有 Docker 权限 ```bash # 在生产服务器上 sudo usermod -aG docker # 需要重新登录生效 ``` --- ## 第二步:项目接入 Drone CI ### 2.1 在 Drone 面板激活仓库 1. 打开 Drone CI 面板(如 `https://drone.example.com`) 2. 点击 **SYNC** 同步 Gitea 仓库列表 3. 找到目标仓库,点击 **ACTIVATE** ### 2.2 开启 Trusted 模式 仓库 Settings → General → Project Settings → 勾选 **Trusted**。 > 这是必须的,因为构建步骤需要挂载宿主机 Docker socket。如果看不到 Trusted 选项,说明当前用户不是 Drone 管理员。检查 Drone Server 的 `DRONE_USER_CREATE` 环境变量,`username` 必须与 Gitea 登录用户名完全一致(不是邮箱)。 ### 2.3 配置 Secrets 在仓库 Settings → Secrets 添加以下密钥: | Secret 名称 | 说明 | 填写示例 | |-------------|------|---------| | `image_repo_` | 每个需构建的服务的完整镜像地址 | `ci.example.com:5000/myproject-backend` | | `deploy_host` | 生产服务器 IP 或域名 | `192.168.1.200` | | `deploy_user` | SSH 登录用户名 | `ubuntu` | | `deploy_ssh_key` | SSH **私钥**完整内容 | `-----BEGIN OPENSSH PRIVATE KEY-----...` | | `deploy_path` | 生产服务器上的项目部署目录 | `/opt/myproject` | | `wecom_webhook` | 通知 Webhook URL(可选) | `https://qyapi.weixin.qq.com/...` | > **关于 `image_repo_`**:每个需要构建的服务单独一个 secret。例如,项目有 backend 和 frontend,就创建 `backend_repo` 和 `frontend_repo`。Registry 地址必须与生产服务器 `.env` 中的镜像地址一致。 ### 2.4 配置 Cron 定时构建(可选) 仓库 Settings → Cron Jobs 添加: | 字段 | 值 | 说明 | |------|------|------| | Name | `nightly-build` | 任务名称 | | Branch | `main` | 构建分支 | | Schedule | `0 16 * * *` | UTC 16:00 = 北京时间 00:00 | > Drone 使用 UTC 时区解释 Cron 表达式。北京时间 = UTC + 8。 --- ## 第三步:编写 .drone.yml 在项目根目录创建 `.drone.yml`。 ### 核心原则 1. **使用 `docker:27-cli` + 宿主机 Docker socket**,不用 `plugins/docker` DinD(避免 DinD 启动失败问题) 2. **使用 `environment: from_secret`** 注入密钥,不用 `secrets:` 字段 3. **使用 `${DRONE_TAG:-latest}`** 作为镜像 tag,不自定义中间变量(避免 Drone 变量替换冲突) 4. **触发条件只用 `event`**,不叠加 `cron: [name]`(Drone 触发条件是 AND 运算) ### 模板 ```yaml kind: pipeline type: docker name: build-and-deploy trigger: event: - tag # Git Tag 触发 - cron # 定时触发 volumes: - name: dockersock host: path: /var/run/docker.sock steps: # ========================================== # 构建步骤 - 每个服务一个 step # ========================================== - name: build- image: docker:27-cli volumes: - name: dockersock path: /var/run/docker.sock environment: IMAGE_REPO: from_secret: _repo commands: - '[ -n "$IMAGE_REPO" ] || (echo "_repo secret is empty" && exit 1)' - echo "Building , tag=${DRONE_TAG:-latest}" - docker build -t "$IMAGE_REPO:${DRONE_TAG:-latest}" -t "$IMAGE_REPO:latest" ./ - docker push "$IMAGE_REPO:${DRONE_TAG:-latest}" - docker push "$IMAGE_REPO:latest" - name: build- image: docker:27-cli volumes: - name: dockersock path: /var/run/docker.sock environment: IMAGE_REPO: from_secret: _repo commands: - '[ -n "$IMAGE_REPO" ] || (echo "_repo secret is empty" && exit 1)' - echo "Building , tag=${DRONE_TAG:-latest}" - docker build -t "$IMAGE_REPO:${DRONE_TAG:-latest}" -t "$IMAGE_REPO:latest" ./ - docker push "$IMAGE_REPO:${DRONE_TAG:-latest}" - docker push "$IMAGE_REPO:latest" # ========================================== # 部署步骤 # ========================================== - name: deploy image: appleboy/drone-ssh environment: DEPLOY_PATH: from_secret: deploy_path settings: host: from_secret: deploy_host username: from_secret: deploy_user key: from_secret: deploy_ssh_key port: 22 # ← 改成实际 SSH 端口 command_timeout: 1800s script_stop: true envs: - DRONE_TAG - DEPLOY_PATH script: - IMAGE_TAG="$DRONE_TAG"; [ -n "$IMAGE_TAG" ] || IMAGE_TAG="latest" - cd "$DEPLOY_PATH" - bash scripts/deploy-remote.sh "$IMAGE_TAG" # ========================================== # 通知步骤(可选) # ========================================== - name: notify-success image: curlimages/curl environment: WEBHOOK_URL: from_secret: wecom_webhook commands: - | if [ -n "${WEBHOOK_URL:-}" ]; then VERSION="${DRONE_TAG:-nightly-$(date +%Y%m%d)}" curl -sS -X POST "$WEBHOOK_URL" \ -H "Content-Type: application/json" \ -d "{\"msgtype\":\"text\",\"text\":{\"content\":\"✅ 部署成功\nProject: ${DRONE_REPO}\n版本: ${VERSION}\n时间: $(date '+%Y-%m-%d %H:%M:%S')\"}}" fi when: status: [success] - name: notify-failure image: curlimages/curl environment: WEBHOOK_URL: from_secret: wecom_webhook commands: - | if [ -n "${WEBHOOK_URL:-}" ]; then VERSION="${DRONE_TAG:-nightly-$(date +%Y%m%d)}" curl -sS -X POST "$WEBHOOK_URL" \ -H "Content-Type: application/json" \ -d "{\"msgtype\":\"text\",\"text\":{\"content\":\"❌ 部署失败\nProject: ${DRONE_REPO}\n版本: ${VERSION}\n构建: ${DRONE_BUILD_LINK}\"}}" fi when: status: [failure] ``` **替换占位符**: | 占位符 | 替换为 | 示例 | |--------|--------|------| | ``, `` | 你的服务名 | `backend`, `frontend`, `api`, `web` | | `` | Docker build context 路径 | `./backend`, `./frontend`, `.` | | `port: 22` | 生产服务器 SSH 端口 | `22`, `3141` | --- ## 第四步:编写部署脚本 在项目中创建 `scripts/deploy-remote.sh`,这个脚本由 Drone SSH 步骤在生产服务器上调用。 ### 通用模板 ```bash #!/usr/bin/env bash # ======================================== # 远程部署脚本 - 被 Drone CI SSH 调用 # ======================================== # 用法: bash scripts/deploy-remote.sh [image_tag] set -euo pipefail # ---------- 日志 ---------- GREEN='\033[0;32m'; RED='\033[0;31m'; YELLOW='\033[1;33m'; NC='\033[0m' log_info() { echo -e "${GREEN}[INFO]${NC} $(date '+%Y-%m-%d %H:%M:%S') $1"; } log_warn() { echo -e "${YELLOW}[WARN]${NC} $(date '+%Y-%m-%d %H:%M:%S') $1"; } log_error() { echo -e "${RED}[ERROR]${NC} $(date '+%Y-%m-%d %H:%M:%S') $1"; } # ---------- 配置(可通过环境变量覆盖) ---------- DEPLOY_PATH="${DEPLOY_PATH:-/opt/myproject}" COMPOSE_FILE="${COMPOSE_FILE:-docker-compose.prod.yml}" IMAGE_TAG="${1:-latest}" LOCK_FILE="/tmp/$(basename "$DEPLOY_PATH")-deploy.lock" MAX_ATTEMPTS="${MAX_ATTEMPTS:-30}" HEALTH_CHECK_INTERVAL="${HEALTH_CHECK_INTERVAL:-3}" # ---------- 部署锁 ---------- cleanup_lock() { rm -f "$LOCK_FILE"; } acquire_lock() { if [ -f "$LOCK_FILE" ]; then local old_pid old_pid=$(cat "$LOCK_FILE" 2>/dev/null || true) if [ -n "$old_pid" ] && kill -0 "$old_pid" 2>/dev/null; then log_error "已有部署进程运行中 (PID: $old_pid)" exit 1 fi rm -f "$LOCK_FILE" fi echo "$$" > "$LOCK_FILE" trap cleanup_lock EXIT } # ---------- 工具函数 ---------- compose() { docker compose -f "$COMPOSE_FILE" "$@" } # 健康检查 - 根据项目实际情况修改 # 方式 1:容器内用 python 请求(适用于 Python 后端镜像) wait_healthy_python() { local service="$1" local url="${2:-http://127.0.0.1:8000/health}" local attempt=1 while [ "$attempt" -le "$MAX_ATTEMPTS" ]; do if compose exec -T "$service" python -c \ "import sys,urllib.request;urllib.request.urlopen('${url}', timeout=3);sys.exit(0)" \ >/dev/null 2>&1; then log_info "${service} 健康检查通过" return 0 fi log_info "等待 ${service} 就绪... (${attempt}/${MAX_ATTEMPTS})" sleep "$HEALTH_CHECK_INTERVAL" attempt=$((attempt + 1)) done return 1 } # 方式 2:容器内用 curl 请求(适用于内置 curl 的镜像) wait_healthy_curl() { local service="$1" local url="${2:-http://127.0.0.1:8000/health}" local attempt=1 while [ "$attempt" -le "$MAX_ATTEMPTS" ]; do if compose exec -T "$service" curl -sf "$url" >/dev/null 2>&1; then log_info "${service} 健康检查通过" return 0 fi log_info "等待 ${service} 就绪... (${attempt}/${MAX_ATTEMPTS})" sleep "$HEALTH_CHECK_INTERVAL" attempt=$((attempt + 1)) done return 1 } # 方式 3:从宿主机请求(适用于有端口映射的服务) wait_healthy_host() { local url="${1:-http://127.0.0.1:8000/health}" local attempt=1 while [ "$attempt" -le "$MAX_ATTEMPTS" ]; do if curl -sf "$url" >/dev/null 2>&1; then log_info "健康检查通过: $url" return 0 fi log_info "等待服务就绪... (${attempt}/${MAX_ATTEMPTS})" sleep "$HEALTH_CHECK_INTERVAL" attempt=$((attempt + 1)) done return 1 } # ---------- 主流程 ---------- main() { acquire_lock [ -d "$DEPLOY_PATH" ] || { log_error "部署目录不存在: $DEPLOY_PATH"; exit 1; } cd "$DEPLOY_PATH" [ -f "$COMPOSE_FILE" ] || { log_error "Compose 文件不存在: $COMPOSE_FILE"; exit 1; } # 导出镜像 tag 变量 - 根据 compose 文件中使用的变量名按需调整 export IMAGE_TAG export VERSION="$IMAGE_TAG" # export TAG="$IMAGE_TAG" # 如果你的 compose 用 ${TAG} log_info "========== 开始部署 ==========" log_info "镜像版本: $IMAGE_TAG" log_info "部署目录: $DEPLOY_PATH" log_info "Compose: $COMPOSE_FILE" # --- 1. 拉取新镜像 --- log_info "[1/5] 拉取新镜像..." compose pull # 或者只拉取特定服务: compose pull backend frontend # --- 2. 滚动更新服务 --- log_info "[2/5] 更新服务..." compose up -d --remove-orphans # 如果需要控制更新顺序,拆分为多步: # compose up -d --no-deps backend # compose up -d --no-deps frontend # --- 3. 健康检查 --- log_info "[3/5] 健康检查..." # 根据你的项目选择合适的健康检查方式,以下是示例: # wait_healthy_python "backend" "http://127.0.0.1:8000/health" # wait_healthy_curl "api" "http://127.0.0.1:3000/health" # wait_healthy_host "http://127.0.0.1:8080/health" # # 如果健康检查失败,取消注释下面的代码: # if ! wait_healthy_python "backend"; then # log_error "健康检查失败" # compose logs --tail=200 backend || true # exit 1 # fi # --- 4. 数据库迁移(如有) --- log_info "[4/5] 数据库迁移..." # 根据项目使用的迁移工具选择: # compose exec -T backend alembic upgrade head # Python Alembic # compose exec -T api npx prisma migrate deploy # Node Prisma # compose exec -T api npm run migrate # 自定义脚本 # compose exec -T backend python manage.py migrate # Django # 如果不需要迁移,注释掉即可 # --- 5. 清理 --- log_info "[5/5] 清理旧镜像..." docker image prune -f >/dev/null 2>&1 || log_warn "镜像清理失败,已跳过" log_info "========== 部署完成 ==========" log_info "版本: $IMAGE_TAG" compose ps } main "$@" ``` ### 脚本要点 1. **部署锁**:通过 PID 文件防止并发部署 2. **`set -euo pipefail`**:任何命令失败立即退出,防止"假成功" 3. **变量导出**:同时 export `IMAGE_TAG` 和 `VERSION`,兼容不同 compose 文件的命名习惯 4. **健康检查**:提供三种方式,根据项目镜像内置的工具选择 5. **数据库迁移**:在健康检查通过后执行,迁移失败会中断部署 --- ## 第五步:生产服务器配置 ### 5.1 目录结构 确保生产服务器上部署目录结构如下: ```text /opt/myproject/ # DEPLOY_PATH ├── docker-compose.prod.yml # 生产 compose 配置 ├── .env # 环境变量(镜像地址、数据库密码等) └── scripts/ └── deploy-remote.sh # 部署脚本(从代码仓库复制) ``` ### 5.2 docker-compose.prod.yml 中的镜像引用 compose 文件中使用变量引用 Registry 中的镜像: ```yaml services: backend: image: ${DOCKER_REGISTRY}/myproject-backend:${VERSION:-latest} # ... frontend: image: ${DOCKER_REGISTRY}/myproject-frontend:${VERSION:-latest} # ... ``` ### 5.3 .env 文件 ```bash # 镜像仓库地址 - 必须与 Drone Secret 中的 image_repo 前缀一致 DOCKER_REGISTRY=ci.example.com:5000 # 其他生产环境配置 POSTGRES_PASSWORD=xxx REDIS_PASSWORD=xxx # ... ``` > **变量一致性**:假设 Drone Secret `backend_repo` = `ci.example.com:5000/myproject-backend`,那么 `.env` 中的 `DOCKER_REGISTRY` 必须是 `ci.example.com:5000`,compose 中的镜像名必须是 `${DOCKER_REGISTRY}/myproject-backend:${VERSION:-latest}`。三者拼起来的镜像全名必须完全一致。 ### 5.4 首次部署 首次部署需要手动初始化: ```bash cd /opt/myproject # 确认 .env 和 compose 文件就位 ls -la .env docker-compose.prod.yml # 手动拉取并启动 export VERSION=latest docker compose -f docker-compose.prod.yml pull docker compose -f docker-compose.prod.yml up -d # 运行数据库迁移(如有) docker compose -f docker-compose.prod.yml exec -T backend alembic upgrade head ``` --- ## 第六步:配置 Drone 仓库设置 ### Secrets 快速清单 针对你的新项目,在 Drone 面板创建以下 Secrets: ```text backend_repo = :5000/-backend frontend_repo = :5000/-frontend # 如果有 deploy_host = deploy_user = deploy_ssh_key = deploy_path = /opt/ wecom_webhook = <通知 webhook url> # 可选 ``` > `deploy_ssh_key` 是第一步中生成的 SSH 私钥内容,所有项目可以共用同一个密钥(只要目标生产服务器相同)。 ### Cron 配置 如果需要定时构建: | 字段 | 值 | |------|------| | Name | `nightly-build` | | Branch | `main` | | Schedule | `0 16 * * *`(北京时间 00:00) | --- ## 第七步:验证 ### 7.1 Registry 验证 ```bash # 在 Drone CI 服务器上 curl http://localhost:5000/v2/_catalog ``` ### 7.2 Tag 触发构建 ```bash git tag v1.0.0 git push origin v1.0.0 ``` 在 Drone 面板观察 pipeline 执行状态。 ### 7.3 生产服务验证 ```bash # 检查容器状态 ssh -p @ "cd /opt/myproject && docker compose -f docker-compose.prod.yml ps" # 检查镜像版本 ssh -p @ "docker images | grep myproject" ``` --- ## 回滚方案 ### 方式 1:手动指定版本回滚 ```bash ssh -p @ cd /opt/myproject bash scripts/deploy-remote.sh v1.0.0 # 指定要回滚到的版本 ``` ### 方式 2:直接 compose 回滚 ```bash ssh -p @ cd /opt/myproject export VERSION=v1.0.0 docker compose -f docker-compose.prod.yml pull docker compose -f docker-compose.prod.yml up -d ``` ### 方式 3:通过 Git Tag 触发 ```bash git tag v1.0.0-rollback v1.0.0 git push origin v1.0.0-rollback ``` --- ## 多项目管理 当多个项目共用同一套 Drone CI + Registry 基础设施时: ### 共享部分(不需要重复) - Drone CI Server + Runner(已部署) - Docker Registry(已运行在 :5000) - SSH 密钥(可共用同一个 `drone_deploy` 密钥) - `insecure-registries` 配置(已在两台服务器上配置) ### 每个新项目需要做的 1. 在 Drone 面板激活仓库 + 开启 Trusted 2. 在 Drone 面板添加该仓库的 Secrets(镜像地址、部署路径) 3. 项目代码中添加 `.drone.yml` 和 `scripts/deploy-remote.sh` 4. 生产服务器创建部署目录 + 放置 compose 文件和 `.env` 5. (可选)配置 Cron 定时构建 ### Registry 镜像命名规范 建议统一命名格式: ```text :5000/-: ``` 示例: ```text ci.example.com:5000/douyin-backend:v1.0.0 ci.example.com:5000/douyin-frontend:v1.0.0 ci.example.com:5000/crm-api:v2.1.0 ci.example.com:5000/crm-web:v2.1.0 ci.example.com:5000/blog-app:latest ``` 查看所有仓库: ```bash curl http://localhost:5000/v2/_catalog ``` 查看某个镜像的所有 tag: ```bash curl http://localhost:5000/v2/-/tags/list ``` --- ## 踩坑清单 以下是实际部署中遇到的问题,按出现频率排序: ### 1. `insecure-registries` 格式错误 **错误**:Docker push 报 `server gave HTTP response to HTTPS client` **原因**:`/etc/docker/daemon.json` 中写了 `http://host:5000` **正确写法**:直接写 `host:5000`,不带协议前缀 ### 2. Drone 变量替换冲突 **错误**:构建命令中的 shell 变量值为空 **原因**:Drone 在执行命令前会对 `${VAR}` 做自身的变量替换,自定义 shell 变量 `${TAG}` 被 Drone 替换为空 **解决**:直接使用 Drone 内置变量 `${DRONE_TAG:-latest}`,不要赋值给中间变量 ### 3. Secret 注入不生效 **错误**:环境变量为空,步骤因 secret 缺失而失败 **原因**:使用了步骤级 `secrets:` 字段 **解决**:改用 `environment: { VAR: { from_secret: name } }` ### 4. plugins/docker DinD 启动失败 **错误**:`Unable to reach Docker Daemon after 15 attempts` **原因**:`plugins/docker` 内部启动 Docker 守护进程失败(cgroup / 存储驱动兼容性) **解决**:使用 `docker:27-cli` + 挂载宿主机 `/var/run/docker.sock` ### 5. Trusted 选项不可见 **错误**:仓库 Settings 中找不到 Trusted 选项 **原因**:`DRONE_USER_CREATE` 的 `username` 填了邮箱,不是 Gitea 用户名 **解决**:`username` 必须与 Gitea 登录用户名完全一致 ### 6. Tag 触发与 Cron 触发互相排斥 **错误**:推送 Tag 后 Drone 不触发构建 **原因**:`.drone.yml` 同时配了 `event: [tag, cron]` 和 `cron: [nightly-build]`,Drone 触发条件是 **AND** 运算 **解决**:只保留 `event: [tag, cron]`,不加 `cron:` 过滤 ### 7. 生产服务器镜像名不匹配 **错误**:`docker compose pull` 报 invalid reference 或拉取到错误镜像 **原因**:Drone Secret 中的 Registry 地址(如 IP)与生产服务器 `.env` 中的(如域名)不一致 **解决**:统一使用同一个地址格式(建议统一用域名或统一用 IP) ### 8. SSH 端口不对 **错误**:deploy 步骤超时或连接拒绝 **原因**:`appleboy/drone-ssh` 默认使用 22 端口 **解决**:在 `.drone.yml` 的 deploy settings 中显式指定 `port: ` ### 9. Docker 权限不足 **错误**:生产服务器 `permission denied while trying to connect to the Docker daemon socket` **解决**:`sudo usermod -aG docker ` 后重新登录 ### 10. daemon.json 被覆盖 **错误**:修改 `insecure-registries` 时丢失了已有的 `registry-mirrors` 配置 **预防**:修改前先 `cat /etc/docker/daemon.json` 查看现有内容,合并修改 --- ## 故障排查速查表 | 现象 | 检查方向 | |------|---------| | Pipeline 不触发 | Gitea Webhook 是否勾选"创建"事件;`.drone.yml` trigger 配置 | | Step 一直 pending | Runner 是否连通 Server;仓库是否 Trusted | | 构建报 secret 为空 | 使用 `environment: from_secret` 而非 `secrets:` | | Docker build 失败 | Dockerfile 是否正确;build context 路径是否对 | | Docker push 失败 (HTTPS) | 两台服务器 `insecure-registries` 配置 | | Docker push 失败 (连接拒绝) | Registry 容器是否运行:`docker ps \| grep registry` | | SSH 部署失败 | 密钥是否正确;端口是否匹配;用户是否有 Docker 权限 | | 找不到部署脚本 | `deploy-remote.sh` 是否已复制到生产服务器的对应目录 | | 镜像名 invalid reference | 生产服务器 `.env` 的 `DOCKER_REGISTRY` 变量是否正确 | | compose pull 拉到旧镜像 | 检查 Registry 地址和镜像 tag 是否一致 | | 数据库迁移失败 | `docker compose logs -f ` 查看报错 | | 健康检查超时 | 增大 `MAX_ATTEMPTS`;检查服务是否正常启动 | --- ## 附录 A:Drone CI 部署 如果 Drone CI 尚未部署,在 Drone CI 服务器上创建 `~/drone/docker-compose.yml`: ```yaml services: drone-server: image: drone/drone:2 container_name: drone-server restart: always ports: - "3080:80" # Drone Web UI 端口,通过反向代理暴露 HTTPS environment: - DRONE_GITEA_SERVER=https:// - DRONE_GITEA_CLIENT_ID= - DRONE_GITEA_CLIENT_SECRET= - DRONE_SERVER_HOST= - DRONE_SERVER_PROTO=https - DRONE_RPC_SECRET=<随机生成的长字符串> - DRONE_USER_CREATE=username:,admin:true volumes: - ./data:/data drone-runner: image: drone/drone-runner-docker:1 container_name: drone-runner restart: always depends_on: - drone-server environment: # Runner 通过 Docker 内网直连 server 的 80 端口 - DRONE_RPC_PROTO=http - DRONE_RPC_HOST=drone-server - DRONE_RPC_SECRET=<与 server 相同的 secret> - DRONE_RUNNER_CAPACITY=2 - DRONE_RUNNER_NAME=drone-runner-1 volumes: - /var/run/docker.sock:/var/run/docker.sock ``` 启动: ```bash cd ~/drone docker compose up -d ``` ### Gitea OAuth App 配置 1. Gitea → 站点管理 → 应用 → 创建 OAuth2 应用 2. 重定向 URI:`https:///login` 3. 将 Client ID 和 Client Secret 填入上面的 compose 配置 ### 生成 RPC Secret ```bash openssl rand -hex 16 ``` ### 反向代理配置(Nginx 示例) ```nginx server { listen 443 ssl; server_name drone.example.com; ssl_certificate /path/to/cert.pem; ssl_certificate_key /path/to/key.pem; location / { proxy_pass http://127.0.0.1:3080; 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; } } ``` --- ## 附录 B:新项目接入 Checklist 为新项目接入 CI/CD 时,按此清单逐项完成: ```text □ 基础设施(一次性,已完成则跳过) □ Registry 运行中 □ insecure-registries 已配置(Drone CI 服务器 + 生产服务器) □ SSH 密钥已配置 □ Drone 面板 □ 仓库已激活(ACTIVATE) □ Trusted 已勾选 □ Secrets 已添加(image_repo, deploy_host, deploy_user, deploy_ssh_key, deploy_path) □ Cron Job 已配置(如需定时构建) □ 项目代码 □ .drone.yml 已创建 □ scripts/deploy-remote.sh 已创建 □ 生产服务器 □ 部署目录已创建 □ docker-compose.prod.yml 已放置 □ .env 已配置(DOCKER_REGISTRY 等) □ scripts/deploy-remote.sh 已复制到部署目录 □ 首次手动部署成功 □ 验证 □ 推送 Tag 后 Drone 自动构建并部署成功 □ 生产服务正常运行 □ 回滚流程测试通过 ```