264 lines
7.3 KiB
Bash
264 lines
7.3 KiB
Bash
#!/usr/bin/env bash
|
|
#
|
|
# deploy.sh — DeerFlow 后端 Linux Docker 一键部署脚本
|
|
#
|
|
# 用法:
|
|
# bash deploy.sh # 首次部署: 检查环境 → 构建/加载镜像 → 启动
|
|
# bash deploy.sh up # 启动服务
|
|
# bash deploy.sh down # 停止并移除容器
|
|
# bash deploy.sh restart # 重启
|
|
# bash deploy.sh status # 查看状态
|
|
# bash deploy.sh logs # 跟踪日志
|
|
# bash deploy.sh build # 仅构建镜像
|
|
# bash deploy.sh load # 从离线 tar 加载镜像
|
|
# bash deploy.sh health # 健康检查
|
|
#
|
|
# 环境变量(可选):
|
|
# HOST_PORT 宿主机映射端口,默认 8001
|
|
# CONTAINER_NAME 容器名,默认 deerflow-backend
|
|
# IMAGE_TAG 镜像标签,默认 deerflow-backend:latest
|
|
# IMAGE_TAR 离线镜像 tar 路径
|
|
# DEPLOY_MODE build | offline,默认 build
|
|
# SKIP_BUILD 1 时跳过构建/加载,直接启动
|
|
|
|
set -euo pipefail
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
cd "${SCRIPT_DIR}"
|
|
|
|
COMPOSE_PROJECT="${COMPOSE_PROJECT:-deerflow-backend}"
|
|
COMPOSE_FILE="${SCRIPT_DIR}/docker-compose.yaml"
|
|
COMPOSE=(docker compose -p "${COMPOSE_PROJECT}" -f "${COMPOSE_FILE}")
|
|
|
|
HOST_PORT="${HOST_PORT:-8001}"
|
|
CONTAINER_NAME="${CONTAINER_NAME:-deerflow-backend}"
|
|
IMAGE_TAG="${IMAGE_TAG:-deerflow-backend:latest}"
|
|
IMAGE_TAR="${IMAGE_TAR:-${SCRIPT_DIR}/deerflow-backend-offline-image.tar}"
|
|
DEPLOY_MODE="${DEPLOY_MODE:-build}"
|
|
SKIP_BUILD="${SKIP_BUILD:-0}"
|
|
|
|
GREEN='\033[0;32m'
|
|
YELLOW='\033[1;33m'
|
|
RED='\033[0;31m'
|
|
BLUE='\033[0;34m'
|
|
NC='\033[0m'
|
|
|
|
info() { echo -e "${BLUE}[INFO]${NC} $*"; }
|
|
ok() { echo -e "${GREEN}[ OK ]${NC} $*"; }
|
|
warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
|
|
fail() { echo -e "${RED}[FAIL]${NC} $*" >&2; exit 1; }
|
|
|
|
usage() {
|
|
sed -n '3,18p' "$0" | sed 's/^# \?//'
|
|
exit 0
|
|
}
|
|
|
|
require_cmd() {
|
|
command -v "$1" >/dev/null 2>&1 || fail "未找到命令: $1"
|
|
}
|
|
|
|
ensure_layout() {
|
|
mkdir -p "${SCRIPT_DIR}/data/data" "${SCRIPT_DIR}/data/backups" "${SCRIPT_DIR}/data/archives/conversations"
|
|
|
|
# switch_database_to_mysql.sh 等脚本期望根目录有这些路径
|
|
[ -e "${SCRIPT_DIR}/wheelhouse" ] || ln -sfn backend/wheelhouse "${SCRIPT_DIR}/wheelhouse"
|
|
[ -e "${SCRIPT_DIR}/skills" ] || ln -sfn backend/skills "${SCRIPT_DIR}/skills"
|
|
if [ ! -e "${SCRIPT_DIR}/extensions_config.json" ]; then
|
|
ln -sfn backend/extensions_config.json "${SCRIPT_DIR}/extensions_config.json"
|
|
fi
|
|
|
|
chmod +x "${SCRIPT_DIR}/scripts/container-entrypoint.sh" 2>/dev/null || true
|
|
}
|
|
|
|
ensure_env() {
|
|
if [ ! -f "${SCRIPT_DIR}/.env" ]; then
|
|
if [ -f "${SCRIPT_DIR}/.env.example" ]; then
|
|
cp "${SCRIPT_DIR}/.env.example" "${SCRIPT_DIR}/.env"
|
|
warn "已从 .env.example 生成 .env,部署前请编辑 API Key 和 AUTH_JWT_SECRET"
|
|
else
|
|
fail "缺少 .env 文件,请先创建并配置环境变量"
|
|
fi
|
|
fi
|
|
|
|
if [ ! -f "${SCRIPT_DIR}/config.yaml" ]; then
|
|
fail "缺少 config.yaml,请复制并编辑 config.example.yaml"
|
|
fi
|
|
|
|
if [ ! -f "${SCRIPT_DIR}/skill_evolution.json" ]; then
|
|
echo '{"enabled":false,"curator":{"enabled":false}}' > "${SCRIPT_DIR}/skill_evolution.json"
|
|
warn "已创建默认 skill_evolution.json"
|
|
fi
|
|
}
|
|
|
|
check_docker() {
|
|
require_cmd docker
|
|
docker info >/dev/null 2>&1 || fail "Docker 未运行或无权限,请确认 docker 服务已启动且当前用户在 docker 组"
|
|
docker compose version >/dev/null 2>&1 || fail "未找到 docker compose,请安装 Docker Compose V2"
|
|
}
|
|
|
|
generate_secret_if_needed() {
|
|
local key="$1"
|
|
local file="$2"
|
|
if grep -qE "^[[:space:]]*${key}=" "${SCRIPT_DIR}/.env" 2>/dev/null; then
|
|
local val
|
|
val="$(grep -E "^[[:space:]]*${key}=" "${SCRIPT_DIR}/.env" | tail -n1 | sed 's/^[^=]*=//')"
|
|
if [ -n "${val}" ] && [ "${val}" != "change-me-to-a-long-random-string" ]; then
|
|
return 0
|
|
fi
|
|
fi
|
|
if [ -f "${file}" ]; then
|
|
return 0
|
|
fi
|
|
mkdir -p "$(dirname "${file}")"
|
|
if command -v openssl >/dev/null 2>&1; then
|
|
openssl rand -hex 32 > "${file}"
|
|
chmod 600 "${file}"
|
|
warn "已生成 ${key} → ${file},建议将其写入 .env 后删除该文件"
|
|
fi
|
|
}
|
|
|
|
load_offline_image() {
|
|
if docker image inspect "${IMAGE_TAG}" >/dev/null 2>&1; then
|
|
ok "镜像已存在: ${IMAGE_TAG}"
|
|
return 0
|
|
fi
|
|
[ -f "${IMAGE_TAR}" ] || fail "离线镜像不存在: ${IMAGE_TAR},请设置 IMAGE_TAR 或先执行 bash deploy.sh build"
|
|
info "加载离线镜像: ${IMAGE_TAR}"
|
|
docker load -i "${IMAGE_TAR}"
|
|
ok "镜像加载完成: ${IMAGE_TAG}"
|
|
}
|
|
|
|
build_image() {
|
|
info "构建 Docker 镜像: ${IMAGE_TAG}"
|
|
export IMAGE_TAG
|
|
"${COMPOSE[@]}" build gateway
|
|
ok "镜像构建完成"
|
|
}
|
|
|
|
prepare_image() {
|
|
if [ "${SKIP_BUILD}" = "1" ]; then
|
|
info "SKIP_BUILD=1,跳过构建/加载"
|
|
return 0
|
|
fi
|
|
if docker image inspect "${IMAGE_TAG}" >/dev/null 2>&1; then
|
|
ok "使用已有镜像: ${IMAGE_TAG}"
|
|
return 0
|
|
fi
|
|
case "${DEPLOY_MODE}" in
|
|
offline)
|
|
load_offline_image
|
|
;;
|
|
build|*)
|
|
build_image
|
|
;;
|
|
esac
|
|
}
|
|
|
|
wait_for_health() {
|
|
local url="http://127.0.0.1:${HOST_PORT}/health"
|
|
local retries="${HEALTH_RETRIES:-30}"
|
|
local i=1
|
|
info "等待服务就绪: ${url}"
|
|
while [ "${i}" -le "${retries}" ]; do
|
|
if curl -fsS "${url}" >/dev/null 2>&1; then
|
|
ok "健康检查通过"
|
|
return 0
|
|
fi
|
|
sleep 2
|
|
i=$((i + 1))
|
|
done
|
|
warn "健康检查超时,请执行: bash deploy.sh logs"
|
|
return 1
|
|
}
|
|
|
|
cmd_up() {
|
|
check_docker
|
|
ensure_layout
|
|
ensure_env
|
|
generate_secret_if_needed "AUTH_JWT_SECRET" "${SCRIPT_DIR}/data/.auth-jwt-secret"
|
|
|
|
export HOST_PORT CONTAINER_NAME IMAGE_TAG
|
|
|
|
if ! docker ps -a --format '{{.Names}}' | grep -qx "${CONTAINER_NAME}"; then
|
|
prepare_image
|
|
fi
|
|
|
|
info "启动容器..."
|
|
"${COMPOSE[@]}" up -d --remove-orphans
|
|
ok "容器已启动: ${CONTAINER_NAME}"
|
|
echo
|
|
echo " API 地址: http://$(hostname -I 2>/dev/null | awk '{print $1}' || echo 127.0.0.1):${HOST_PORT}"
|
|
echo " 健康检查: http://127.0.0.1:${HOST_PORT}/health"
|
|
echo " API 文档: http://127.0.0.1:${HOST_PORT}/docs"
|
|
echo
|
|
wait_for_health || true
|
|
}
|
|
|
|
cmd_down() {
|
|
check_docker
|
|
export HOST_PORT CONTAINER_NAME IMAGE_TAG
|
|
info "停止容器..."
|
|
"${COMPOSE[@]}" down --remove-orphans
|
|
ok "已停止"
|
|
}
|
|
|
|
cmd_restart() {
|
|
cmd_down
|
|
cmd_up
|
|
}
|
|
|
|
cmd_status() {
|
|
check_docker
|
|
export HOST_PORT CONTAINER_NAME IMAGE_TAG
|
|
"${COMPOSE[@]}" ps
|
|
echo
|
|
if docker ps --format '{{.Names}}' | grep -qx "${CONTAINER_NAME}"; then
|
|
curl -fsS "http://127.0.0.1:${HOST_PORT}/health" 2>/dev/null && echo || warn "健康检查失败"
|
|
else
|
|
warn "容器未运行"
|
|
fi
|
|
}
|
|
|
|
cmd_logs() {
|
|
check_docker
|
|
export HOST_PORT CONTAINER_NAME IMAGE_TAG
|
|
"${COMPOSE[@]}" logs -f --tail=200 gateway
|
|
}
|
|
|
|
cmd_build() {
|
|
check_docker
|
|
ensure_layout
|
|
build_image
|
|
}
|
|
|
|
cmd_load() {
|
|
check_docker
|
|
DEPLOY_MODE=offline
|
|
load_offline_image
|
|
}
|
|
|
|
cmd_health() {
|
|
curl -fsS "http://127.0.0.1:${HOST_PORT}/health" && echo || fail "健康检查失败"
|
|
}
|
|
|
|
main() {
|
|
local cmd="${1:-up}"
|
|
case "${cmd}" in
|
|
-h|--help|help) usage ;;
|
|
up|start|"") cmd_up ;;
|
|
down|stop) cmd_down ;;
|
|
restart) cmd_restart ;;
|
|
status) cmd_status ;;
|
|
logs) cmd_logs ;;
|
|
build) cmd_build ;;
|
|
load) cmd_load ;;
|
|
health) cmd_health ;;
|
|
deploy) cmd_up ;;
|
|
*)
|
|
fail "未知命令: ${cmd}。运行 bash deploy.sh --help 查看帮助"
|
|
;;
|
|
esac
|
|
}
|
|
|
|
main "${1:-up}"
|