在本地写了个 ML Playground,同事想试一下,让他跑我电脑前来操作也太业余了,干脆部署到服务器上。本以为装个 Python、跑个 uvicorn 就完事,结果从拿到服务器 IP 到跑成开机自启的服务,折腾了小半天。
这篇把整个过程记下来,从最原始的 SSH 连上去,到用 systemd 做成一个"挂了自动重启、开机自动拉起"的生产级服务。
以三篇文章走通一个完整的 Web 应用部署路线。
本篇从零徒手部署(SSH → scp → venv → nohup → systemd),工作中你不会用这种方式部署新项目,但 Docker 藏起来的就是这层,理解了它,出问题才知道从哪查。
Docker 容器化把代码和环境打包成镜像,这是行业默认标准,换服务器、加节点、回退版本都不用重复搭环境。
Docker Compose把 api + 数据库 + 反向代理串起来,真实项目很少只有单个服务,Compose 用一个 YAML 文件定义所有服务,一行命令启动全部。
apt install docker.io),Windows 用户启用 WSL2 即可跟着操作。ML Playground是一个简单的 FastAPI 应用,目录结构不复杂:
ml-learning/├── projects/│ └── ml_playground/│ ├── app.py # FastAPI 入口│ ├── core.py # 算法注册中心│ ├── ui.py # Gradio 前端│ └── datasets.py # 内置数据集加载├── models/ # 14 个 numpy 算法实现├── data/ # Titanic、Mall 等 CSV├── requirements.txt└── ...本地已经跑过很多次了,一行命令就能启动:
pip install -r requirements.txtuvicorn projects.ml_playground.app:app --host 0.0.0.0 --port 8000现在问题很简单:怎么让它跑在服务器上,别人也能访问?答案没有那么简单。整个过程可以拆成五步。

ML Playground 界面。本地跑好的应用,目标是部署到服务器上让同事也能用
拿到服务器 IP 之后,第一步当然是连上去:
ssh root@123.123.123.123第一次连会让你确认主机指纹,输入 yes,再输密码。
每次 SSH 都要输密码,既繁琐又不安全,后面如果要写自动部署脚本,密码登录根本行不通。
配置 SSH 密钥对可以一劳永逸:
ssh-copy-id root@123.123.123.123这条命令把公钥追加到服务器 ~/.ssh/authorized_keys 里。之后 SSH 连接时,服务器用你的公钥验证身份,自动放行,不再需要密码。

左半部分:密码登录,三步手动操作,每次都要重复;右半部分:密钥登录,配置一次,后续自动登录。
连上服务器了,代码还在本地。传文件最直接的方法是 scp:
# 把整个项目根目录传上去(projects/、models/、data/、requirements.txt 一起)scp -r projects/ models/ data/ requirements.txt root@123.123.123.123:~/ml-learning/但 scp 每次全量传输,项目大了之后很慢。日常更新代码时,用 rsync 只传变化的部分:
rsync -avz --progress \ projects/ models/ data/ requirements.txt \ root@123.123.123.123:~/ml-learning/-a 保留文件属性,-z 压缩传输,-v 显示详情。第一次传输差别不大,但后续更新时 rsync 比 scp 快一个数量级,因为只传改过的文件。
以下说的是源路径(要传的目录)的斜杠行为,目标路径带不带斜杠效果一样:
ml-learning/(带斜杠)和 ml-learning(不带斜杠)行为不同:
ml-learning/:复制目录里的内容到目标路径下ml-learning(无斜杠):把整个目录复制到目标路径下,结果是 ~/ml-learning/ml-learning/...日常同步推荐带斜杠,符合直觉。
ML Playground 有 14 个算法模型文件,经常改一个就要重新部署。首次部署用 scp(命令简单),日常更新用 rsync(只传改动)。

