chore: Add 3 dev rules to CLAUDE.md template and archive CICD reference docs

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
zfc
2026-03-02 21:38:43 +08:00
co-authored by Claude Opus 4.6
parent 50005945bc
commit 755dd3409c
3 changed files with 1591 additions and 0 deletions
+947
View File
@@ -0,0 +1,947 @@
# 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 尚未部署,参见 [附录 ADrone 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": ["<DRONE_CI_IP_OR_DOMAIN>: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 <user>@<production-server-ip>
# 如果 SSH 端口不是 22(如 3141
ssh-copy-id -i ~/.ssh/drone_deploy.pub -p <port> <user>@<production-server-ip>
```
验证:
```bash
ssh -i ~/.ssh/drone_deploy -p <port> <user>@<production-server-ip> "echo ok"
```
### 1.4 确保生产服务器用户有 Docker 权限
```bash
# 在生产服务器上
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_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-<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 步骤在生产服务器上调用。
### 通用模板
```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 = <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 验证
```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 <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:手动指定版本回滚
```bash
ssh -p <port> <user>@<prod-ip>
cd /opt/myproject
bash scripts/deploy-remote.sh v1.0.0 # 指定要回滚到的版本
```
### 方式 2:直接 compose 回滚
```bash
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 触发
```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
<registry-host>:5000/<project-name>-<service>:<tag>
```
示例:
```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/<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_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: <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 | 生产服务器 `.env``DOCKER_REGISTRY` 变量是否正确 |
| compose pull 拉到旧镜像 | 检查 Registry 地址和镜像 tag 是否一致 |
| 数据库迁移失败 | `docker compose logs -f <service>` 查看报错 |
| 健康检查超时 | 增大 `MAX_ATTEMPTS`;检查服务是否正常启动 |
---
## 附录 ADrone 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://<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
```
启动:
```bash
cd ~/drone
docker compose up -d
```
### Gitea OAuth App 配置
1. Gitea → 站点管理 → 应用 → 创建 OAuth2 应用
2. 重定向 URI`https://<your-drone-domain>/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 自动构建并部署成功
□ 生产服务正常运行
□ 回滚流程测试通过
```