#!/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 <