muse_creative_hotspots/externaldocs/drone_cicd_deployment_guide.md
wxs 6cc703ada2 feat: monorepo 重构 + 新增 5 个平台适配器
项目从单体结构重构为 pnpm monorepo (shared/backend/frontend),
新增 YouTube、Instagram、Twitter/X、哔哩哔哩、微博 5 个平台适配器,
包含完整的单元测试和 E2E 测试覆盖。

- 完成 T-031~T-044: 5 个适配器实现、注册、配置和测试
- 重构前后端分离: Hono 后端 + Next.js 前端
- 151 个单元测试 + 21 个 Mock E2E + 25 个真实 E2E
- 适配器基于真实 TikHub API 响应结构实现

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 15:43:25 +08:00

28 KiB
Raw Permalink Blame History

Drone CI/CD 通用部署指南

基于 Drone CI + 私有 Docker Registry + Docker Compose 的自动化部署方案。适用于任何基于 Docker 镜像部署的项目。

本指南基于抖音评论管理系统的实际落地经验编写,涵盖从零搭建到生产可用的全流程。


目录


前提条件

组件 要求
Gitea 服务器 已有,可通过 HTTPS 访问
Drone CI 服务器 Ubuntu x64已安装 Docker已部署 Drone Server + Runner
生产服务器 Ubuntu x64已安装 Docker + Docker Compose
项目 包含 Dockerfile可通过 docker compose 部署

如果 Drone CI 尚未部署,参见 附录 ADrone CI 部署


架构概览

触发方式:
  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 服务器上执行:

docker run -d --name registry \
  -p 5000:5000 \
  -v /opt/registry-data:/var/lib/registry \
  --restart always \
  registry:2

验证:

curl http://localhost:5000/v2/_catalog
# 预期输出: {"repositories":[]}

1.2 配置 insecure-registries

由于 Registry 未配置 TLS需要在 所有需要访问 Registry 的服务器 上配置 insecure-registries。

编辑 /etc/docker/daemon.json

{
  "insecure-registries": ["<DRONE_CI_IP_OR_DOMAIN>:5000"]
}

然后重启 Docker

sudo systemctl restart docker

重要

  • insecure-registries 的值 不要http:// 前缀,直接写 host:port
  • 如果已有 registry-mirrors 等其他配置,合并到同一个 JSON 文件中,不要覆盖
  • Drone CI 服务器和生产服务器 都需要 配置

示例(假设 Drone CI 服务器 IP 为 192.168.1.100,域名为 ci.example.com

{
  "registry-mirrors": [
    "https://docker.1panel.live"
  ],
  "insecure-registries": [
    "ci.example.com:5000",
    "192.168.1.100:5000"
  ]
}

1.3 配置 SSH 免密登录

Drone CI 服务器上生成部署专用密钥:

ssh-keygen -t ed25519 -C "drone-ci-deploy" -f ~/.ssh/drone_deploy -N ""

将公钥添加到 生产服务器

# 如果 SSH 端口是 22
ssh-copy-id -i ~/.ssh/drone_deploy.pub <user>@<production-server-ip>

# 如果 SSH 端口不是 22如 3141
ssh-copy-id -i ~/.ssh/drone_deploy.pub -p <port> <user>@<production-server-ip>

验证:

ssh -i ~/.ssh/drone_deploy -p <port> <user>@<production-server-ip> "echo ok"

1.4 确保生产服务器用户有 Docker 权限

# 在生产服务器上
sudo usermod -aG docker <deploy-user>
# 需要重新登录生效

第二步:项目接入 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_<service> 每个需构建的服务的完整镜像地址 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_<service>:每个需要构建的服务单独一个 secret。例如项目有 backend 和 frontend就创建 backend_repofrontend_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 运算)

模板

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-<service-1>
    image: docker:27-cli
    volumes:
      - name: dockersock
        path: /var/run/docker.sock
    environment:
      IMAGE_REPO:
        from_secret: <service-1>_repo
    commands:
      - '[ -n "$IMAGE_REPO" ] || (echo "<service-1>_repo secret is empty" && exit 1)'
      - echo "Building <service-1>, tag=${DRONE_TAG:-latest}"
      - docker build -t "$IMAGE_REPO:${DRONE_TAG:-latest}" -t "$IMAGE_REPO:latest" ./<service-1-context>
      - docker push "$IMAGE_REPO:${DRONE_TAG:-latest}"
      - docker push "$IMAGE_REPO:latest"

  - name: build-<service-2>
    image: docker:27-cli
    volumes:
      - name: dockersock
        path: /var/run/docker.sock
    environment:
      IMAGE_REPO:
        from_secret: <service-2>_repo
    commands:
      - '[ -n "$IMAGE_REPO" ] || (echo "<service-2>_repo secret is empty" && exit 1)'
      - echo "Building <service-2>, tag=${DRONE_TAG:-latest}"
      - docker build -t "$IMAGE_REPO:${DRONE_TAG:-latest}" -t "$IMAGE_REPO:latest" ./<service-2-context>
      - 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]

替换占位符

占位符 替换为 示例
<service-1>, <service-2> 你的服务名 backend, frontend, api, web
<service-1-context> Docker build context 路径 ./backend, ./frontend, .
port: 22 生产服务器 SSH 端口 22, 3141

