From 9556a30747d6602c4c3d4e0e5c3989bd597e9777 Mon Sep 17 00:00:00 2001 From: "mula.liu" Date: Thu, 24 Sep 2026 17:05:48 +0800 Subject: [PATCH] v 0.0.1 --- .env | 2 - .env.example | 11 +++-- Dockerfile | 6 +-- README.md | 20 ++++++---- deploy.sh | 111 +++++++++++++++++++++++---------------------------- 5 files changed, 72 insertions(+), 78 deletions(-) diff --git a/.env b/.env index 1d10a27..103dbfa 100644 --- a/.env +++ b/.env @@ -10,5 +10,3 @@ AI_MODEL=deepseek-chat AI_TEMPERATURE=0.2 PORT=8770 -PIP_INDEX_URL=https://pypi.tuna.tsinghua.edu.cn/simple -BASE_IMAGE=docker.1ms.run/library/python:3.12-slim diff --git a/.env.example b/.env.example index 28ad250..a40336d 100644 --- a/.env.example +++ b/.env.example @@ -26,10 +26,13 @@ HOST_PORT=8770 # 周期/周次/入库时间等日期按此时区计算,别留 UTC,否则整体差 8 小时 TZ=Asia/Shanghai -# ---- 国内网络加速(构建镜像用,deploy.sh 首次 up 时自动探测并写回这里)---- -# 国内直连 docker.io / pypi.org 基本超时,deploy.sh 会先探一个能真拉下镜像的加速站。 -# 手工钉死(推荐内网环境写死,省掉每次探测): -# BASE_IMAGE=docker.1ms.run/library/python:3.12-slim +# ---- 国内网络加速(只在构建镜像时用到)---- +# 基础镜像:优先用本地已有的镜像,脚本不会联网去试各种加速站。 +# 先在机器上准备好 python:3.12-slim(docker pull / 内网仓库 / docker load)即可; +# 本地是别的名字或别的 tag,就在下面钉死。别把某台机器上的前缀提交进仓库,换台机器就找不到了。 +# BASE_IMAGE=python:3.12-slim +# pip 源:构建时在容器里装依赖,容器不认宿主机的代理,只能换源; +# ./deploy.sh 会在容器里真访问一次挑可用的(默认清华源),想固定就取消注释: # PIP_INDEX_URL=https://pypi.tuna.tsinghua.edu.cn/simple # 关掉自动探测(源已在上面配好,或机器能直连官方源):CN_MIRROR=0 ./deploy.sh up # 跳过构建、直接用本地已有镜像(离线机器上 docker load 完之后用):NO_BUILD=1 ./deploy.sh up diff --git a/Dockerfile b/Dockerfile index 4f27c35..46a0621 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,9 +6,9 @@ # 镜像里只有依赖和代码,数据库/附件都在 data/,靠 bind mount 挂进来。 # data/ 已被 .dockerignore 排除,不会把本机 board.db 打进镜像。 # -# 基础镜像。国内直连 docker.io 会在 load metadata 阶段 TLS 超时,所以支持换仓库前缀: -# docker build --build-arg BASE_IMAGE=docker.1ms.run/library/python:3.12-slim . -# ./deploy.sh up 会自动探测可用加速站并把结果写进 .env,正常情况下不用手工传。 +# 基础镜像。国内直连 docker.io 会在 load metadata 阶段 TLS 超时,所以支持指成本地已有的镜像: +# docker pull python:3.12-slim && docker build --build-arg BASE_IMAGE=python:3.12-slim . +# ./deploy.sh 会优先用本地已有的同名镜像,不会去联网试各种加速站。 ARG BASE_IMAGE=python:3.12-slim FROM ${BASE_IMAGE} diff --git a/README.md b/README.md index e847dd1..8fe26a5 100644 --- a/README.md +++ b/README.md @@ -110,7 +110,7 @@ cd /path/to/unis_manager |---|---| | `Dockerfile` | 运行镜像:装依赖 + 拷 `app/ web/ scripts/`;启动时先跑 `init_db.py`(幂等)再起 uvicorn | | `docker-compose.yml` | 编排:`./data` 挂进容器存数据库、`HOST_PORT` 控对外端口、`restart: unless-stopped` 随 Docker 自启 | -| `deploy.sh` | 命令封装:`up` / `update` / `restart` / `down` / `logs` / `status` / `backup` / `mirror`,顺带检查 docker 是否在跑、`.env` 与 `data/` 是否存在,并在构建前探测国内可用源 | +| `deploy.sh` | 命令封装:`up` / `update` / `restart` / `down` / `logs` / `status` / `backup` / `mirror`,顺带检查 docker 是否在跑、`.env` 与 `data/` 是否存在,并在构建前定好基础镜像与 pip 源 | | `.dockerignore` | 保证 `data/`、`.env`、`.git` 不会被烤进镜像 | | 场景 | 命令 | @@ -131,21 +131,27 @@ cd /path/to/unis_manager | 卡在哪 | 典型报错 | 治法 | |---|---|---| -| ① 守护进程拉基础镜像 | `failed to resolve source metadata for docker.io/library/python:3.12-slim: ... TLS handshake timeout` | 换仓库前缀 `BASE_IMAGE`(走加速站,或给守护进程配 registry-mirrors) | +| ① 守护进程拉基础镜像 | `failed to resolve source metadata for docker.io/library/python:3.12-slim: ... TLS handshake timeout` | 用本地已有的镜像(`BASE_IMAGE=` 指定),或先 `docker pull` / 给守护进程配 registry-mirrors | | ② 构建容器里装依赖 | `[4/8] RUN pip install ... -i https://pypi.org/simple` 非零退出 | 换 pip 源 `PIP_INDEX_URL`(清华 / 阿里 / 腾讯) | -`./deploy.sh up` 在构建前会自动把两道源都探出来(判据是真的 `docker pull`、是在容器里 `urlopen`,不信宿主机 curl,也不信 `docker manifest inspect` 的假 `denied`),结果写进 `.env`,下次以及裸跑 compose 都复用: +`./deploy.sh up` 构建前把这两道都定下来,原则是**能本地解决就不联网**: + +- 基础镜像只看本地有没有:`BASE_IMAGE` 指定的镜像在本地 → 直接用;只有同名 tag(如 `python:3.12-slim`)→ 也用(仓库前缀只是当初从哪儿拉的,镜像是同一份)。本地确实没有才试拉一次(守护进程配的 registry-mirrors 在这一步生效),还拉不到就**直接报错停下**并打印补救命令——不去逐个试加速站,那种循环只会白等几分钟。 +- pip 源:在构建用的容器里真 `urlopen` 一次逐个试(容器网络 = 构建网络,比宿主机 curl 准),默认清华源。 +- 定下来的值只 export 给本次构建,**不写回 `.env`**(`.env` 在 git 里,把某台机器的地址带到另一台机器正是故障源)。 ```bash -./deploy.sh up # 自动探测 → 写 .env → 构建 → 等健康检查 -./deploy.sh mirror # 只想重探一遍源(不会冲掉 .env 里手工钉死的 BASE_IMAGE) +./deploy.sh up # 定源(本地有镜像就不联网)→ 构建 → 等健康检查 +./deploy.sh mirror # 重探一次 pip 源,并打印可写进 .env 的两行 ``` +> **别把某台机器上的 `BASE_IMAGE` 前缀提交进仓库。** 换机器后那个前缀对应的镜像本地不存在,构建就会卡在 `load metadata`;这行留空或写裸 `python:3.12-slim`,让每台机器自己的镜像说了算。 + 不想用脚本,等价的两条 build-arg: ```bash docker compose build \ - --build-arg BASE_IMAGE=docker.1ms.run/library/python:3.12-slim \ + --build-arg BASE_IMAGE=python:3.12-slim \ --build-arg PIP_INDEX_URL=https://pypi.tuna.tsinghua.edu.cn/simple docker compose up -d ``` @@ -154,7 +160,7 @@ docker compose up -d - **基础镜像拉成功 ≠ 构建能过**:`docker pull` 只治①,容器里的 `pip install` 照旧撞 pypi.org,必须另外给②换源。 - **shell 里的 `HTTP(S)_PROXY` 对构建没用**:Docker 守护进程和构建容器都不读宿主机的代理变量。要走代理得在 Docker Desktop → Settings → Resources → Proxies 里配 `http://host.docker.internal:7897`(端口换成你本机代理的)。 -- **境外机器 / 内网有自建源**:`CN_MIRROR=0 ./deploy.sh up` 关掉探测,源完全由 `.env` 说了算。 +- **境外机器 / 内网有自建源**:`CN_MIRROR=0 ./deploy.sh up` 跳过 pip 探测,源完全由 `.env` 说了算。 - **目标机完全不能上网**:走离线路。`docker save -o unis-board.tar unis-board:latest` → 拷过去 `docker load -i unis-board.tar` → `NO_BUILD=1 ./deploy.sh up`(跳过构建直接用本地镜像)。 - 构建失败时脚本会自己打印一份排查清单(`deploy.sh` 的 `build_hint`),照抄命令即可。 diff --git a/deploy.sh b/deploy.sh index 11b3ee3..f92efbd 100755 --- a/deploy.sh +++ b/deploy.sh @@ -7,7 +7,7 @@ # ./deploy.sh logs 跟踪日志(Ctrl+C 退出) # ./deploy.sh backup 导出数据库快照(部署到新机器时带走数据) # ./deploy.sh status 容器状态 + 健康检查 + 访问地址 -# ./deploy.sh mirror 重新探测国内加速源(基础镜像 + pip 源)并写进 .env +# ./deploy.sh mirror 重新探测可用的 pip 源(基础镜像只认本地已有的,不去试加速站) # 环境变量:CN_MIRROR=0 跳过加速探测;NO_BUILD=1 不构建、直接用本地镜像 set -euo pipefail cd "$(dirname "$0")" @@ -37,26 +37,19 @@ URL="http://127.0.0.1:${HOST_PORT}" # ---------- 0.5 通用小工具(供后面读写 .env、探测、加超时用)---------- env_get() { { [ -f .env ] && sed -n "s/^$1=//p" .env | tail -1; } || true; } -# 覆盖式写回 .env(没有这个键就追加) -env_set() { - awk -v k="$1" -v v="$2" 'BEGIN{hit=0} index($0,k"=")==1{print k"="v; hit=1; next} {print} END{if(!hit)print k"="v}' .env > .env.tmp && mv .env.tmp .env -} - -# 站点可达?能握手且不是 5xx 就算数(401/403 只是不允许匿名列表) -http_ok() { - local code - code="$(curl -s -o /dev/null -w '%{http_code}' --connect-timeout 3 --max-time "${2:-8}" "$1" 2>/dev/null || true)" - case "$code" in 000|"") return 1 ;; 5*) return 1 ;; *) return 0 ;; esac -} - # with_timeout 秒 命令…(macOS 没有 timeout 命令,自己搭一个,防止探测卡死) with_timeout() { - local t="$1" rc=0 pid wpid; shift - "$@" >/dev/null 2>&1 & pid=$! - ( sleep "$t"; kill "$pid" 2>/dev/null ) >/dev/null 2>&1 & wpid=$! - wait "$pid" || rc=$? - kill "$wpid" 2>/dev/null || true; wait "$wpid" 2>/dev/null || true - return "$rc" + local t="$1" rc=0; shift + # 整个动作丢进子 shell 并把它的 stderr 关掉:否则后台任务被 kill 时 + # bash 会往终端插一行「已终止」,看着像脚本自己挂了。 + ( "$@" >/dev/null 2>&1 & + pid=$! + ( sleep "$t"; kill "$pid" 2>/dev/null ) >/dev/null 2>&1 & + wpid=$! + wait "$pid" || rc=$? + kill "$wpid" 2>/dev/null || true + wait "$wpid" 2>/dev/null || true + exit "$rc" ) 2>/dev/null } # ---------- 1. 国内加速:基础镜像 + pip 源 ---------- @@ -65,63 +58,56 @@ with_timeout() { # 并写回 .env —— 之后裸跑 docker compose up -d --build 用的也是同一套配置。 BASE_IMAGE="${BASE_IMAGE:-$(env_get BASE_IMAGE)}" -BASE_IMAGE="${BASE_IMAGE:-python:3.12-slim}" # 想换加速站就写进 .env,例:docker.1ms.run/library/python:3.12-slim -BASE_TAG="${BASE_TAG:-${BASE_IMAGE##*/}}" # 加速站上的固定路径是 library/ -MIRROR_HOSTS="${MIRROR_HOSTS:-docker.1ms.run docker.m.daocloud.io docker.1panel.live docker.xuanyuan.me}" +BASE_IMAGE="${BASE_IMAGE:-python:3.12-slim}" # 本地有哪个 python 镜像就用哪个;内网自建仓库才需要在这里钉死 PIP_URLS="${PIP_URLS:-https://pypi.tuna.tsinghua.edu.cn/simple https://mirrors.aliyun.com/pypi/simple https://mirrors.cloud.tencent.com/pypi/simple https://pypi.org/simple}" configure_mirror() { - local force="${1:-}" host url ref tag pip_picked=0 base_picked=0 + local force="${1:-}" url tag PIP_INDEX_URL="${PIP_INDEX_URL:-$(env_get PIP_INDEX_URL)}" if [ "$force" = "--force" ]; then PIP_INDEX_URL="" # 手工钉死过的基础镜像不冲掉,只重探 pip 源 - if [ -z "$(env_get BASE_IMAGE)" ]; then BASE_IMAGE="python:3.12-slim"; fi # .env 里钉死过的基础镜像不冲掉 + # .env 里钉死过的基础镜像不冲掉,--force 只重探 pip 源 + if [ -z "$(env_get BASE_IMAGE)" ]; then BASE_IMAGE="python:3.12-slim"; fi fi BASE_IMAGE="${BASE_IMAGE:-python:3.12-slim}" if [ "${CN_MIRROR:-1}" = "0" ] || [ "${NO_BUILD:-0}" = "1" ]; then export BASE_IMAGE PIP_INDEX_URL - echo "==> 跳过加速探测(CN_MIRROR=0 或 NO_BUILD=1):${BASE_IMAGE:-官方 python} / ${PIP_INDEX_URL:-官方 pypi}" + echo "==> 跳过源探测(CN_MIRROR=0 或 NO_BUILD=1):${BASE_IMAGE:-官方 python} / ${PIP_INDEX_URL:-官方 pypi}" return 0 fi - # 基础镜像的判据是「Docker 守护进程能不能拿到它」,不是宿主机 curl 能不能通: - # 宿主机 curl 走 HTTP_PROXY,守护进程和构建容器都不认 shell 的代理变量。 - tag="${BASE_IMAGE##*/}" + # 基础镜像只用「本地有没有」做判据,不去逐个试加速站(试一圈要几分钟,而且多半全挂)。 + # 判据也不看宿主机 curl:宿主机走 HTTP_PROXY,守护进程和构建容器都不认 shell 的代理变量。 + tag="${BASE_IMAGE##*/}" # 纯 tag,例 python:3.12-slim if docker image inspect "$BASE_IMAGE" >/dev/null 2>&1; then echo "==> 基础镜像本地已有,不联网:$BASE_IMAGE" + elif docker image inspect "$tag" >/dev/null 2>&1; then + # 本地已有同名 tag 就直接用。镜像内容是同一份,仓库前缀只是「当初从哪儿拉的」, + # 换台机器还抱着旧前缀去联网拉,反而会把一次本来能成的构建搞挂。 + echo "==> 本地已有 ${tag},直接用它(忽略 .env 里的仓库前缀)" + BASE_IMAGE="$tag" elif with_timeout 25 docker pull --quiet "$BASE_IMAGE"; then - echo "==> 基础镜像可从当前配置源拉取:$BASE_IMAGE" + echo "==> 基础镜像已从当前配置的源拉取:$BASE_IMAGE" else - echo " $BASE_IMAGE 守护进程侧拉不到,开始找加速站" - for host in $MIRROR_HOSTS; do - ref="$host/library/$tag" - echo -n "==> 测试加速站 $host " - if with_timeout 120 docker pull --quiet "$ref"; then - BASE_IMAGE="$ref"; base_picked=1; echo " 可用"; break - else - echo " 不可用" - fi - done - if [ "$base_picked" = 0 ]; then - echo "!! 所有加速站都拉不到 $tag;可以先在服务器上手拉,再 ./deploy.sh up(本地有就不会再联网):" >&2 - echo " docker pull docker.1ms.run/library/$tag && docker tag docker.1ms.run/library/$tag $BASE_IMAGE" >&2 - fi + echo "!! 本地没有 ${BASE_IMAGE},25 秒内也没拉到;这一步过不去,构建必然失败,先解决基础镜像:" >&2 + echo " 1) 本地有别的 python 镜像 → 指定它:BASE_IMAGE=$(docker images --format '{{.Repository}}:{{.Tag}}' | grep -m1 python || echo python:3.12-slim) ./deploy.sh up" >&2 + echo " 2) 手工拉一次(守护进程配的 registry-mirrors 在这一步生效):docker pull $tag" >&2 + echo " 3) 机器不能上网 → 从别处拷:docker save -o py.tar $tag && scp py.tar 目标机 && docker load -i py.tar" >&2 + echo " 4) 已经构建过本项目镜像 → 跳过构建直接起:NO_BUILD=1 ./deploy.sh up" >&2 + return 1 fi - # pip 源:装依赖是在构建容器里发生的,容器不继承 shell 的代理, - # 所以拿刚选好的基础镜像真跑一次访问探测,比在宿主机上 curl 靠谱。 + # pip 源:装依赖是在构建容器里发生的,容器不继承宿主机的代理, + # 所以拿刚定下的基础镜像(此时必然已在本地)真跑一次访问探测,比宿主机 curl 靠谱。 if [ -n "${PIP_INDEX_URL:-}" ]; then echo "==> pip 源(沿用已有配置):$PIP_INDEX_URL" else - docker image inspect "$BASE_IMAGE" >/dev/null 2>&1 \ - || with_timeout 120 docker pull --quiet "$BASE_IMAGE" \ - || BASE_IMAGE="$(docker images --format '{{.Repository}}:{{.Tag}}' | grep -m1 '^python:3' || true)" for url in $PIP_URLS; do - if [ -n "${BASE_IMAGE:-}" ] && with_timeout 40 docker run --rm --entrypoint python "$BASE_IMAGE" \ + if with_timeout 40 docker run --rm --entrypoint python "$BASE_IMAGE" \ -c "import sys,urllib.request;urllib.request.urlopen(sys.argv[1],timeout=8)" "$url/"; then - PIP_INDEX_URL="$url"; pip_picked=1; break + PIP_INDEX_URL="$url"; break fi done if [ -z "${PIP_INDEX_URL:-}" ]; then @@ -131,9 +117,9 @@ configure_mirror() { fi fi - # 探出来的结果落盘,下次(以及裸 compose 命令)直接复用 - if [ "$pip_picked" = 1 ]; then env_set PIP_INDEX_URL "$PIP_INDEX_URL"; fi - if [ "$base_picked" = 1 ]; then env_set BASE_IMAGE "$BASE_IMAGE"; fi + # 只 export 给本次构建,故意不写回 .env —— .env 在 git 里,把这台机器探到的 + # 加速站带到另一台机器上,正是那边拉不到镜像的原因。 + # 想固定就在 .env 里手工写 BASE_IMAGE= / PIP_INDEX_URL= 两行(裸 compose 命令也吃这两个值)。 export BASE_IMAGE PIP_INDEX_URL echo "==> 本次构建用:基础镜像 $BASE_IMAGE | pip 源 $PIP_INDEX_URL" } @@ -143,16 +129,16 @@ build_hint() { if [ -n "${HTTPS_PROXY:-}${HTTP_PROXY:-}${https_proxy:-}${http_proxy:-}" ]; then echo "!! 注意:当前 shell 配了 HTTP(S)_PROXY,但 Docker 守护进程不认 shell 环境变量。" >&2 echo " 要用代理出网,得在 Docker Desktop → Settings → Resources → Proxies 里配" >&2 - echo " http://host.docker.internal:7897(端口换成你本机代理的),否则只能走加速站。" >&2 + echo " http://host.docker.internal:7897(端口换成你本机代理的),否则构建期出不了网。" >&2 fi cat >&2 <<'HINT' !! 构建失败。国内环境最常见的两个原因: 1) 基础镜像拉不到(failed to resolve source metadata / TLS handshake timeout) - ./deploy.sh mirror # 重跑自动探测 - BASE_IMAGE=docker.1ms.run/library/python:3.12-slim ./deploy.sh up # 手工指定加速站 - 也可以给 Docker 守护进程配 registry-mirrors(Docker Desktop → Settings → Docker Engine): - { "registry-mirrors": ["https://docker.1ms.run"] } - 保存并重启 Docker 后,把 .env 里的 BASE_IMAGE 那行删掉再 ./deploy.sh up + docker images | grep python # 看看本地有哪个可用 + BASE_IMAGE=python:3.12-slim ./deploy.sh up # 显式指定本地已有的镜像 + docker pull python:3.12-slim # 或手工拉一次 + 也可以给 Docker 守护进程配 registry-mirrors(服务器改 /etc/docker/daemon.json, + 桌面端 Settings → Docker Engine),保存并重启 Docker 后再 ./deploy.sh up 2) pip 装依赖超时 PIP_INDEX_URL=https://mirrors.aliyun.com/pypi/simple ./deploy.sh up 完全不能上网的机器:在能上网的机器上构建后拷镜像过去 @@ -184,7 +170,7 @@ wait_ready() { echo -n "." sleep 2 done - echo; echo "!! 等待超时(当前状态:$state),执行 ./deploy.sh logs 看原因" >&2 + echo; echo "!! 等待超时(当前状态:${state}),执行 ./deploy.sh logs 看原因" >&2 return 1 } @@ -253,8 +239,9 @@ PYEOF mirror) prepare configure_mirror --force - echo "==> 已写入 .env 的配置:" - grep -E '^(BASE_IMAGE|PIP_INDEX_URL)=' .env 2>/dev/null || echo " (无:说明用的是默认源或 CN_MIRROR=0)" + echo "==> 想让以后的构建固定用这组源,把下面两行写进 .env 即可(不写也行):" + echo " BASE_IMAGE=${BASE_IMAGE:-python:3.12-slim}" + if [ -n "${PIP_INDEX_URL:-}" ]; then echo " PIP_INDEX_URL=$PIP_INDEX_URL"; fi ;; status) "${COMPOSE[@]}" ps