119 lines
4.2 KiB
Bash
Executable File
119 lines
4.2 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
# 部门关键任务追踪看板 · Docker 部署脚本
|
||
# 用法: ./deploy.sh up 构建镜像并后台启动(默认命令)
|
||
# ./deploy.sh update 改完代码后重建镜像并替换容器
|
||
# ./deploy.sh restart 重启容器(不换镜像)
|
||
# ./deploy.sh down 停止并删除容器,数据留在 ./data
|
||
# ./deploy.sh logs 跟踪日志(Ctrl+C 退出)
|
||
# ./deploy.sh backup 导出数据库快照(部署到新机器时带走数据)
|
||
# ./deploy.sh status 容器状态 + 健康检查 + 访问地址
|
||
set -euo pipefail
|
||
cd "$(dirname "$0")"
|
||
|
||
CONTAINER=unis-board
|
||
|
||
# ---------- 0. 前置检查 ----------
|
||
command -v docker >/dev/null 2>&1 || { echo "!! 没有 docker 命令,请先安装 Docker / Docker Desktop" >&2; exit 1; }
|
||
docker info >/dev/null 2>&1 || { echo "!! docker 守护进程没起来(Docker Desktop 是否没运行?)" >&2; exit 1; }
|
||
|
||
if docker compose version >/dev/null 2>&1; then
|
||
COMPOSE=(docker compose)
|
||
elif command -v docker-compose >/dev/null 2>&1; then
|
||
COMPOSE=(docker-compose)
|
||
else
|
||
echo "!! 没找到 docker compose,也没找到 docker-compose" >&2
|
||
exit 1
|
||
fi
|
||
|
||
# 对外端口:命令行环境变量 > .env 里的 HOST_PORT > 8770
|
||
if [ -z "${HOST_PORT:-}" ] && [ -f .env ]; then
|
||
HOST_PORT="$(sed -n 's/^HOST_PORT=//p' .env | tail -1)"
|
||
fi
|
||
HOST_PORT="${HOST_PORT:-8770}"
|
||
URL="http://127.0.0.1:${HOST_PORT}"
|
||
|
||
# ---------- 1. 准备 .env 与 data 目录 ----------
|
||
prepare() {
|
||
if [ ! -f .env ]; then
|
||
cp .env.example .env
|
||
echo "==> 已从 .env.example 生成 .env(AI Key、管理员账号密码请按需修改,改完 restart 生效)"
|
||
fi
|
||
# 先建好目录,免得 Docker 用 root 建出来盖掉宿主机权限
|
||
mkdir -p data/uploads data/exports
|
||
}
|
||
|
||
# ---------- 2. 等服务就绪 ----------
|
||
wait_ready() {
|
||
echo -n "==> 等待服务就绪 "
|
||
for _ in $(seq 1 30); do
|
||
state="$(docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}' "$CONTAINER" 2>/dev/null || echo missing)"
|
||
case "$state" in
|
||
healthy) echo; echo "==> 已就绪:$URL"; return 0 ;;
|
||
unhealthy) echo; echo "!! 容器不健康,执行 ./deploy.sh logs 看原因" >&2; return 1 ;;
|
||
esac
|
||
echo -n "."
|
||
sleep 2
|
||
done
|
||
echo; echo "!! 等待超时(当前状态:$state),执行 ./deploy.sh logs 看原因" >&2
|
||
return 1
|
||
}
|
||
|
||
# ---------- 3. 分发命令 ----------
|
||
case "${1:-up}" in
|
||
up|start)
|
||
first_run=0
|
||
[ -f data/board.db ] || first_run=1
|
||
prepare
|
||
"${COMPOSE[@]}" up -d --build
|
||
wait_ready
|
||
echo " API 文档:$URL/api/docs"
|
||
if [ "$first_run" = "1" ]; then
|
||
echo " 首次启动:管理员账号取自 .env 的 ADMIN_USERNAME/ADMIN_PASSWORD(默认 admin / admin123,请登录后立刻改密码)"
|
||
fi
|
||
;;
|
||
update)
|
||
prepare
|
||
"${COMPOSE[@]}" up -d --build --force-recreate
|
||
wait_ready
|
||
;;
|
||
restart)
|
||
"${COMPOSE[@]}" restart
|
||
wait_ready
|
||
;;
|
||
down|stop)
|
||
"${COMPOSE[@]}" down
|
||
echo "==> 已停止(数据仍在 ./data,要备份请复制 data/board.db)"
|
||
;;
|
||
logs)
|
||
exec "${COMPOSE[@]}" logs -f --tail=100
|
||
;;
|
||
backup)
|
||
# 用 SQLite 备份 API 取一致性快照:服务正在写也不会拷到半截页
|
||
[ -f data/board.db ] || { echo "!! 没找到 data/board.db(若已换 MySQL,请改用 mysqldump)" >&2; exit 1; }
|
||
PYBIN="$(command -v python3 || command -v python || true)"
|
||
[ -n "$PYBIN" ] || { echo "!! 备份需要 python3(只拷库,不用装项目依赖)" >&2; exit 1; }
|
||
mkdir -p data/backups
|
||
OUT="data/backups/board-$(date +%Y%m%d_%H%M%S).db"
|
||
"$PYBIN" - "$OUT" <<'PYEOF'
|
||
import os, sqlite3, sys
|
||
src = sqlite3.connect("data/board.db")
|
||
dst = sqlite3.connect(sys.argv[1])
|
||
with dst:
|
||
src.backup(dst)
|
||
print(f"[ok] 已导出 {sys.argv[1]}({os.path.getsize(sys.argv[1]) // 1024} KB)")
|
||
PYEOF
|
||
echo "==> 部署到新机器:把这个文件放到目标机的 data/board.db,再执行 ./deploy.sh up"
|
||
;;
|
||
status)
|
||
"${COMPOSE[@]}" ps
|
||
echo -n "==> /health:"
|
||
curl -s --max-time 3 "$URL/health" 2>/dev/null || echo " 无响应(容器未启动?./deploy.sh up)"
|
||
echo
|
||
echo "==> 访问地址:$URL"
|
||
;;
|
||
*)
|
||
sed -n '3,9p' "$0"
|
||
exit 1
|
||
;;
|
||
esac
|