项目是一个去水印小工具:前端是 Vue 做的 H5 页面,后端是 FastAPI,流程是上传图片、检测水印位置、人工确认框选、再做修复出图,数据存在 SQLite 里。
2026 年 10 月 21 日,我把它部署到了一台华为云 ECS 上。服务器是 Ubuntu 24.04.4 LTS,2 核、1.7 GiB 内存、40 GB 磁盘,部署方式没有用 Docker,用的是 Python 虚拟环境 + systemd + nginx 反向代理这套老办法。
一、部署前:先把服务器摸清楚
第一件事不是传代码,而是确认环境:
cat /etc/os-release
whoami
hostname -I这里有个新手容易踩的点:服务器网卡上显示的是内网 IP,浏览器访问要用云控制台分配的弹性公网 IP,两者不是一回事,拿内网 IP 是打不开的。
文件上传有三种常见方式:WinSCP / FileZilla 图形化拖拽、scp 命令行直传、git clone 服务器端拉取。这次实际用的是 SSH / SFTP 脚本化上传,本质和 scp 相同。
装 nginx 时还遇到一个包管理锁冲突,报错是拿不到 dpkg 锁。查下来是后台一个跑了 53 分钟的 apt upgrade 进程卡死了,一直占着锁。处理办法是先中断当前安装,找到那个进程并结束它,删掉残留锁文件,再修复被中断的包配置、补齐缺失依赖:
ps aux | grep -i apt
kill -9 <PID>
rm -f /var/lib/dpkg/lock*
dpkg --configure -a
apt --fix-broken install注意,kill -9 是强制终止,可能会留下损坏的安装状态,所以后面修复那两步必须做。正常情况下可以先试普通的 kill。
排错之后,服务器上已经有了 nginx 1.24、PHP 8.3、MySQL 8.0。不过这个项目后端是 FastAPI,并不依赖 PHP 和 MySQL,持久化用的是 SQLite,那套环境只是留着给别的用途,不影响本服务。
二、本地准备:前端先构建,后端补依赖
前端源码是 Vue 单文件组件,浏览器没法直接跑,必须先编译:
cd frontend
npm run build:h5
# 产物在 frontend/dist/build/h5/,也就是 index.html 加 assets/接口地址也顺手改了一下。原来生产地址是个占位符,改成按平台条件编译:H5 在开发时直连本地 127.0.0.1:8000,生产时用同域相对路径,让 nginx 去反代 /api/v1/。这样页面和接口同源,既不用处理跨域,换 IP、换域名也不怕。小程序则不同,必须用已备案的 HTTPS 域名,不能用 IP 或 HTTP。
后端方面,requirements.txt 里补了一个 langchain-openai,因为视觉检测走的是 OpenAI 兼容接口,缺了它启动会报模块找不到。
另外新增了一个 deploy/ 目录,把部署相关的东西都收在一起:本地一键打包脚本、服务器一键安装脚本、systemd 单元文件、nginx 配置和部署说明。打包原则是只带“能重建环境的东西”:后端源码和依赖清单、前端构建产物、部署配置,不带 node_modules、不带虚拟环境、不带本地数据库,最终包只有约 0.12 MB。
三、上服务器:一键初始化
上传和解压很简单:
# 本地
scp dist/wm-eraser-deploy.tar.gz root@120.46.175.101:/tmp/
# 服务器
mkdir -p /opt/wm-eraser
tar -xzf /tmp/wm-eraser-deploy.tar.gz -C /opt/wm-eraser --strip-components=1/opt 是 Linux 放第三方应用的约定目录,和系统文件隔离开。
然后跑一键初始化脚本:
bash /opt/wm-eraser/deploy/bootstrap.sh它按顺序做六件事:装系统依赖、建一个最小权限的专用系统用户(不用 root 跑业务)、建 Python 虚拟环境、装后端依赖、配置 nginx 并校验重载、安装并启动 systemd 服务。
最终的运行架构是这样的:
浏览器
│ http://120.46.175.101/(80 端口)
▼
nginx ──┬─ / → 前端静态文件,直接读盘
└─ /api/v1/ → 反向代理到 127.0.0.1:8000
│
uvicorn(systemd 守护)
│
FastAPI + SQLite + data/后端只监听 127.0.0.1,不直接对外提供服务,所有流量统一走 nginx 这个入口。
四、验证:先测健康检查,再跑全链路
先看两个基础检查:健康检查接口返回正常,文档页返回 200。然后做了一次业务全链路冒烟,全部通过:
- 上传一张 10 KB 的测试图,拿到任务 ID,状态是排队中;
- 轮询任务,状态从检测中走到等待确认,返回的水印框位置和图中的水印位置吻合;
- 提交确认框,状态进入修复中;
- 再轮询,状态变成已完成;
- 下载结果,HTTP 200,是一张 14 KB 的 JPEG。
也就是说,上传、检测、人工确认、修复、出图这一整条线,在生产环境是通的。
五、这次踩过的坑
| 问题 | 现象 | 原因 | 解决办法 |
|---|---|---|---|
| dpkg 锁被占用 | 装 nginx 时拿不到锁 | 后台 apt upgrade 卡死 53 分钟 | 结束进程、删锁、修复包配置、补齐依赖 |
| SSH 22 端口不通 | 本机连不上服务器 | 短时间反复连接触发了防爆破机制,临时拉黑了本机公网 IP,外部节点检测 22 端口是正常的 | 等 10–30 分钟自动解封,急用可以走云控制台 VNC |
| 脚本执行报错 | bad interpreter: /bin/bash^M | 脚本是在 Windows 下写的,换行是 CRLF | 打包时统一转成 LF |
| SFTP 写入失败 | 报文件找不到 | sftp.put() 在这台服务器上异常 | 改用 sftp.file() 流式写入 |
| 解压路径多一层 | 文件落到了多余的一层目录下 | tar 包保留了顶层目录 | 解压时加 --strip-components=1 |
| 依赖安装慢 | pip 只有约 128 KB/s,装了约 8 分钟 | 服务器访问 PyPI 受限 | 可以配置国内镜像源加速 |
六、日常运维和更新
服务状态、启停、重启都走 systemd,日志看 journalctl,nginx 改配置前先 nginx -t 校验再重载,端口占用用 ss 查,资源用 free、df、top 看。
更新发布时,本地重新打包上传,服务器上解压覆盖,再重装依赖、重启服务、重载 nginx。要注意保留已有的数据文件,不要把数据库和上传的数据覆盖掉。数据备份也很简单,定期把数据库和数据目录打个包就行。
结尾
到这里,网页版、健康检查接口和文档页都上线了,业务全链路实测通过,systemd 也保证了崩溃自动重启和开机自启。对一个小项目来说,这套“虚拟环境 + systemd + nginx”的部署方式够用、也好排查,后续如果要接正式域名,再把 HTTPS 接上就可以了。