This commit is contained in:
owenchen committed 2026-10-18 08:55:43 +08:00
1 parent 53184877cd
commit 479762b57e
5 files changed
+457 -1

No files matched your search

+42
View File
@@ -0,0 +1,42 @@
# 复制为 .env 后按需修改:cp .env.example .env
# .env 会被 docker compose 自动读取,不要提交到公开仓库
# ============ 鉴权(三选一,推荐第 1 种) ============
# 生成 token:openssl rand -hex 24 或 python -c "import secrets;print(secrets.token_urlsafe(32))"
# 1) 多 token(推荐):名称:token,名称2:token2
# 每个调用方发一把,吊销时只删对应的那一段,不影响别人。
# 日志里会打印调用方名称,方便排查是谁在调。
API_TOKENS=frontend:c419f1c903ac03c4f4329f21a8f49af57118fc1bce6660f2,win-service:3444df79189bbf16e872d109c5d56bc7684e24645c39b416
# 2) 旧版单密钥(兼容保留,不推荐):所有调用方共用一把
# API_KEY=
# 3) 短期 token(可选):设置后启用 POST /api/v1/auth/token
# 调用方用 client_secret 换有效期 TOKEN_TTL 秒的签名 token,
# 适合前端——页面里不硬编码长期密钥,只持有短时 token。
# CLIENT_ID=default
# CLIENT_SECRET=
# TOKEN_TTL=3600
# ============ 跨域 ============
# 允许跨域的前端来源,逗号分隔。浏览器里的 Vue/React 页面调这个接口必须配。
# * = 允许所有来源(方便开发,但任何网页都能读结果,生产环境务必收紧)
# 例:CORS_ORIGINS=http://10.1.1.163:5173,http://10.1.1.163:8080
# 后端到后端调用(curl / requests / 其他容器)不受此项影响。
CORS_ORIGINS=*
# ============ 基础镜像源 ============
# 默认国内公共源(fnOS 自带加速源需授权,常 401)。
# 备选:
# BASE_IMAGE=docker.1ms.run/library/python:3.11-slim
# BASE_IMAGE=mirror.ccs.tencentyun.com/library/python:3.11-slim
# 离线导入基础镜像(docker load)后改用本地 tag:
# BASE_IMAGE=python:3.11-slim
# BASE_IMAGE=docker.m.daocloud.io/library/python:3.11-slim
# —— 以下一般不用改,低配吃紧时再动 ——
# MAX_SIDE=960
# DET_LIMIT_SIDE_LEN=736
# MAX_DIMENSION=6000
# MAX_IMAGE_BYTES=5242880
+276
View File
@@ -0,0 +1,276 @@
# Windows 操作 NAS Docker · 命令手册
针对本项目的实际环境整理(身份证识别 API 部署在飞牛 fnOS)。
## 本机参数
| 项 | 值 |
|---|---|
| NAS IP | `10.1.1.163` |
| 项目目录 | `/vol1/1000/Docker/idcard-api` |
| compose 项目名 | `icd`(镜像名 `icd-idcard-api`) |
| 容器名 | `idcard-api` |
| 服务端口 | `8000`(容器内 8000 映射到宿主机 8000) |
| 宿主机用户 | `owen`(**不在 docker 组,docker 命令都要 `sudo`**) |
---
## 一、三个操作平面(最容易搞错的地方)
```
┌─────────────────────────────────────────────┐
│ ① Windows PowerShell(你的电脑) │
│ 能做:curl.exe 测接口、WinSCP 传文件 │
│ 不能:任何 docker 命令 │
│ 提示符:PS C:\Users\Administrator> │
├─────────────────────────────────────────────┤
│ ② NAS 宿主机(SSH 连进去) │
│ 能做:docker / docker compose / sudo / 文件操作 │
│ 登录:ssh owen@10.1.1.163 │
│ 提示符:owen@youjiala:/$ │
├─────────────────────────────────────────────┤
│ ③ 容器内部(fnOS Docker 界面「终端」按钮) │
│ 能做:容器内的 ls / grep / cat / env │
│ 不能:docker 命令(容器里没装 docker CLI) │
│ 提示符:root@c5bbd0b03ba1:/# │
└─────────────────────────────────────────────┘
```
**判断口诀**:命令里有 `docker` 或要动 NAS 上的文件 → 平面 ②;只是想试试接口通不通 → 平面 ①;想偷懒看容器里某个文件 → 平面 ③(但别在里面敲 docker)。
---
## 二、快速判断表
| 你要做的事 | 在哪里执行 | 需要登录吗 |
|---|---|---|
| 测接口通不通(curl) | Windows PowerShell | ❌ 不需要 |
| 上传/覆盖代码文件 | Windows(WinSCP / 资源管理器) | ❌ 不需要(SFTP) |
| `docker ps` / `docker logs` | NAS 宿主机 | ✅ `ssh owen@10.1.1.163` |
| `docker compose build/up` | NAS 宿主机 | ✅ SSH |
| 看镜像创建时间 | NAS 宿主机 | ✅ SSH |
| 改 `.env` | WinSCP 或 SSH 均可 | 视习惯 |
| 查看容器内 `main.py` | NAS 宿主机 `docker exec` | ✅ SSH |
| 快速瞄一眼容器内文件 | fnOS Docker 界面「终端」 | ❌ 界面点开即可 |
| 加 docker 组免 sudo | NAS 宿主机 | ✅ SSH |
---
## 三、Windows 连接方式
### SSH 登录宿主机
```powershell
ssh owen@10.1.1.163
# 首次会问 yes/no,输 yes,然后输 owen 的密码
# 退出:exit
```
看到提示符 `owen@youjiala:/$` 就说明进去了。
### 传文件(WinSCP / FileZilla)
- 协议:`SFTP`
- 主机:`10.1.1.163`,端口 `22`
- 用户:`owen`
- 目标目录:`/vol1/1000/Docker/idcard-api`
或者在 fnOS 网页的「我的文件 → Docker → idcard-api」里直接上传。
### 一次性免 sudo(推荐做一次)
```bash
sudo usermod -aG docker $USER
newgrp docker # 当前会话立即生效,不用退出重登
```
之后所有 docker 命令都不用加 `sudo` 了。本文档仍按**带 sudo** 写,两种都能用。
---
## 四、命令速查
### 1. 测试接口(Windows PowerShell)
> ⚠️ PowerShell 里 `curl` 是 `Invoke-WebRequest` 的别名,**必须写 `curl.exe`**。
```powershell
# 健康检查
curl.exe -s http://10.1.1.163:8000/api/v1/idcard/health
# 根路径(服务信息 + 端点清单)
curl.exe -s http://10.1.1.163:8000/
# 识别(带 token)
curl.exe -X POST http://10.1.1.163:8000/api/v1/idcard/recognize ^
-H "Authorization: Bearer 你的token" ^
-F "file=@C:/path/idcard.jpg"
# 识别(旧版 X-API-Key 头)
curl.exe -X POST http://10.1.1.163:8000/api/v1/idcard/recognize ^
-H "X-API-Key: 你的key" ^
-F "file=@C:/path/idcard.jpg"
# 反向验证:不带 token 应该 401
curl.exe -X POST http://10.1.1.163:8000/api/v1/idcard/recognize ^
-F "file=@C:/path/idcard.jpg"
```
> `^` 是 CMD 的续行符。PowerShell 用反引号 `` ` ``,或者直接写成一行。
### 2. 首次部署(SSH)
```bash
# ① 建目录(WinSCP 已传过文件可跳过)
mkdir -p /vol1/1000/Docker/idcard-api
cd /vol1/1000/Docker/idcard-api
# ② 确认文件都在
ls -la
# ③ 配 .env(必须有 .env,.env.example 不生效)
cp .env.example .env
vi .env # 或 WinSCP 直接编辑
# ④ 检查 compose 解析结果
sudo docker compose -p icd config | grep -A2 API_TOKENS
# ⑤ 首次构建(会装 numpy/onnxruntime,3-5 分钟)
sudo docker compose -p icd up -d --build
# ⑥ 跟踪日志
sudo docker logs -f idcard-api
```
### 3. 日常更新代码(SSH)
```bash
cd /vol1/1000/Docker/idcard-api
sudo docker compose -p icd up -d --build # 增量构建,10-30 秒
```
或者用项目自带脚本(会校验镜像里的代码真的更新了):
```bash
bash /vol1/1000/Docker/idcard-api/deploy.sh
```
**只改了 `.env` 或 `docker-compose.yml`**(不需要 build):
```bash
sudo docker compose -p icd up -d
```
**怀疑缓存坏了**(全量重建,3-5 分钟):
```bash
sudo docker compose -p icd build --no-cache
sudo docker compose -p icd up -d --force-recreate
```
### 4. 排查"改了代码没生效"
这是本项目最容易踩的坑:**fnOS 的「重新部署」只重建容器,不重建镜像**。
```bash
# ① 容器里的代码是新版吗?0=旧代码,≥1=新代码
sudo docker exec idcard-api grep -c auth_enabled /app/app/main.py
# ② 镜像什么时候建的?
sudo docker images --format "{{.Repository}}:{{.Tag}} {{.ID}} 创建于 {{.CreatedSince}}" | grep -i idcard
# ③ compose 项目真实工作目录(和你上传的目录一致吗?)
sudo docker inspect idcard-api --format '{{index .Config.Labels "com.docker.compose.project.working_dir"}}'
# ④ 环境变量注入情况
sudo docker exec idcard-api env | grep -E "API_TOKENS|API_KEY|CLIENT_SECRET"
# ⑤ 容器状态与端口
sudo docker ps --filter name=idcard-api
```
### 5. 日常运维(SSH)
```bash
# 实时日志
sudo docker logs -f idcard-api
# 最近 50 行
sudo docker logs --tail 50 idcard-api
# 过滤调用记录(看谁在调、耗时多少)
sudo docker logs --tail 100 idcard-api | grep "recognize by="
# 内存占用(关键:1C2G 机器要盯这个)
sudo docker stats --no-stream idcard-api
# 重启(不重新读 .env,改配置请用 up -d)
sudo docker restart idcard-api
# 停掉并删除容器 + 镜像
sudo docker compose -p icd down --rmi local
# 进容器内部 shell(在宿主机执行)
sudo docker exec -it idcard-api bash
```
### 6. 容器内部(fnOS 界面「终端」里执行)
```bash
grep -c auth_enabled /app/app/main.py # 代码版本
ls -l --time-style=long-iso /app/app/ # 文件时间
echo $API_TOKENS # 环境变量
```
> 这里**不能**跑 `docker` 命令。要跑 docker,回宿主机 SSH。
---
## 五、完整流程:从改代码到生效
```bash
# ① Windows:改本地文件(WorkBuddy 生成)
# ② Windows:WinSCP 上传到 /vol1/1000/Docker/idcard-api
# 注意保持目录结构:app/、Dockerfile、docker-compose.yml 等
# ③ SSH 登录 NAS
ssh owen@10.1.1.163
# ④ 构建并启动
cd /vol1/1000/Docker/idcard-api
sudo docker compose -p icd up -d --build
# ⑤ 校验镜像里的代码真的换了(关键!别跳过)
sudo docker exec idcard-api grep -c 你的新特征字符串 /app/app/main.py
# ⑥ 退出 SSH,回 Windows 测接口
exit
```
```powershell
curl.exe -s http://10.1.1.163:8000/api/v1/idcard/health
```
---
## 六、错误对照表(本对话踩过的坑)
| 报错 | 在哪发生 | 原因 | 解决 |
|---|---|---|---|
| `docker: command not found` | 容器内部 | 在容器里跑 docker 命令 | 回宿主机 SSH,或去掉 `docker exec` 前缀用容器内路径 |
| `permission denied ... docker.sock` | 宿主机 | 用户不在 docker 组 | 命令前加 `sudo`,或 `usermod -aG docker` |
| `401 Unauthorized ... docker.fnnas.com` | 宿主机 build | fnOS 加速源不授权,且会覆盖 daemon.json | Dockerfile 用 `ARG BASE_IMAGE=国内仓库全路径`,绕开 daemon 配置 |
| `could not find ... dockerfile` | 宿主机 build | Dockerfile 首行 `# syntax=` 触发前端镜像拉取 | 删掉该行 |
| health 返回缺字段 / 行为没变化 | Windows 测接口 | 镜像没重建,跑的还是旧镜像 | 必须 `up -d --build`,不能只「重新部署」 |
| `.env` 改了不生效 | 宿主机 | ①文件叫 `.env.example` ②只 `restart` 了 | 创建真正的 `.env`,然后 `up -d`(不是 restart) |
| `curl: 无法将...识别为 cmdlet` | Windows | `curl` 是 PowerShell 别名 | 用 `curl.exe` |
| `\r: command not found` | 宿主机 | Windows CRLF 换行 | `sed -i 's/\r$//' 脚本名.sh` |
| 识别返回空,前端提示"未识别到信息" | C# 调用方 | 401 被静默吞掉(没检查状态码) | C# 加鉴权头 + 检查 `IsSuccessStatusCode` |
---
## 七、两条铁律
1. **改代码必须 build**。`docker compose up -d`(不带 `--build`)在镜像已存在时不会重新构建,只重建容器。代码改动一点都进不去。
2. **`-p icd` 不能省**。项目名不是目录名,不带这个参数会创建新项目,容器和镜像都对不上。
+60 -1
View File
@@ -130,10 +130,38 @@ docker stats idcard-api # 看内存峰值是
### 第 5 步:接入调用方
- **同 NAS 上的其他 Docker 容器**:把调用方容器加入 `idcard-api_idcard-api` 网络(或在调用方 compose 里加 `external_links` / 同一自定义网络),地址写 `http://idcard-api:8000`;图省事直接用 `http://飞牛IP:8000`。
- **局域网其他设备**:`http://飞牛IP:8000`,带 `X-API-Key` 头。
- **局域网其他设备**:`http://飞牛IP:8000`,带 `Authorization: Bearer <token>` 头。
- **要域名 + HTTPS**:用 fnOS 自带的反向代理/远程访问把域名指到 `127.0.0.1:8000`,不要在路由器上映射公网端口直连。
- **ARM 机型(aarch64)**:python:3.11-slim 和 opencv-headless 有 ARM 版,但 onnxruntime 需 ≥1.19 才有完善的 ARM wheel,先在 NAS上 `docker compose build` 试;若 onnxruntime 装不上,只能换 x86 机型或用云侧部署。
### 第 6 步:以后更新代码怎么发(重要)
**fnOS 的「重新部署 / 重建容器」只会 recreate 容器,不会 rebuild 镜像**——environment 变了会把新变量注进新容器,但代码改动完全不会进去。所以改完代码必须显式 build。
```bash
cd /vol1/1000/Docker/idcard-api # 换成你的实际目录
sudo docker compose -p icd up -d --build
```
> `-p icd` 不能省:镜像名 `icd-idcard-api` 说明 compose 项目名是 `icd`,不带这个参数会建成另一个项目,跟现有容器对不上。宿主机用户如果不在 docker 组,命令前都要 `sudo`(或执行一次 `sudo usermod -aG docker $USER && newgrp docker`)。
| 你改了什么 | 命令 | 耗时 |
|---|---|---|
| `app/*.py` | `up -d --build` | 10–30 秒 |
| `requirements.txt` | `up -d --build`(pip 层失效重跑) | 1–3 分钟 |
| `.env` / `docker-compose.yml` | `up -d`(自动 recreate,无需 build) | 秒级 |
| 怀疑缓存脏 / 要刷基础镜像 | `build --no-cache` + `up -d --force-recreate` | 3–5 分钟 |
**`--no-cache` 不是日常操作**:Docker 的层缓存按内容哈希失效,`COPY app ./app` 依赖代码内容,代码一变这一层和后续层必然重跑,前面的 apt/pip 层照常命中缓存。
也可以直接用 `deploy.sh`(已随项目提供),它会增量构建、等就绪、校验镜像内代码真的更新了,失败自动降级全量重建:
```bash
bash /vol1/1000/Docker/idcard-api/deploy.sh
```
> 如果脚本报 `\r: command not found`,是 Windows 换行符问题,执行 `sed -i 's/\r$//' deploy.sh` 修一下。
## 调用
```bash
@@ -175,6 +203,37 @@ curl -X POST http://127.0.0.1:8000/api/v1/idcard/recognize/base64 \
Python 示例见 `client_example.py`。
### C# / .NET 调用示例(server-to-server)
要点三个:**必须带鉴权头**、**必须检查 HTTP 状态码**(否则 401 会被当成"未识别到"静默吞掉)、**超时要给足**(低端 NAS 首张要加载模型 10–20 秒)。
```csharp
// 配置里加一项 IdCardOcrToken,与服务端 .env 的 API_TOKENS 对应
string token = ServiceSettingsPolicy.GetIdCardOcrToken(service);
using var request = new HttpRequestMessage(HttpMethod.Post, apiUrl);
if (!string.IsNullOrWhiteSpace(token))
{
// 新版服务(推荐):标准 Bearer 头
request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
// 或旧版服务(仅设置了 API_KEY):改用下面这行
// request.Headers.TryAddWithoutValidation("X-API-Key", token);
}
request.Content = form;
using var response = client.SendAsync(request).GetAwaiter().GetResult();
// 关键:先判状态码,别让 401/413/422 掉进 JSON 解析分支变成"静默失败"
if (!response.IsSuccessStatusCode)
{
string body = response.Content.ReadAsStringAsync().GetAwaiter().GetResult();
Console.WriteLine($"[UploadIdCardFile] OCR HTTP {(int)response.StatusCode}: {body}");
return data; // 前端提示"未识别到信息",后端日志里有真实原因
}
```
`HttpClient` 建议用 `IHttpClientFactory` 或静态单例,不要每次 `new`——高频调用会耗尽 socket。
## 鉴权(Token 验证)
支持三种方式,全部用**标准库实现**(HMAC-SHA256),不引入 PyJWT 等依赖,对 1C/2G 无额外开销。
+11
View File
@@ -99,6 +99,17 @@ def _parse_tokens() -> dict:
_TOKEN_MAP = _parse_tokens()
# 弱口令提醒:示例占位符(REPLACE_ME_*)或过短的 token 不该出现在正式环境。
# 只告警不阻止启动,避免影响联调;换成真随机串后这条日志自动消失。
_weak_clients = sorted(
{name for tok, name in _TOKEN_MAP.items() if "REPLACE_ME" in tok.upper() or len(tok) < 12}
)
if _weak_clients:
log.warning(
"token 过弱或仍是示例占位符,请尽快更换:%s(生成:openssl rand -hex 24)",
", ".join(_weak_clients),
)
_engine = None
+68
View File
@@ -0,0 +1,68 @@
#!/usr/bin/env bash
# ------------------------------------------------------------------
# fnOS 一键更新部署
#
# 用法(在 NAS 宿主机):
# bash /vol1/1000/Docker/idcard-api/deploy.sh
#
# 逻辑:增量构建(只重跑失效层,通常 10-30s)→ 等就绪 → 校验镜像内代码
# 是否真的更新了 → 打印 health。校验失败自动降级为 --no-cache 全量重建。
# ------------------------------------------------------------------
set -uo pipefail
# 切到脚本所在目录,避免路径写死
cd "$(dirname "$0")" || exit 1
PROJECT_NAME="icd" # compose 项目名(镜像名 icd-idcard-api 的前缀)
SERVICE="idcard-api" # container_name
PORT=8000
MARKER="auth_enabled" # 用它能判断镜像里是新代码
echo "==> 目录: $(pwd)"
# ---------- 1. 增量构建 + 启动 ----------
echo "==> 1/4 构建并启动(增量,通常 10-30 秒)"
if ! sudo docker compose -p "$PROJECT_NAME" up -d --build; then
echo "!! 构建失败。常见原因:基础镜像源 401,检查 Dockerfile 的 BASE_IMAGE"
exit 1
fi
# ---------- 2. 等服务就绪 ----------
echo "==> 2/4 等待服务就绪"
ready=0
for _ in $(seq 1 30); do
if curl -sf "http://127.0.0.1:$PORT/api/v1/idcard/health" >/dev/null 2>&1; then
ready=1
break
fi
sleep 2
done
if [ "$ready" -ne 1 ]; then
echo "!! 服务 60 秒内未就绪,看日志:sudo docker logs --tail 50 $SERVICE"
exit 1
fi
# ---------- 3. 校验镜像内代码是否真的更新 ----------
echo "==> 3/4 校验容器内代码版本"
count=$(sudo docker exec "$SERVICE" grep -c "$MARKER" /app/app/main.py 2>/dev/null || echo 0)
if [ "${count:-0}" -lt 1 ]; then
echo "!! 容器内仍是旧代码,降级为 --no-cache 全量重建(需要几分钟)"
sudo docker compose -p "$PROJECT_NAME" build --no-cache || exit 1
sudo docker compose -p "$PROJECT_NAME" up -d --force-recreate || exit 1
count=$(sudo docker exec "$SERVICE" grep -c "$MARKER" /app/app/main.py 2>/dev/null || echo 0)
if [ "${count:-0}" -lt 1 ]; then
echo "!! 全量重建后代码仍是旧的。检查 compose 项目目录是否就是本目录:"
sudo docker inspect "$SERVICE" --format '{{index .Config.Labels "com.docker.compose.project.working_dir"}}'
exit 1
fi
fi
echo " 容器内代码已更新(匹配 $count 处)"
# ---------- 4. 健康检查 ----------
echo "==> 4/4 健康检查"
curl -s "http://127.0.0.1:$PORT/api/v1/idcard/health"
echo
echo
echo "部署完成。"
echo " 看日志:sudo docker logs -f $SERVICE"
echo " 看内存:sudo docker stats --no-stream $SERVICE"