fix(cloud-image): enhance firstboot and cleanup scripts for improved security and functionality

- Updated cleanup.sh to avoid recreating .env during cleanup, preventing exposure of default passwords before firstboot.
- Modified firstboot.sh to create .env from .env.example only if it doesn't exist, ensuring no sensitive data is present before initialization.
- Added support for Docker Hub and GitHub tarball download acceleration via new environment variables WEKNORA_GH_PROXY and DOCKER_REGISTRY_MIRROR.
- Implemented a mechanism to prune old WeKnora images based on the current version, reducing image size and maintaining a clean environment.
- Enhanced README.md with instructions for using the new acceleration features and image pruning options.
This commit is contained in:
wizardchen
2026-05-11 15:21:28 +08:00
committed by lyingbug
parent 0eb95ccbd8
commit 0cfbad7f97
5 changed files with 145 additions and 22 deletions
+19
View File
@@ -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://<your-id>.mirror.aliyuncs.com`、华为云 `https://<id>.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 几百 MB4 个镜像 ~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
+5 -2
View File
@@ -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
+32 -5
View File
@@ -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' </dev/urandom | head -c 32; }
# 用 `() ... ()` 启子 shell 并关闭 pipefail: head 只读 N 字节后关闭 stdin,
# tr 会收到 SIGPIPE (退出码 141), 在 `set -o pipefail` 下整条管道被判失败,
# 触发顶层 `set -e` 把 firstboot.sh 8ms exit 1 干掉。
gen32() ( set +o pipefail; LC_ALL=C tr -dc 'A-Za-z0-9' </dev/urandom | head -c 32 )
# 通用密码: 24 字符, 不含 / + = (避免出现在 URL / sed 替换时出问题)
genpw() { LC_ALL=C tr -dc 'A-Za-z0-9' </dev/urandom | head -c 24; }
genpw() ( set +o pipefail; LC_ALL=C tr -dc 'A-Za-z0-9' </dev/urandom | head -c 24 )
DB_PWD=$(genpw)
REDIS_PWD=$(genpw)
@@ -64,6 +79,18 @@ replace SYSTEM_AES_KEY "${SYS_AES}"
replace TENANT_AES_KEY "${TENANT_AES}"
replace GIN_MODE "release"
# 把 prepare.sh 阶段记录在 .cloud-image-meta 里的 WEKNORA_REF 还原为 .env 的
# WEKNORA_VERSION, 否则 docker compose 会落回 :latest 默认值, 导致镜像版本
# 与 prepare 时拉取的版本不一致。
META_FILE="${WEKNORA_DIR}/.cloud-image-meta"
if [[ -f "${META_FILE}" ]]; then
META_REF=$(grep -E '^WEKNORA_REF=' "${META_FILE}" | tail -1 | cut -d= -f2- || true)
if [[ -n "${META_REF}" ]]; then
replace WEKNORA_VERSION "${META_REF}"
echo "restored WEKNORA_VERSION=${META_REF} from ${META_FILE}"
fi
fi
# 关键: .env 改完立刻写 marker。
# 这之后即便 docker compose up 失败, 重启也不会再次重写 .env,
# 防止与 postgres 已经持久化的密码不一致。
+83 -14
View File
@@ -4,14 +4,28 @@
# 兼容: Ubuntu / Debian / CentOS / Rocky / TencentOS 等带 systemd + Docker 的发行版。
# 使用方式: sudo bash prepare.sh
# 可调环境变量:
# WEKNORA_REF 要拉取的 git ref (tag / branch / commit), 默认 main
# WEKNORA_DIR 部署目录, 默认 /opt/WeKnora
# WEKNORA_REPO 仓库地址, 默认 https://github.com/Tencent/WeKnora
# WEKNORA_REF 要拉取的 git ref (tag / branch / commit), 默认 main
# WEKNORA_DIR 部署目录, 默认 /opt/WeKnora
# WEKNORA_REPO 仓库地址, 默认 https://github.com/Tencent/WeKnora
# WEKNORA_GH_PROXY GitHub 加速前缀, 默认空。中国大陆机器可设
# https://gh-proxy.com/ 或 https://ghfast.top/
# (实际下载地址变成 ${WEKNORA_GH_PROXY}${WEKNORA_REPO}/archive/...)
# DOCKER_REGISTRY_MIRROR Docker Hub 加速器, 默认空。腾讯云内网可设
# https://mirror.ccs.tencentyun.com
# (会写入 /etc/docker/daemon.json 并重启 docker)
# PRUNE_OLD_IMAGES 升级场景下是否清理 dangling / 旧版本 tag 镜像,
# 默认 false。设为 true 时在拉新镜像之后执行
# `docker image prune -af`, 把没有容器引用的镜像
# (含旧 WEKNORA_VERSION 的 wechatopenai/weknora-*)
# 一次性删掉, 减少要打进云镜像的体积。
set -euo pipefail
WEKNORA_REF="${WEKNORA_REF:-main}"
WEKNORA_DIR="${WEKNORA_DIR:-/opt/WeKnora}"
WEKNORA_REPO="${WEKNORA_REPO:-https://github.com/Tencent/WeKnora}"
WEKNORA_GH_PROXY="${WEKNORA_GH_PROXY:-}"
DOCKER_REGISTRY_MIRROR="${DOCKER_REGISTRY_MIRROR:-}"
PRUNE_OLD_IMAGES="${PRUNE_OLD_IMAGES:-false}"
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
if [[ "${EUID}" -ne 0 ]]; then
@@ -34,6 +48,42 @@ if ! docker compose version >/dev/null 2>&1; then
fi
fi
# 可选: 配置 Docker Hub 加速器, 解决直连 registry-1.docker.io 超时的场景
# (典型: 中国大陆云主机 / 内网受限环境)。仅在用户显式传入时才动 daemon.json。
if [[ -n "${DOCKER_REGISTRY_MIRROR}" ]]; then
echo "[prepare] 1.5/6 配置 Docker Hub 加速器: ${DOCKER_REGISTRY_MIRROR}"
mkdir -p /etc/docker
# 已存在的 daemon.json 走 python 合并, 避免覆盖用户其它配置
if [[ -s /etc/docker/daemon.json ]] && command -v python3 >/dev/null 2>&1; then
python3 - "$DOCKER_REGISTRY_MIRROR" <<'PY'
import json, sys, pathlib
p = pathlib.Path("/etc/docker/daemon.json")
mirror = sys.argv[1]
try:
cfg = json.loads(p.read_text())
except Exception:
cfg = {}
mirrors = cfg.get("registry-mirrors") or []
if mirror not in mirrors:
mirrors.insert(0, mirror)
cfg["registry-mirrors"] = mirrors
p.write_text(json.dumps(cfg, indent=2) + "\n")
PY
else
cat >/etc/docker/daemon.json <<EOF
{
"registry-mirrors": ["${DOCKER_REGISTRY_MIRROR}"]
}
EOF
fi
systemctl restart docker
# 给 docker daemon 一点重启时间, 避免下面 docker compose pull 立刻拿到 EOF
for _ in 1 2 3 4 5; do
docker info >/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 <name> 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)"
+6 -1
View File
@@ -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