spec-coding-skills/drone_cicd_deployment_guide (2).md
zfc 755dd3409c chore: Add 3 dev rules to CLAUDE.md template and archive CICD reference docs
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 21:38:43 +08:00

948 lines
28 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 自动构建并部署成功
□ 生产服务正常运行
□ 回滚流程测试通过
```