v 0.0.1
parent
871a52ddb0
commit
ac6d641f17
2
.env
2
.env
|
|
@ -10,3 +10,5 @@ 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
|
||||
|
|
|
|||
|
|
@ -25,3 +25,11 @@ SESSION_DAYS=30
|
|||
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
|
||||
# PIP_INDEX_URL=https://pypi.tuna.tsinghua.edu.cn/simple
|
||||
# 关掉自动探测(源已在上面配好,或机器能直连官方源):CN_MIRROR=0 ./deploy.sh up
|
||||
# 跳过构建、直接用本地已有镜像(离线机器上 docker load 完之后用):NO_BUILD=1 ./deploy.sh up
|
||||
|
|
|
|||
15
Dockerfile
15
Dockerfile
|
|
@ -5,7 +5,12 @@
|
|||
#
|
||||
# 镜像里只有依赖和代码,数据库/附件都在 data/,靠 bind mount 挂进来。
|
||||
# data/ 已被 .dockerignore 排除,不会把本机 board.db 打进镜像。
|
||||
FROM python:3.12-slim
|
||||
#
|
||||
# 基础镜像。国内直连 docker.io 会在 load metadata 阶段 TLS 超时,所以支持换仓库前缀:
|
||||
# docker build --build-arg BASE_IMAGE=docker.1ms.run/library/python:3.12-slim .
|
||||
# ./deploy.sh up 会自动探测可用加速站并把结果写进 .env,正常情况下不用手工传。
|
||||
ARG BASE_IMAGE=python:3.12-slim
|
||||
FROM ${BASE_IMAGE}
|
||||
|
||||
ENV PYTHONUNBUFFERED=1 \
|
||||
PYTHONDONTWRITEBYTECODE=1 \
|
||||
|
|
@ -20,9 +25,11 @@ WORKDIR /app
|
|||
|
||||
# 依赖单独一层:改业务代码不触发重装依赖
|
||||
COPY requirements.txt ./
|
||||
# 国内网络直连 PyPI 容易超时,构建时换源即可:
|
||||
# PIP_INDEX_URL=https://pypi.tuna.tsinghua.edu.cn/simple ./deploy.sh up
|
||||
ARG PIP_INDEX_URL=https://pypi.org/simple
|
||||
# pip 源:默认清华源(国内直连 pypi.org 基本超时,而且换基础镜像/代理都救不了它,
|
||||
# 构建容器里的请求只能靠这里换源)。境外机器或内网自建源就覆盖:
|
||||
# docker build --build-arg PIP_INDEX_URL=https://pypi.org/simple .
|
||||
# ./deploy.sh up 会自动探测可用源并写进 .env,正常不用手工传。
|
||||
ARG PIP_INDEX_URL=https://pypi.tuna.tsinghua.edu.cn/simple
|
||||
RUN pip install --retries 5 --timeout 60 -i "$PIP_INDEX_URL" -r requirements.txt
|
||||
# 用 MySQL 部署时放开下面这行(与 requirements.txt 的注释一致)
|
||||
# RUN pip install "pymysql>=1.1" "cryptography>=42.0"
|
||||
|
|
|
|||
37
README.md
37
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`,顺带检查 docker 是否在跑、`.env` 与 `data/` 是否存在 |
|
||||
| `deploy.sh` | 命令封装:`up` / `update` / `restart` / `down` / `logs` / `status` / `backup` / `mirror`,顺带检查 docker 是否在跑、`.env` 与 `data/` 是否存在,并在构建前探测国内可用源 |
|
||||
| `.dockerignore` | 保证 `data/`、`.env`、`.git` 不会被烤进镜像 |
|
||||
|
||||
| 场景 | 命令 |
|
||||
|
|
@ -122,9 +122,42 @@ cd /path/to/unis_manager
|
|||
| 停止(数据保留) | `./deploy.sh down` |
|
||||
| 换对外端口 | `.env` 里 `HOST_PORT=8080`,再 `./deploy.sh up` |
|
||||
| 裸 compose 命令 | `docker compose up -d --build` / `docker compose down` |
|
||||
| 构建时 pip 下载超时 | `PIP_INDEX_URL=https://pypi.tuna.tsinghua.edu.cn/simple ./deploy.sh up`(也可写进 `.env`) |
|
||||
| 构建时基础镜像 / pip 下载超时 | `./deploy.sh mirror` 重探,或手工指定 `PIP_INDEX_URL=... BASE_IMAGE=... ./deploy.sh up`(见下「国内网络加速」) |
|
||||
| 把现有数据带到新机器 | `./deploy.sh backup` → 拷到目标机 `data/board.db` → `./deploy.sh up` |
|
||||
|
||||
#### 国内网络加速(docker.io / pypi.org 连不上时)
|
||||
|
||||
国内构建会撞两道**互不相干**的墙,得分开治,只治一道必然还剩一道:
|
||||
|
||||
| 卡在哪 | 典型报错 | 治法 |
|
||||
|---|---|---|
|
||||
| ① 守护进程拉基础镜像 | `failed to resolve source metadata for docker.io/library/python:3.12-slim: ... TLS handshake timeout` | 换仓库前缀 `BASE_IMAGE`(走加速站,或给守护进程配 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 都复用:
|
||||
|
||||
```bash
|
||||
./deploy.sh up # 自动探测 → 写 .env → 构建 → 等健康检查
|
||||
./deploy.sh mirror # 只想重探一遍源(不会冲掉 .env 里手工钉死的 BASE_IMAGE)
|
||||
```
|
||||
|
||||
不想用脚本,等价的两条 build-arg:
|
||||
|
||||
```bash
|
||||
docker compose build \
|
||||
--build-arg BASE_IMAGE=docker.1ms.run/library/python:3.12-slim \
|
||||
--build-arg PIP_INDEX_URL=https://pypi.tuna.tsinghua.edu.cn/simple
|
||||
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` 说了算。
|
||||
- **目标机完全不能上网**:走离线路。`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`),照抄命令即可。
|
||||
|
||||
几个坑:
|
||||
|
||||
- **数据只落在 `./data`**:重建镜像、`down` 再 `up` 都不动数据;备份依旧是拷走 `data/board.db`。
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
164
deploy.sh
164
deploy.sh
|
|
@ -7,6 +7,8 @@
|
|||
# ./deploy.sh logs 跟踪日志(Ctrl+C 退出)
|
||||
# ./deploy.sh backup 导出数据库快照(部署到新机器时带走数据)
|
||||
# ./deploy.sh status 容器状态 + 健康检查 + 访问地址
|
||||
# ./deploy.sh mirror 重新探测国内加速源(基础镜像 + pip 源)并写进 .env
|
||||
# 环境变量:CN_MIRROR=0 跳过加速探测;NO_BUILD=1 不构建、直接用本地镜像
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
|
|
@ -32,7 +34,135 @@ fi
|
|||
HOST_PORT="${HOST_PORT:-8770}"
|
||||
URL="http://127.0.0.1:${HOST_PORT}"
|
||||
|
||||
# ---------- 1. 准备 .env 与 data 目录 ----------
|
||||
# ---------- 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"
|
||||
}
|
||||
|
||||
# ---------- 1. 国内加速:基础镜像 + pip 源 ----------
|
||||
# 国内直连 docker.io / pypi.org 会 TLS 超时(报错形如 failed to resolve source
|
||||
# metadata for docker.io/library/python),所以构建前先把「真能拉到东西的源」探出来,
|
||||
# 并写回 .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/<BASE_TAG>
|
||||
MIRROR_HOSTS="${MIRROR_HOSTS:-docker.1ms.run docker.m.daocloud.io docker.1panel.live docker.xuanyuan.me}"
|
||||
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
|
||||
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 里钉死过的基础镜像不冲掉
|
||||
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}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# 基础镜像的判据是「Docker 守护进程能不能拿到它」,不是宿主机 curl 能不能通:
|
||||
# 宿主机 curl 走 HTTP_PROXY,守护进程和构建容器都不认 shell 的代理变量。
|
||||
tag="${BASE_IMAGE##*/}"
|
||||
if docker image inspect "$BASE_IMAGE" >/dev/null 2>&1; then
|
||||
echo "==> 基础镜像本地已有,不联网:$BASE_IMAGE"
|
||||
elif with_timeout 25 docker pull --quiet "$BASE_IMAGE"; then
|
||||
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
|
||||
fi
|
||||
|
||||
# pip 源:装依赖是在构建容器里发生的,容器不继承 shell 的代理,
|
||||
# 所以拿刚选好的基础镜像真跑一次访问探测,比在宿主机上 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" \
|
||||
-c "import sys,urllib.request;urllib.request.urlopen(sys.argv[1],timeout=8)" "$url/"; then
|
||||
PIP_INDEX_URL="$url"; pip_picked=1; break
|
||||
fi
|
||||
done
|
||||
if [ -z "${PIP_INDEX_URL:-}" ]; then
|
||||
PIP_INDEX_URL="https://pypi.org/simple"
|
||||
echo "!! 构建容器里访问不到任何国内 pip 源,先用官方源;超时就手工指定:" >&2
|
||||
echo " PIP_INDEX_URL=https://mirrors.aliyun.com/pypi/simple ./deploy.sh up" >&2
|
||||
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 BASE_IMAGE PIP_INDEX_URL
|
||||
echo "==> 本次构建用:基础镜像 $BASE_IMAGE | pip 源 $PIP_INDEX_URL"
|
||||
}
|
||||
|
||||
# 构建失败时给一份可操作的排查清单(国内环境九成是源的问题)
|
||||
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
|
||||
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
|
||||
2) pip 装依赖超时
|
||||
PIP_INDEX_URL=https://mirrors.aliyun.com/pypi/simple ./deploy.sh up
|
||||
完全不能上网的机器:在能上网的机器上构建后拷镜像过去
|
||||
docker save -o unis-board.tar unis-board:latest && scp unis-board.tar host:/srv/unis_manager/
|
||||
ssh host 'docker load -i /srv/unis_manager/unis-board.tar'
|
||||
然后在目标机 NO_BUILD=1 ./deploy.sh up
|
||||
HINT
|
||||
}
|
||||
|
||||
# ---------- 2. 准备 .env 与 data 目录 ----------
|
||||
prepare() {
|
||||
if [ ! -f .env ]; then
|
||||
cp .env.example .env
|
||||
|
|
@ -42,7 +172,7 @@ prepare() {
|
|||
mkdir -p data/uploads data/exports
|
||||
}
|
||||
|
||||
# ---------- 2. 等服务就绪 ----------
|
||||
# ---------- 3. 等服务就绪 ----------
|
||||
wait_ready() {
|
||||
echo -n "==> 等待服务就绪 "
|
||||
for _ in $(seq 1 30); do
|
||||
|
|
@ -58,13 +188,26 @@ wait_ready() {
|
|||
return 1
|
||||
}
|
||||
|
||||
# ---------- 3. 分发命令 ----------
|
||||
# ---------- 4. 构建并启动(NO_BUILD=1 时直接用本地镜像)----------
|
||||
up_or_exit() {
|
||||
if [ "${NO_BUILD:-0}" = "1" ]; then
|
||||
echo "==> NO_BUILD=1:跳过构建,直接用本地镜像 unis-board:latest"
|
||||
"${COMPOSE[@]}" up -d
|
||||
elif "${COMPOSE[@]}" up -d --build; then
|
||||
return 0
|
||||
else
|
||||
build_hint; return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# ---------- 5. 分发命令 ----------
|
||||
case "${1:-up}" in
|
||||
up|start)
|
||||
first_run=0
|
||||
[ -f data/board.db ] || first_run=1
|
||||
prepare
|
||||
"${COMPOSE[@]}" up -d --build
|
||||
configure_mirror
|
||||
up_or_exit || exit 1
|
||||
wait_ready
|
||||
echo " API 文档:$URL/api/docs"
|
||||
if [ "$first_run" = "1" ]; then
|
||||
|
|
@ -73,7 +216,10 @@ case "${1:-up}" in
|
|||
;;
|
||||
update)
|
||||
prepare
|
||||
"${COMPOSE[@]}" up -d --build --force-recreate
|
||||
configure_mirror
|
||||
if [ "${NO_BUILD:-0}" = "1" ]; then "${COMPOSE[@]}" up -d --force-recreate
|
||||
else "${COMPOSE[@]}" up -d --build --force-recreate || { build_hint; exit 1; }
|
||||
fi
|
||||
wait_ready
|
||||
;;
|
||||
restart)
|
||||
|
|
@ -104,6 +250,12 @@ print(f"[ok] 已导出 {sys.argv[1]}({os.path.getsize(sys.argv[1]) // 1024} KB
|
|||
PYEOF
|
||||
echo "==> 部署到新机器:把这个文件放到目标机的 data/board.db,再执行 ./deploy.sh up"
|
||||
;;
|
||||
mirror)
|
||||
prepare
|
||||
configure_mirror --force
|
||||
echo "==> 已写入 .env 的配置:"
|
||||
grep -E '^(BASE_IMAGE|PIP_INDEX_URL)=' .env 2>/dev/null || echo " (无:说明用的是默认源或 CN_MIRROR=0)"
|
||||
;;
|
||||
status)
|
||||
"${COMPOSE[@]}" ps
|
||||
echo -n "==> /health:"
|
||||
|
|
@ -112,7 +264,7 @@ PYEOF
|
|||
echo "==> 访问地址:$URL"
|
||||
;;
|
||||
*)
|
||||
sed -n '3,9p' "$0"
|
||||
sed -n '3,11p' "$0"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
|
|
|||
|
|
@ -13,9 +13,9 @@ services:
|
|||
build:
|
||||
context: .
|
||||
args:
|
||||
# 传给 Dockerfile 的 pip 源,默认官方;国内网络可在 .env 里设
|
||||
# PIP_INDEX_URL=https://pypi.tuna.tsinghua.edu.cn/simple
|
||||
PIP_INDEX_URL: "${PIP_INDEX_URL:-https://pypi.org/simple}"
|
||||
# 基础镜像仓库前缀 + pip 源,都由 deploy.sh 探测后写进 .env(也可手工在 .env 里钉死)
|
||||
BASE_IMAGE: "${BASE_IMAGE:-python:3.12-slim}"
|
||||
PIP_INDEX_URL: "${PIP_INDEX_URL:-https://pypi.tuna.tsinghua.edu.cn/simple}"
|
||||
image: unis-board:latest
|
||||
container_name: unis-board
|
||||
restart: unless-stopped
|
||||
|
|
|
|||
Loading…
Reference in New Issue