左侧本地机器(项目目录树)、右侧服务器(已接收的文件)。scp 全量传输适合首次,rsync 增量传输适合日常更新。
SSH 进去了,代码传上去了,接下来装 Python 环境:
sudo apt updatesudo apt install -y python3 python3-pip python3-venv然后创建虚拟环境,装项目依赖:
cd ~/ml-learningpython3 -m venv .venvsource .venv/bin/activatepip install -r requirements.txt每个项目独立环境,互相不干扰。
环境装好了,启动应用:
nohup .venv/bin/python -m uvicorn projects.ml_playground.app:app --host 0.0.0.0 --port 8000 &nohup 保证 SSH 断开后进程继续跑,& 把进程放到后台。直接用 .venv/bin/python 调起虚拟环境里的 Python,不依赖 source activate。
验证一下:
curl http://localhost:8000/docsSwagger 页面返回了。关掉 SSH 终端,在本地浏览器打开 http://123.123.123.123:8000 也能打开。
看起来搞定了,同事试了一下说还行,但是过两天又挂了,同事跟我说打不开了,再跑。
nohup 只管"SSH 断开后继续跑",管不了"进程崩溃后重新跑"和"机器重启后自动跑"。 它是个临时方案,不是服务管理工具。

左栏 nohup(临时方案:一行命令能跑,但重启消失、崩溃不自愈);右栏 systemd(生产方案:开机自启、崩溃自动重启、日志集中管理)。
nohup 适合临时测试,生产环境需要更可靠的方案。Linux 自带的 systemd 是操作系统的服务管理器,让程序开机自启、崩溃自动重启。
创建一个服务文件,告诉 systemd 怎么管理你的应用:
sudo vim /etc/systemd/system/ml-playground.service[Unit]Description=ML Playground APIAfter=network.target[Service]Type=simpleUser=rootWorkingDirectory=/root/ml-learningExecStart=/root/ml-learning/.venv/bin/python -m uvicorn projects.ml_playground.app:app --host 0.0.0.0 --port 8000Restart=alwaysRestartSec=5[Install]WantedBy=multi-user.target几个关键配置说明:
WorkingDirectory=/root/ml-learning 指定项目根目录(与前面 cd ~/ml-learning 一致)ExecStart 用虚拟环境里的 python -m uvicorn,模块路径 projects.ml_playground.app:app 与第一节启动命令一致Restart=always + RestartSec=5:挂了等 5 秒自动重启WantedBy=multi-user.target:开机自启然后加载并启动:
sudo systemctl daemon-reload # 重新加载配置sudo systemctl enable ml-playground # 开机自启sudo systemctl start ml-playground # 启动服务sudo systemctl status ml-playground # 查看状态看到 active (running) 就说明成功了,从这以后不管服务器重启多少次、进程崩溃多少次,systemd 都会自动把它拉起来。
服务运行过程中出了问题,第一件事是看日志。这几个命令可以记下来:
# systemd 日志(包含 uvicorn 的 stdout)sudo journalctl -u ml-playground -n 50 --no-pager # 最近 50 行sudo journalctl -u ml-playground -f # 实时跟踪# 看谁占了端口ss -tlnp | grep 8000# 看进程是否活着ps aux | grep uvicorn这几个命令不用记住每条参数,记住三条即可:日志用 journalctl、端口用 ss、进程用 ps,具体参数忘了就 --help。

journalctl(systemd 日志,等价于 tail -f 应用日志)、ss/ps(系统状态),三组工具覆盖 90% 的排查场景。
这一套走下来,核心就是五步:

但这套流程有一个本质问题:换一台机器,以上步骤全部重来。
哪怕两台都是 Ubuntu 22.04,也要重新 SSH、重新 apt、重新 pip install、重新配 systemd。换成 CentOS 的话,apt 变 yum,systemd 路径都不一样,几乎等于重新学一遍。
systemd 管的是进程挂了自动重启,管不了从零搭环境。下一篇 Docker 容器化:一次构建,到处运行 把代码和环境打包在一起,解决这个问题。
部署环境: Ubuntu 22.04 LTS(Windows WSL2)