deploy.sh
Rama main · réplica de lectura del repositorio original.
#!/usr/bin/env bash
# OpenWorkBuddy 服务器一键部署(Docker)
#
# bash deploy.sh # 起在 127.0.0.1:3800,自己用 / 先试试
# bash deploy.sh --domain buddy.example.com # 带自动 HTTPS,直接对外能用
# bash deploy.sh --update # 拉最新代码,重建,重启
# bash deploy.sh --logs # 跟日志
# bash deploy.sh --down # 停掉(数据留着)
#
# 数据全在 ./openworkbuddy-data 一个目录里。容器随便删,那个目录别删。
#
# 这个脚本不装 Docker、不改防火墙、不动系统配置——那些得你自己点头。
# 缺什么它会告诉你怎么装。
set -euo pipefail
REPO_URL="${OPENWORKBUDDY_REPO:-https://github.com/CatCatUncle/openworkbuddy.git}"
DIR="${OPENWORKBUDDY_DIR:-$HOME/openworkbuddy}"
DOMAIN=""
ACTION="up"
BIND=""
PORT=""
say() { printf "\033[1;34m▸\033[0m %s\n" "$*"; }
ok() { printf "\033[1;32m✓\033[0m %s\n" "$*"; }
warn() { printf "\033[1;33m!\033[0m %s\n" "$*"; }
die() { printf "\033[1;31m✗ %s\033[0m\n" "$*" >&2; exit 1; }
while [ $# -gt 0 ]; do
case "$1" in
--domain) DOMAIN="${2:-}"; [ -n "$DOMAIN" ] || die "--domain 后面得跟域名"; shift 2 ;;
--domain=*) DOMAIN="${1#*=}"; shift ;;
--bind) BIND="${2:-}"; shift 2 ;;
--bind=*) BIND="${1#*=}"; shift ;;
--port) PORT="${2:-}"; shift 2 ;;
--port=*) PORT="${1#*=}"; shift ;;
--update) ACTION="update"; shift ;;
--logs) ACTION="logs"; shift ;;
--down) ACTION="down"; shift ;;
-h|--help) sed -n '2,/^$/p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
*) die "不认识的参数:$1(-h 看用法)" ;;
esac
done
# ---------- 1. 环境 ----------
command -v docker >/dev/null 2>&1 || die "没装 Docker。
Linux:curl -fsSL https://get.docker.com | sh
macOS:https://www.docker.com/products/docker-desktop/
装完记得 sudo usermod -aG docker \$USER 然后重新登录,不然每条命令都得 sudo。"
docker compose version >/dev/null 2>&1 || die "Docker 装了,但没有 compose 插件(docker compose)。
Debian/Ubuntu:sudo apt-get install -y docker-compose-plugin
或者升级到新版 Docker Desktop。"
docker info >/dev/null 2>&1 || die "Docker 守护进程没跑起来,或者当前用户没权限。
systemctl start docker / sudo usermod -aG docker \$USER 后重新登录"
ok "Docker $(docker version --format '{{.Server.Version}}' 2>/dev/null || echo '')"
# ---------- 2. 取代码 ----------
if [ -f "package.json" ] && grep -q '"name": "openworkbuddy"' package.json 2>/dev/null; then
DIR="$(pwd)"
say "就在当前目录部署:$DIR"
elif [ -d "$DIR/.git" ]; then
say "已存在 $DIR"
else
command -v git >/dev/null 2>&1 || die "没装 git"
say "克隆到 $DIR"
git clone --depth 1 "$REPO_URL" "$DIR"
fi
cd "$DIR"
[ -f docker-compose.yml ] || die "$DIR 里没有 docker-compose.yml,这不像是 OpenWorkBuddy 的目录"
# ---------- 3. .env ----------
if [ ! -f .env ]; then
cp deploy/env.example .env
ok "已生成 .env(照抄 deploy/env.example,没有任何密钥在里面)"
fi
# 把命令行参数写回 .env,这样下次不带参数跑也是同一套配置
setenv() { # setenv KEY VALUE
local k="$1" v="$2"
if grep -qE "^${k}=" .env; then
# BSD sed(macOS)和 GNU sed 的 -i 语法不一样,用临时文件绕开
awk -v k="$k" -v v="$v" 'BEGIN{FS=OFS="="} $1==k{print k "=" v; next} {print}' .env > .env.tmp && mv .env.tmp .env
else
printf '%s=%s\n' "$k" "$v" >> .env
fi
}
[ -n "$DOMAIN" ] && setenv OPENWORKBUDDY_DOMAIN "$DOMAIN"
[ -n "$BIND" ] && setenv OPENWORKBUDDY_BIND "$BIND"
[ -n "$PORT" ] && setenv OPENWORKBUDDY_PORT "$PORT"
# 走 --domain 就是我们自己在前面起 caddy,那这一层反代是确定存在的,直接把开关打开。
# 不打开的话:所有请求在应用看来都来自 caddy 那一个 IP,注册限流 5 次/15 分钟
# 会变成「第 6 个同事注册不了」,登录限流会变成「有人连错几次密码全公司一起进不去」。
# 只在 --domain 时自动填,是因为「填了却没反代」比「没填」更糟——那等于把闸拆给外网。
[ -n "$DOMAIN" ] && setenv OPENWORKBUDDY_TRUST_PROXY 1
# 读回最终值(.env 是纯 KEY=VALUE,可以直接 source)
set -a; . ./.env; set +a
OPENWORKBUDDY_DATA="${OPENWORKBUDDY_DATA:-./openworkbuddy-data}"
OPENWORKBUDDY_BIND="${OPENWORKBUDDY_BIND:-127.0.0.1}"
OPENWORKBUDDY_PORT="${OPENWORKBUDDY_PORT:-3800}"
OPENWORKBUDDY_DOMAIN="${OPENWORKBUDDY_DOMAIN:-}"
OPENWORKBUDDY_TRUST_PROXY="${OPENWORKBUDDY_TRUST_PROXY:-0}"
mkdir -p "$OPENWORKBUDDY_DATA"
# 空数组在 macOS 自带的 bash 3.2 + set -u 下直接展开会报 unbound variable,
# 所以下面一律写成 ${PROFILE[@]+"${PROFILE[@]}"}
PROFILE=()
[ -n "$OPENWORKBUDDY_DOMAIN" ] && PROFILE=(--profile https)
# ---------- 4. 干活 ----------
case "$ACTION" in
logs) exec docker compose ${PROFILE[@]+"${PROFILE[@]}"} logs -f ;;
down) docker compose ${PROFILE[@]+"${PROFILE[@]}"} down; ok "停了。数据还在 $OPENWORKBUDDY_DATA,下次 bash deploy.sh 接着用"; exit 0 ;;
update)
if [ -d .git ]; then say "拉最新代码"; git pull --ff-only || warn "拉取失败(本地有改动?),用现有代码继续"; fi
;;
esac
say "构建镜像(第一次要几分钟,之后有缓存就快了)"
docker compose ${PROFILE[@]+"${PROFILE[@]}"} build
say "启动"
docker compose ${PROFILE[@]+"${PROFILE[@]}"} up -d
# ---------- 5. 等它真的活过来 ----------
say "等健康检查通过…"
HEALTHY=0
for _ in $(seq 1 60); do
st="$(docker inspect --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}}' openworkbuddy 2>/dev/null || echo missing)"
case "$st" in
healthy) HEALTHY=1; break ;;
unhealthy) break ;;
esac
# 容器可能直接挂了,别傻等满 5 分钟
[ "$(docker inspect --format '{{.State.Running}}' openworkbuddy 2>/dev/null || echo false)" = "true" ] || break
sleep 5
done
if [ "$HEALTHY" != "1" ]; then
warn "没等到健康。最后 40 行日志:"
docker compose logs --tail=40 app || true
die "起不来。常见原因:端口被占(换 --port)、config.json 写坏了(删掉 $OPENWORKBUDDY_DATA/config.json 让它重建)。"
fi
if [ -n "$OPENWORKBUDDY_DOMAIN" ]; then URL="https://$OPENWORKBUDDY_DOMAIN"; else URL="http://$OPENWORKBUDDY_BIND:$OPENWORKBUDDY_PORT"; fi
cat <<EOF
$(ok "起来了:$URL")
下一步(这两条别拖,尤其第一条):
1. 现在就打开 $URL 注册第一个账号。
第一个注册的就是管理员,注册完之后默认不再允许别人自建账号。
空着的实例挂在那儿,等于谁先访问谁是管理员。
2. 填模型 API Key(界面会引导),填完当场发一条真请求验活,通过才存。
Key 存在 $OPENWORKBUDDY_DATA/config.json,不进镜像也不进 git。
管理员登录后,头像菜单 → 企业管理后台,可以建组织、分席位、看用量、
配组织级的安全策略(能不能跑命令行、能访问哪些域名、登录多久过期)。
常用命令:
bash deploy.sh --logs 跟日志
bash deploy.sh --update 拉代码 + 重建 + 重启
bash deploy.sh --down 停掉(数据留着)
EOF
if [ -z "$OPENWORKBUDDY_DOMAIN" ] && [ "$OPENWORKBUDDY_BIND" = "127.0.0.1" ]; then
cat <<'EOF'
现在只有这台机器自己连得上。要让别人也能用,二选一:
bash deploy.sh --domain buddy.example.com # 自带 HTTPS,推荐
自己配 nginx 反代 # 抄 deploy/README.md 里的配置
EOF
fi
if [ "$OPENWORKBUDDY_BIND" = "0.0.0.0" ] && [ -z "$OPENWORKBUDDY_DOMAIN" ]; then
warn "你把它直接挂在 0.0.0.0 上,而且没有 HTTPS。这个 agent 手里有 run_shell,"
warn "等于把这台机器的 shell 用明文 HTTP 挂到了公网。至少去 设置 → 安全 确认「安全网关」开着。"
fi