diff --git a/scripts/cloud-image/README.md b/scripts/cloud-image/README.md index 204450dae..5a2d1d0fa 100644 --- a/scripts/cloud-image/README.md +++ b/scripts/cloud-image/README.md @@ -156,8 +156,20 @@ sudo bash /opt/weknora-tools/scripts/cloud-image/prepare.sh # 想 pin 特定版本(推荐, 保证镜像可复现) sudo WEKNORA_REF=v0.5.0 bash /opt/weknora-tools/scripts/cloud-image/prepare.sh + +# 中国大陆 / Docker Hub 直连不通时,配合加速器(举例:腾讯云) +sudo \ + WEKNORA_REF=v0.5.0 \ + WEKNORA_GH_PROXY=https://gh-proxy.com/ \ + DOCKER_REGISTRY_MIRROR=https://mirror.ccs.tencentyun.com \ + bash /opt/weknora-tools/scripts/cloud-image/prepare.sh ``` +> `WEKNORA_GH_PROXY` 用于加速 GitHub tarball 下载(步骤二), +> `DOCKER_REGISTRY_MIRROR` 用于加速 Docker Hub 镜像拉取(步骤三/四)。 +> 不同云厂商加速器地址不同:腾讯云 `https://mirror.ccs.tencentyun.com`、 +> 阿里云 `https://.mirror.aliyuncs.com`、华为云 `https://.mirror.swr.myhuaweicloud.com`。 + `prepare.sh` 会: 1. 安装 Docker / Docker Compose plugin(已装则跳过) @@ -217,6 +229,13 @@ sudo WEKNORA_REF=v0.6.0 bash /opt/weknora-tools/scripts/cloud-image/prepare.sh sudo bash /opt/weknora-tools/scripts/cloud-image/cleanup.sh # 制作新镜像前 ``` +> **打新镜像时想顺便清掉旧版本镜像层**(每个旧 weknora-* tag 几百 MB,4 个镜像 ~2-4GB): +> ```bash +> sudo PRUNE_OLD_IMAGES=true WEKNORA_REF=v0.6.0 \ +> bash /opt/weknora-tools/scripts/cloud-image/prepare.sh +> ``` +> 默认 `false` 是为了保留回滚路径。打镜像前确认新版本稳定后再开。 + ## 安全注意事项 - 镜像里**不要**预置任何 LLM API Key、Langfuse Key、个人 SSH key diff --git a/scripts/cloud-image/cleanup.sh b/scripts/cloud-image/cleanup.sh index d899449d4..fa07df41f 100755 --- a/scripts/cloud-image/cleanup.sh +++ b/scripts/cloud-image/cleanup.sh @@ -30,8 +30,11 @@ fi echo "[cleanup] 2/8 清空 WeKnora 业务数据 + 首启 marker / 日志" if [[ -d "${WEKNORA_DIR}" ]]; then rm -rf "${WEKNORA_DIR}/data"/* "${WEKNORA_DIR}/logs"/* 2>/dev/null || true - rm -f "${WEKNORA_DIR}/.env" "${WEKNORA_DIR}/.firstboot.done" - cp "${WEKNORA_DIR}/.env.example" "${WEKNORA_DIR}/.env" + # 故意不在这里重建 .env: 让镜像里 .env 缺失, 任何在 firstboot 之前 + # 起来的 docker compose 都会因找不到 .env 而失败, 避免明文默认密码 + # (postgres123!@# 之类) 把 postgres 数据卷初始化坏。 + # firstboot.sh 自己会从 .env.example 拷贝并替换密钥。 + rm -f "${WEKNORA_DIR}/.env" "${WEKNORA_DIR}/.firstboot.done" fi rm -f /root/weknora-credentials.txt /var/log/weknora-firstboot.log diff --git a/scripts/cloud-image/firstboot.sh b/scripts/cloud-image/firstboot.sh index dc17e8cdd..5f95223be 100755 --- a/scripts/cloud-image/firstboot.sh +++ b/scripts/cloud-image/firstboot.sh @@ -13,11 +13,15 @@ set -euo pipefail WEKNORA_DIR="${WEKNORA_DIR:-/opt/WeKnora}" ENV_FILE="${WEKNORA_DIR}/.env" +ENV_TEMPLATE="${WEKNORA_DIR}/.env.example" CRED_FILE="/root/weknora-credentials.txt" LOG_FILE="/var/log/weknora-firstboot.log" MARKER="${WEKNORA_DIR}/.firstboot.done" -exec >>"${LOG_FILE}" 2>&1 +# 提前打开 LOG_FILE, 同时把 stderr 也复制一份到 systemd journal 方便调试 +# (单纯 exec >> LOG_FILE 时, 脚本一开始的失败会因为 stderr 已被吞掉而看不到) +mkdir -p "$(dirname "${LOG_FILE}")" +exec > >(tee -a "${LOG_FILE}") 2>&1 echo "==== firstboot started at $(date -Iseconds) ====" if [[ -f "${MARKER}" ]]; then @@ -25,9 +29,17 @@ if [[ -f "${MARKER}" ]]; then exit 0 fi +# cleanup.sh 不再保留 .env, 这里从 .env.example 拷贝模板再做替换。 +# 这样保证 firstboot 之前不会有任何含明文默认密码的 .env 让 weknora.service +# 抢先把 postgres 数据卷用错的密码初始化掉。 if [[ ! -f "${ENV_FILE}" ]]; then - echo "ERROR: ${ENV_FILE} not found" - exit 1 + if [[ -f "${ENV_TEMPLATE}" ]]; then + echo "creating ${ENV_FILE} from ${ENV_TEMPLATE}" + cp "${ENV_TEMPLATE}" "${ENV_FILE}" + else + echo "ERROR: neither ${ENV_FILE} nor ${ENV_TEMPLATE} found" + exit 1 + fi fi DOCKER_BIN="$(command -v docker || true)" @@ -37,9 +49,12 @@ if [[ -z "${DOCKER_BIN}" ]]; then fi # 生成 32 字节强随机字符串(用于 AES-256 key, 必须刚好 32 字节) -gen32() { LC_ALL=C tr -dc 'A-Za-z0-9' /etc/docker/daemon.json </dev/null 2>&1 && break + sleep 1 + done +fi + echo "[prepare] 2/6 拉取 WeKnora 运行时文件 (ref=${WEKNORA_REF})" # 只下载实际需要的 4 个文件, 不 clone 整个仓库 (~MB 级 -> ~KB 级) mkdir -p "${WEKNORA_DIR}/config" "${WEKNORA_DIR}/skills" @@ -41,7 +91,9 @@ mkdir -p "${WEKNORA_DIR}/config" "${WEKNORA_DIR}/skills" tmp=$(mktemp -d) trap 'rm -rf "${tmp}"' EXIT -curl -fsSL "${WEKNORA_REPO}/archive/${WEKNORA_REF}.tar.gz" -o "${tmp}/repo.tar.gz" +tarball_url="${WEKNORA_GH_PROXY}${WEKNORA_REPO}/archive/${WEKNORA_REF}.tar.gz" +echo "[prepare] tarball: ${tarball_url}" +curl -fsSL "${tarball_url}" -o "${tmp}/repo.tar.gz" # 仅解压需要的路径, 显著加速且省空间 tar -xzf "${tmp}/repo.tar.gz" -C "${tmp}" \ --wildcards \ @@ -73,17 +125,17 @@ cd "${WEKNORA_DIR}" [[ -f .env ]] || cp .env.example .env sed -i 's/^GIN_MODE=.*/GIN_MODE=release/' .env || true -# 若 WEKNORA_REF 形如 v1.2.3, 把同名版本号写到 .env 的 WEKNORA_VERSION, -# 让 docker compose 拉取与 ref 对齐的镜像 tag, 避免 ref/image 版本错配。 -if [[ "${WEKNORA_REF}" =~ ^v[0-9] ]]; then - WEKNORA_VERSION_VAL="${WEKNORA_REF#v}" - if grep -qE '^WEKNORA_VERSION=' .env; then - sed -i "s|^WEKNORA_VERSION=.*|WEKNORA_VERSION=${WEKNORA_VERSION_VAL}|" .env - else - echo "WEKNORA_VERSION=${WEKNORA_VERSION_VAL}" >>.env - fi - echo "[prepare] -> WEKNORA_VERSION=${WEKNORA_VERSION_VAL}" +# 把 WEKNORA_VERSION 与 WEKNORA_REF 对齐, 让 docker compose 拉取与 ref 一致的 +# 镜像 tag。无条件覆盖, 避免 .env 残留上一次 prepare 留下的旧版本号。 +# Docker Hub 上 wechatopenai/weknora-* 的 tag 实际值就是 git ref 原样 +# (`main` / `v0.5.1`), 因此这里不剥 v、也不映射到 latest。 +WEKNORA_VERSION_VAL="${WEKNORA_REF}" +if grep -qE '^WEKNORA_VERSION=' .env; then + sed -i "s|^WEKNORA_VERSION=.*|WEKNORA_VERSION=${WEKNORA_VERSION_VAL}|" .env +else + echo "WEKNORA_VERSION=${WEKNORA_VERSION_VAL}" >>.env fi +echo "[prepare] -> WEKNORA_VERSION=${WEKNORA_VERSION_VAL}" echo "[prepare] 4/6 拉取并启动默认 5 个常驻容器 (frontend/app/docreader/postgres/redis)" docker compose pull @@ -98,6 +150,23 @@ docker compose --profile full pull sandbox || true # 不预拉, 体积可省 5-15GB. 用户如需启用: # cd /opt/WeKnora && docker compose --profile up -d +# 升级场景: 清理旧版本 tag 的 wechatopenai/weknora-* 镜像。 +# 默认关闭, 保留回滚路径; 制作镜像前显式打开以减小体积。 +# +# 注意: 不用 `docker image prune -af`! +# sandbox 镜像在 compose 里只 pull 不 up (Agent Skills 由 app 按需 docker run), +# 没有任何容器引用它, 一旦 `prune -a` 会把当前版本的 sandbox 一起删掉, +# 反而违背 prepare.sh 4.5 步预拉 sandbox 的目的。 +# 这里精确按 tag 比对, 只删 wechatopenai/weknora-* 仓库下、tag 不等于当前 +# WEKNORA_VERSION 的镜像, 基础设施镜像 (paradedb / redis) 不动。 +if [[ "${PRUNE_OLD_IMAGES,,}" == "true" || "${PRUNE_OLD_IMAGES}" == "1" ]]; then + echo "[prepare] 4.6/6 清理 wechatopenai/weknora-* 仓库下旧版本镜像 (PRUNE_OLD_IMAGES=true, keep=${WEKNORA_VERSION_VAL})" + docker image ls --format '{{.Repository}}:{{.Tag}}' \ + | grep -E '^wechatopenai/weknora-' \ + | grep -vE ":${WEKNORA_VERSION_VAL}\$" \ + | xargs -r docker rmi -f 2>/dev/null || true +fi + echo "[prepare] 5/6 安装 systemd 单元" # 探测 docker 二进制路径, 不同发行版可能在 /usr/bin 或 /usr/local/bin DOCKER_BIN="$(command -v docker)" diff --git a/scripts/cloud-image/systemd/weknora.service b/scripts/cloud-image/systemd/weknora.service index bfaf49d90..6fa837693 100644 --- a/scripts/cloud-image/systemd/weknora.service +++ b/scripts/cloud-image/systemd/weknora.service @@ -1,8 +1,13 @@ [Unit] Description=WeKnora (docker compose) -Requires=docker.service +Requires=docker.service weknora-firstboot.service After=docker.service network-online.target weknora-firstboot.service Wants=network-online.target +# 关键: Requires=weknora-firstboot.service 让首启 init 失败时本服务也不启动, +# 避免 firstboot 没成功生成随机密钥就用 .env.example 默认明文密码把 postgres +# 数据卷初始化掉。weknora-firstboot.service 在已初始化的实例上由 +# ConditionPathExists=!/opt/WeKnora/.firstboot.done 跳过, 跳过 = 成功, +# 不会阻塞本服务后续重启。 [Service] Type=oneshot