将 Python Web 小工具部署到 VPS:以 TCC5110 LVDS 寄存器生成器为例
背景
最近做了一个 TCC5110 LVDS Timing Register Generator 小工具,用来根据 LCD/LVDS panel timing spec 自动生成 Telechips TCC5110 的寄存器配置,包括 register list、C header、CSV 和 report。
这个工具最开始是本地运行的 Python Web UI:
python3 tcc5110_lvds_webui.py
默认只监听本机:
http://127.0.0.1:8765/
本地使用没有问题,但如果希望团队里的其他 FAE、RD 或客户支持同事也能远程访问,就需要把它部署到 VPS 上,并通过域名和 HTTPS 对外提供服务。
最终部署后的访问地址是:
https://lvds.snailszzy.top/
本文记录完整部署过程。
是否还需要 Python 后端?
需要。
这个工具并不是一个纯静态 HTML 页面。Web UI 只是负责收集参数,真正的寄存器生成逻辑在 Python 文件中:
tcc5110_lvds_reg_generator.py
Web UI 点击 “生成寄存器配置” 后,会向后端接口发送请求:
POST /generate
后端再调用 Python 生成器,返回 report、register list、C header、CSV 等结果。
所以部署时有两种路线:
- 保留 Python 后端,直接把现有工具部署到服务器
- 把生成逻辑重写成 JavaScript,改成纯前端工具
这次选择第一种。原因是风险更低、维护更简单,而且现有生成逻辑已经验证过,不需要重新实现一套算法。
整体架构
部署后的结构如下:
用户浏览器
|
| HTTPS
v
Cloudflare DNS / CDN
|
| HTTPS
v
VPS 上的 Caddy
|
| Docker 内网 HTTP
v
tcc5110-lvds Python 容器
|
v
tcc5110_lvds_webui.py + tcc5110_lvds_reg_generator.py
其中:
- Cloudflare 负责 DNS 和公网访问入口
- Caddy 负责 HTTPS 证书和反向代理
- Docker 负责运行 Python Web 工具
- Python 后端负责实际寄存器计算
准备部署目录
先把需要部署的文件整理到一个独立目录:
tcc5110_lvds_reg_generator_deploy/
├── Dockerfile
├── docker-compose.yml
├── DEPLOY.md
├── README.md
├── examples/
├── tcc5110_lvds_reg_generator.py
└── tcc5110_lvds_webui.py
核心文件是:
tcc5110_lvds_webui.py
tcc5110_lvds_reg_generator.py
其中 tcc5110_lvds_webui.py 需要支持在容器中监听 0.0.0.0,否则容器外部访问不到服务。
启动参数设计为:
python3 tcc5110_lvds_webui.py --host 0.0.0.0 --port 8765 --no-browser
本地运行时可以继续使用:
python3 tcc5110_lvds_webui.py
服务器运行时不需要自动打开浏览器,所以加 --no-browser。
修改 Web UI 的监听地址
原始版本通常只监听:
ThreadingHTTPServer(("127.0.0.1", args.port), Handler)
这适合本地工具,但不适合 Docker 容器部署。
可以增加 --host 参数:
ap.add_argument("--host", default="127.0.0.1")
ap.add_argument("--port", type=int, default=8765)
ap.add_argument("--no-browser", action="store_true",
help="不自动打开浏览器")
args = ap.parse_args()
srv = ThreadingHTTPServer((args.host, args.port), Handler)
这样本地默认仍然安全地监听 127.0.0.1,而容器部署时可以指定:
--host 0.0.0.0
处理多人使用时的自动保存问题
原始 Web UI 有一个服务端自动保存:
webui_autosave.json
这在本地使用很好,但多人远程访问时会有问题:
- A 同事填的参数会保存到服务器
- B 同事打开页面时可能看到 A 的参数
- 多人同时使用会互相覆盖
所以部署版建议把自动保存改成浏览器本地 localStorage。
前端定义一个本地保存 key:
const STORAGE_KEY = "tcc5110_lvds_webui_state_v1";
保存:
localStorage.setItem(STORAGE_KEY, JSON.stringify(rawState()));
恢复:
const s = JSON.parse(localStorage.getItem(STORAGE_KEY) || "{}");
这样每个用户、每个浏览器各自保存自己的表单状态,不会互相影响。
编写 Dockerfile
这个工具只依赖 Python 标准库,不需要安装额外 Python package,所以 Dockerfile 很简单:
FROM python:3.10-slim
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1
WORKDIR /app
COPY . /app
EXPOSE 8765
CMD ["python3", "tcc5110_lvds_webui.py", "--host", "0.0.0.0", "--port", "8765", "--no-browser"]
几点说明:
python:3.10-slim足够使用EXPOSE 8765表示容器内部服务端口--host 0.0.0.0让服务可以被 Docker 网络访问--no-browser避免服务器尝试打开浏览器
编写 docker-compose.yml
当前 VPS 上已经有一个 Halo 博客和 Caddy 容器,它们在 Docker 网络:
halo-blog_halo-net
为了让 Caddy 可以通过容器名访问这个工具,需要把工具容器也加入同一个网络:
services:
tcc5110-lvds:
build: .
container_name: tcc5110-lvds
restart: unless-stopped
networks:
- halo-blog_halo-net
networks:
halo-blog_halo-net:
external: true
这样 Caddy 可以直接反向代理到:
tcc5110-lvds:8765
上传文件到 VPS
本地部署目录:
/Users/telechips/Documents/FAE AI Improment/tcc5110_lvds_reg_generator_deploy/
VPS 目标目录:
/home/ubuntu/tcc5110-lvds-webui/
上传命令:
ssh myvps2 'mkdir -p /home/ubuntu/tcc5110-lvds-webui'
rsync -az --delete \
'/Users/telechips/Documents/FAE AI Improment/tcc5110_lvds_reg_generator_deploy/' \
myvps2:/home/ubuntu/tcc5110-lvds-webui/
在 VPS 上构建并启动容器
登录 VPS:
ssh myvps2
进入部署目录:
cd /home/ubuntu/tcc5110-lvds-webui
构建并启动:
docker compose up -d --build
查看容器状态:
docker ps --filter name=tcc5110-lvds
正常情况下可以看到:
tcc5110-lvds Up ...
查看日志:
docker logs --tail=80 tcc5110-lvds
正常日志类似:
TCC5110 LVDS Timing Register Generator Web UI 运行中: http://127.0.0.1:8765/ (Ctrl+C 退出)
这里显示 127.0.0.1 只是程序打印的展示 URL,不影响容器内实际监听 0.0.0.0。
配置 Caddy 反向代理
VPS 上原来已有 Halo 博客:
blog.snailszzy.top
Caddy 配置文件路径:
/home/ubuntu/halo-blog/Caddyfile
先备份:
cp /home/ubuntu/halo-blog/Caddyfile \
/home/ubuntu/halo-blog/Caddyfile.bak.$(date +%Y%m%d%H%M%S)
然后添加 lvds.snailszzy.top:
lvds.snailszzy.top {
reverse_proxy tcc5110-lvds:8765
encode gzip
header {
X-Content-Type-Options nosniff
X-Frame-Options DENY
}
}
如果希望旧地址也兼容,可以在 blog.snailszzy.top 下保留 /lvds/ 路径代理:
blog.snailszzy.top {
redir /lvds /lvds/
handle_path /lvds* {
reverse_proxy tcc5110-lvds:8765
}
handle {
reverse_proxy halo:8090
}
encode gzip
header {
X-Content-Type-Options nosniff
X-Frame-Options DENY
}
}
lvds.snailszzy.top {
reverse_proxy tcc5110-lvds:8765
encode gzip
header {
X-Content-Type-Options nosniff
X-Frame-Options DENY
}
}
验证 Caddy 配置:
docker exec caddy caddy validate --config /etc/caddy/Caddyfile
重新加载:
docker exec caddy caddy reload --config /etc/caddy/Caddyfile
配置 Cloudflare DNS
域名 snailszzy.top 使用 Cloudflare DNS。
在 Cloudflare 后台添加记录:
Type: CNAME
Name: lvds
Target: blog.snailszzy.top
Proxy status: Proxied
TTL: Auto
也可以使用 A 记录:
Type: A
Name: lvds
IPv4: 138.2.111.81
Proxy status: Proxied
TTL: Auto
推荐使用 CNAME 指向 blog.snailszzy.top,这样如果以后 VPS IP 变化,只需要维护 blog 的解析。
DNS 生效后检查:
dig +short lvds.snailszzy.top A
如果返回 Cloudflare 的 IP,例如:
104.21.x.x
172.67.x.x
说明 Cloudflare 代理记录已经生效。
验证服务
验证页面:
curl -sS https://lvds.snailszzy.top/ | head
应返回 HTML:
<!doctype html>
<html lang="zh">
验证生成接口:
curl -sS -X POST https://lvds.snailszzy.top/generate \
-H 'Content-Type: application/json' \
--data-binary @examples/hsd123jpw2_a00_full_panel.json
返回中应包含:
{
"ok": true,
"n_regs": 46,
"frame_rate": 59.999224565756826,
"scope": "full_panel"
}
也可以用 Python 简单解析:
curl -sS -X POST https://lvds.snailszzy.top/generate \
-H 'Content-Type: application/json' \
--data-binary @examples/hsd123jpw2_a00_full_panel.json \
| python3 -c 'import json,sys; d=json.load(sys.stdin); print(d.get("ok"), d.get("n_regs"), round(d.get("frame_rate",0),3), d.get("scope"), len(d.get("warnings",[])))'
期望输出:
True 46 59.999 full_panel 0
问题 1:点击按钮提示 Access Denied
部署过程中遇到过一个问题:
请求失败: SyntaxError: Unexpected token 'A', "Access Denied" is not valid JSON
原因是访问旧路径时少了最后的 /:
https://blog.snailszzy.top/lvds
页面中的相对请求 generate 会被浏览器解析成:
https://blog.snailszzy.top/generate
这个路径属于原来的 Halo 博客,不是 LVDS 工具,所以返回:
Access Denied
前端再把这个纯文本当 JSON 解析,就出现了错误。
解决方法有两个:
- Caddy 把
/lvds重定向到/lvds/ - 前端明确请求
/lvds/generate或在独立域名下请求generate
Caddy 配置:
redir /lvds /lvds/
前端也可以根据路径选择接口:
const GENERATE_URL = window.location.pathname === "/lvds" ||
window.location.pathname.startsWith("/lvds/")
? "/lvds/generate" : "generate";
这样不管用户打开 /lvds 还是 /lvds/,按钮都不会请求到错误路径。
问题 2:lvds.snailszzy.top 提示 SSL handshake failed
切换到独立子域名时,浏览器曾提示:
SSL handshake failed
这个问题发生在 Cloudflare 已经把 lvds.snailszzy.top 代理到源站,但源站 Caddy 还没有成功申请 lvds.snailszzy.top 证书时。
检查 Caddy 日志:
docker logs --tail=160 caddy
一开始可以看到类似:
DNS problem: NXDOMAIN looking up A for lvds.snailszzy.top
说明 Caddy 申请证书时 DNS 还没有生效。
DNS 生效后,强制 Caddy 重新加载:
docker exec caddy caddy reload --config /etc/caddy/Caddyfile
然后继续查看日志:
docker logs --since=2m caddy
正常情况下会看到:
authorization finalized
validations succeeded; finalizing order
certificate obtained successfully
证书文件也会出现在:
/data/caddy/certificates/acme-v02.api.letsencrypt.org-directory/lvds.snailszzy.top/
之后再访问:
https://lvds.snailszzy.top/
就可以正常打开。
更新工具
以后如果修改了 Python 代码或 Web UI,只需要重新上传并重建容器:
rsync -az --delete \
'/Users/telechips/Documents/FAE AI Improment/tcc5110_lvds_reg_generator_deploy/' \
myvps2:/home/ubuntu/tcc5110-lvds-webui/
ssh myvps2
cd /home/ubuntu/tcc5110-lvds-webui
docker compose up -d --build
如果只改了 Caddyfile:
docker exec caddy caddy validate --config /etc/caddy/Caddyfile
docker exec caddy caddy reload --config /etc/caddy/Caddyfile
安全建议
当前工具是公开访问的。如果只是内部团队使用,建议后续加一层访问控制。
可选方案:
- Cloudflare Access
- Caddy basic_auth
- 限制公司 VPN 或固定 IP 访问
如果只是临时给团队使用,Caddy basic_auth 最简单。
示例:
lvds.snailszzy.top {
basic_auth {
user JDJhJDE0JEF4YW1wbGVIYXNo
}
reverse_proxy tcc5110-lvds:8765
}
密码 hash 可以用 Caddy 生成:
docker exec -it caddy caddy hash-password
总结
这次部署的关键点有三个:
- 这个工具不是纯前端,仍然需要 Python 后端执行寄存器生成逻辑
- Docker + Caddy 是比较轻量、稳定、容易维护的部署方式
- 独立子域名部署比挂在博客路径下更干净,最终推荐使用
https://lvds.snailszzy.top/
最终访问地址:
https://lvds.snailszzy.top/
后续如果还要部署类似的小工具,可以复用这套模式:
Python Web UI -> Docker 容器 -> Caddy 反向代理 -> Cloudflare DNS -> HTTPS 域名访问