第四步:编写部署脚本

在项目中创建 scripts/deploy-remote.sh,这个脚本由 Drone SSH 步骤在生产服务器上调用。

通用模板

#!/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_TAGVERSION,兼容不同 compose 文件的命名习惯
  4. 健康检查:提供三种方式,根据项目镜像内置的工具选择
  5. 数据库迁移:在健康检查通过后执行,迁移失败会中断部署

第五步:生产服务器配置

5.1 目录结构

确保生产服务器上部署目录结构如下:

/opt/myproject/                    # DEPLOY_PATH
├── docker-compose.prod.yml        # 生产 compose 配置
├── .env                           # 环境变量(镜像地址、数据库密码等)
└── scripts/
    └── deploy-remote.sh           # 部署脚本(从代码仓库复制)

5.2 docker-compose.prod.yml 中的镜像引用

compose 文件中使用变量引用 Registry 中的镜像:

services:
  backend:
    image: ${DOCKER_REGISTRY}/myproject-backend:${VERSION:-latest}
    # ...

  frontend:
    image: ${DOCKER_REGISTRY}/myproject-frontend:${VERSION:-latest}
    # ...

5.3 .env 文件

# 镜像仓库地址 - 必须与 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:5000compose 中的镜像名必须是 ${DOCKER_REGISTRY}/myproject-backend:${VERSION:-latest}。三者拼起来的镜像全名必须完全一致。

5.4 首次部署

首次部署需要手动初始化:

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

backend_repo     = <registry-host>:5000/<project>-backend
frontend_repo    = <registry-host>:5000/<project>-frontend    # 如果有
deploy_host      = <production-server-ip>
deploy_user      = <ssh-username>
deploy_ssh_key   = <SSH 私钥完整内容cat ~/.ssh/drone_deploy>
deploy_path      = /opt/<project>
wecom_webhook    = <通知 webhook url>                          # 可选

deploy_ssh_key 是第一步中生成的 SSH 私钥内容,所有项目可以共用同一个密钥(只要目标生产服务器相同)。

Cron 配置

如果需要定时构建:

字段
Name nightly-build
Branch main
Schedule 0 16 * * *(北京时间 00:00

第七步:验证

7.1 Registry 验证

# 在 Drone CI 服务器上
curl http://localhost:5000/v2/_catalog

7.2 Tag 触发构建

git tag v1.0.0
git push origin v1.0.0

在 Drone 面板观察 pipeline 执行状态。

7.3 生产服务验证

# 检查容器状态
ssh -p <port> <user>@<prod-ip> "cd /opt/myproject && docker compose -f docker-compose.prod.yml ps"

# 检查镜像版本
ssh -p <port> <user>@<prod-ip> "docker images | grep myproject"

回滚方案

方式 1手动指定版本回滚

ssh -p <port> <user>@<prod-ip>
cd /opt/myproject
bash scripts/deploy-remote.sh v1.0.0    # 指定要回滚到的版本

方式 2直接 compose 回滚

ssh -p <port> <user>@<prod-ip>
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 触发

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.ymlscripts/deploy-remote.sh
  4. 生产服务器创建部署目录 + 放置 compose 文件和 .env
  5. (可选)配置 Cron 定时构建

Registry 镜像命名规范

建议统一命名格式:

<registry-host>:5000/<project-name>-<service>:<tag>

示例:

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

查看所有仓库:

curl http://localhost:5000/v2/_catalog

查看某个镜像的所有 tag

curl http://localhost:5000/v2/<project>-<service>/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_CREATEusername 填了邮箱,不是 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: <actual-port>

9. Docker 权限不足

错误:生产服务器 permission denied while trying to connect to the Docker daemon socket

解决sudo usermod -aG docker <user> 后重新登录

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 生产服务器 .envDOCKER_REGISTRY 变量是否正确
compose pull 拉到旧镜像 检查 Registry 地址和镜像 tag 是否一致
数据库迁移失败 docker compose logs -f <service> 查看报错
健康检查超时 增大 MAX_ATTEMPTS;检查服务是否正常启动

附录 ADrone CI 部署

如果 Drone CI 尚未部署,在 Drone CI 服务器上创建 ~/drone/docker-compose.yml

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://<your-gitea-domain>
      - DRONE_GITEA_CLIENT_ID=<gitea-oauth-app-client-id>
      - DRONE_GITEA_CLIENT_SECRET=<gitea-oauth-app-client-secret>
      - DRONE_SERVER_HOST=<your-drone-domain>
      - DRONE_SERVER_PROTO=https
      - DRONE_RPC_SECRET=<随机生成的长字符串>
      - DRONE_USER_CREATE=username:<your-gitea-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

启动:

cd ~/drone
docker compose up -d

Gitea OAuth App 配置

  1. Gitea → 站点管理 → 应用 → 创建 OAuth2 应用
  2. 重定向 URIhttps://<your-drone-domain>/login
  3. 将 Client ID 和 Client Secret 填入上面的 compose 配置

生成 RPC Secret

openssl rand -hex 16

反向代理配置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 时,按此清单逐项完成:

□ 基础设施(一次性,已完成则跳过)
  □ 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 自动构建并部署成功
  □ 生产服务正常运行
  □ 回滚流程测试通过