ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

手把手FastAPI部署实战:VPS裸机与Docker容器化全流程解析

手把手FastAPI部署实战:VPS裸机与Docker容器化全流程解析

“写完了接口、调通了数据库、本地跑得飞起,然后呢?”——这是我被问得最多的一句话。很多零基础的朋友学FastAPI,一头扎进路由、依赖注入、Tortoise ORM里不可自拔,前六章顺风顺水,一到“部署上线”就开始发怵:服务器是什么?VPS和云主机哪个好?我在自己电脑上跑得好好的,为什么上了服务器就有各种奇怪问题?

这篇就专门解决上一章悬而未决的“最后一公里”。我会从方案选型开始讲,把VPS裸机部署和容器化部署两条路径都拆开,配合Nginx反向代理、进程守护、日志、监控、上线前预检这些实战环节,把一台服务器从零变成能稳定跑FastAPI的生产环境。你不需要有运维基础,跟着一步步抄作业就行,但落地之前最好搞清楚每条命令背后的为什么,不然一次配置错了,排错就变成灾难片。

这章结束后,你的API就不只是“本地能跑”,而是真正“跑在服务器上”,能被别人通过公网地址访问到。这套东西适合给“项目写到一半、准备给导师或用户演示”、“想发布一个小产品PRD跑通流程”、“找工作要展示可访问的在线项目”的所有人。

1. 部署前要想清楚的事:不是“能访问”就完事了

1.1 “跑在服务器上”到底意味着什么?

本地开发时,你执行uvicorn main:app --reload,服务监听在127.0.0.1:8000,只有你自己电脑能访问。部署上线的本质,是让这个服务换一台7x24小时不关机的公共主机,监听在0.0.0.0上,对外提供访问。

但生产环境的老牌三座大山也跟着来了:稳定性、性能、安全。本地崩溃了你随手重启继续写,线上崩了用户就访问不了;本地只有一个进程,线上可能要扛几百个并发;本地没有黑客盯着,线上每一秒都有人扫描端口。

所以学习部署,学的不是怎么敲几条命令,而是建立一套“生产思维”:进程怎么守护、流量怎么转发、日志怎么留存、异常怎么发现。这也是为什么明明直接跑uvicorn main:app --host 0.0.0.0也能访问,我却坚决不建议你在生产环境这么干的原因,后面我会详细解释。

1.2 零基础选哪条路:VPS裸机部署 vs 容器化部署

很多教程一上来就甩给你一个Dockerfile,让你docker compose up -d完事。对于零基础来说,我劝你先别急。我把两条路放在一起对比一下:

对比项VPS裸机部署容器化部署(Docker)
上手门槛相对低,命令直观需要额外理解镜像、容器、卷的概念
环境隔离差,依赖容易互相污染好,每个项目独立
排错难度直观,看进程看端口多一层抽象,要看容器日志
适合场景学习、小型项目、单机部署微服务、多组件、团队协作、云平台扩缩容

我的建议是:第一次部署,老老实实走一遍VPS裸机流程。因为你需要亲眼看到“进程是怎么起的”“nginx配置怎么生效的”“日志打到哪个文件”,这些概念会在你脑子里形成一张地图。有了这张地图,后面再学Docker,你一眼就能看出来它到底解决了什么,不会沦为纯粹抄配置文件。

1.3 服务器选型与环境初始化建议

买服务器的预算和目的直接相关。学习用,挑便宜的入门款就行,1核1G内存跑FastAPI完全够;给真实用户用,至少2核4G起,因为操作系统本身要吃内存,Nginx和Python进程各占一部分,1G很容易在并发稍微上来的时候触发OOM。

选完系统,推荐Debian系,个人经验是Ubuntu 22.04 LTS或Debian 11/12最省心,网上教程多、软件源全。拿到服务器第一件事,创建一个普通用户并赋予sudo权限,拒绝用root直接跑业务,这不是洁癖,是防线。

提示:生产服务器的SSH强烈建议改用密钥登录,密码登录太容易被暴力扫描命中。改sshd_config后记得先开一个新终端测试,确认能登录再断开旧会话,不然你会把自己锁在外面。

2. VPS裸机部署全流程:手把手把FastAPI揍上服务器

2.1 安装Python、创建虚拟环境、锁定依赖版本

先把环境准备好。Ubuntu自带的Python可能是3.8或3.10,先确认版本要和本地开发一致,避免语法和包行为差异:

# 更新系统 sudo apt update && sudo apt upgrade -y # 确认Python版本 python3 --version # 安装venv和pip sudo apt install python3-venv python3-pip -y # 进入项目目录,创建虚拟环境 cd /var/www/myfastapi python3 -m venv venv source venv/bin/activate

虚拟环境是本阶段最关键的操作。它把项目依赖和系统Python彻底隔离,不同项目用不同版本的FastAPI也不会冲突。不用的后果很实在:某天你给另一个项目装了一堆包,然后发现这个项目的Pydantic开始报错,排查到深夜怀疑人生。

依赖锁定也有技巧。本地执行pip freeze会输出一大坨间接依赖,新版直接生成requirements.txt。但更稳妥的方式是用pip freeze > requirements.txt前,先把本地环境clean一点,不要让开发阶段的调试工具混进生产。然后服务上执行:

pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple

国内服务器用镜像源能省一半以上的安装时间。但这里有个圈坑:编译型包(比如uvloop或某些含C扩展的依赖)在低配服务器上编译可能卡很久,等两分钟没动就以为卡死了。解法要么直接上预编译版,要么在配置里把内存加一档,别在1G的机器上用pip install装一堆带C扩展的包,容易OOM。

2.2 Uvicorn生产参数:为什么禁止用--reload

本地开发用uvicorn main:app --reload,改了代码自动重启。生产环境如果还这么干,后果有两个:一是每有人触发代码改动服务器就会重启,服务中断;二是--reload会启动额外的监听进程来监视文件变化,白白多吃一倍内存和CPU。

生产环境的标准姿势:

uvicorn main:app --host 0.0.0.0 --port 8000 --workers 4

解释几个关键参数,以后面试也可能用到:

  • --host 0.0.0.0:监听所有网卡,让外部请求能到达。只写127.0.0.1的话,就算Nginx配好也永远转发不进来。
  • --workers:进程数。FastAPI单进程是单事件循环,实际是跑在一个CPU核心上的。多开几个worker能充分利用多核。但注意worker数不是越多越好,建议等于服务器的CPU核心数的两倍再减1,比如2核机器配3~4个worker。
  • 不写--reload就是生产模式。

怎么验证是否起来了?ss -tlnp | grep 8000看到LISTEN状态基本就没问题。这时候在浏览器访问http://服务器公网IP:8000,你能看到FastAPI的默认JSON响应。

注意:这一步先别急着上HTTPS和域名,先把“裸IP能访问”这个里程碑搞定,后面再一层层加防护。过早引入太多组件,出了问题都不知道是哪一环的锅。

2.3 上Nginx:为什么不能直接把FastAPI暴露给公网

很多人不理解:FastAPI明明自己能监听公网端口,我为什么非要再挡一个Nginx在前面?为了让你彻底死心,我先举个反面例子:我见过有人把FastAPI直接怼到0.0.0.0:80,跑了一个多月没出事,直到被某扫描工具打穿了一个未鉴权的接口,数据库差点被删。FastAPI本身不处理SSL终止、不做请求体大小限制、不防简单DDoS,这些活必须交给专业工具。

生产环境的标准架构:

用户 → 公网 → Nginx(443) → FastAPI Python进程(Uvicorn)

Nginx承包的大部分脏活包括:HTTPS证书终止、静态文件直接返回(根本不用进Python)、对代理头的处理(让FastAPI正确识别用户IP和协议)、请求体大小控制。这意味着你应用本身可以只关注业务逻辑,安全性高一大截。

装Nginx然后写配置:

sudo apt install nginx -y

在/etc/nginx/sites-available/myfastapi.conf新建一个配置:

server { listen 80; server_name your_domain_or_ip; # 请求体上限,防止超大上传拖垮后端 client_max_body_size 20m; location / { # 把请求转发给本机FastAPI监听端口 proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_read_timeout 300s; } }

然后启用配置:

sudo ln -s /etc/nginx/sites-available/myfastapi.conf /etc/nginx/sites-enabled/ sudo nginx -t # 测试配置语法,返回successful才能继续 sudo systemctl reload nginx

这段配置里proxy_set_header X-Real-IP和X-Forwarded-For这两行最容易被漏掉。如果漏了,FastAPI里通过request.client.host拿到的永远是127.0.0.1,你后面做IP限制、审计日志全都会废掉。另外千万别忘记在FastAPI里开启ProxyHeaders支持:

# main.py import uvicorn if __name__ == "__main__": uvicorn.run( "main:app", host="0.0.0.0", port=8000, proxy_headers=True, forwarded_allow_ips="*", )

空着的话,Nginx传过来的真实IP头会被信任,但是如果不开启,Uvicorn就无法依据X-Forwarded-For算出真实IP。这个坑我踩过一次,当时做登录IP封禁,结果封的全是Nginx的内网地址,用户还是照常刷接口。

2.4 进程守护与开机自启:让崩溃恢复成为“自动挡”

生产里最噩梦的一个场景:服务跑去半夜,内存稍微泄漏一把,进程挂了。如果这时候没有自动拉起,那么等到第二天早上打开监控面板才发现已经挂了一整夜,用户骂声一片。所以这一步要搞“进程守护”,收费极低但带来的是24小时值班的安保。

先选supervisor还是systemd。现在Ubuntu 22.04已经自带systemd流程,配置简单直观,直接用前者不香吗?这里就给你用systemd的战斗姿势。

创建服务文件/etc/systemd/system/myfastapi.service:

[Unit] Description=My FastAPI app After=network.target [Service] User=www-data WorkingDirectory=/var/www/myfastapi ExecStart=/var/www/myfastapi/venv/bin/uvicorn main:app --host 127.0.0.1 --port 8000 --workers 4 Restart=always RestartSec=3 Environment=PYTHONUNBUFFERED=1 [Install] WantedBy=multi-user.target

注意几个生产铁律:

  • Restart=always唯一正解,不需要手动重启,崩溃了3秒后自动拉起,加上RestartSec=3防止疯狂重启打满日志。
  • ExecStart一定要写绝对路径,写uvicorn main:app会因为PATH环境不同找不到命令。
  • User=www-data不要用root跑业务进程。权限最小化,是保住服务器的第一道保险。

然后启动:

sudo systemctl daemon-reload sudo systemctl enable --now myfastapi sudo systemctl status myfastapi

现在把该协议定义清楚:把systemctl restart myfastapi列为一个“上线之后你经常会用到的命令”。改了代码之后传上去,重启一次服务生效。不用再开个ssh窗口跑uvicorn,因为你关掉终端,进程也不会跟着死了。

3. 容器化部署:用Docker把“环境乱象”焊死在镜像里

3.1 Docker为什么适合FastAPI项目:一次构建,处处运行

裸机部署会把很多环境问题暴露出来:别人的服务器没有你这个版本的OpenSSL、系统库、Python小版本,跑起来各种段错误。容器化部署的思路是:连Python解释器、操作系统依赖、FastAPI代码一起打成一个只读镜像,启动成隔离的容器。

对于零基础来说,核心就理解四个关键词:镜像(模板)、容器(模板运行出的实例)、数据卷(持久化存储)、网络(容器间通信方式)。

一旦本地写好Dockerfile,在本地build跑通,扔到任何一台装Docker的服务器上,docker compose up -d一键拉起。不用再手动装Python、装Nginx、配虚拟环境,可复现性直接拉满。

3.2 写一个能上生产的Dockerfile

最基本的Dockerfile长这样:

FROM python:3.11-slim # 避免出现交互式提示 ENV PYTHONDONTWRITEBYTECODE=1 \ PYTHONUNBUFFERED=1 WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple COPY . . EXPOSE 8000 CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "3"]

这里有三个避开天坑的经验:

  • 用python:3.11-slim而不是python:3.11,后者包含大量编译工具链,镜像体积能差出1GB以上,拉取慢不说,服务器磁盘也遭殃。但注意slim版有时缺系统库,如果依赖里用到Pillow之类的图像库,还要额外装libjpeg-dev。
  • 把COPY requirements.txt放在COPY . .前面,能利用Docker缓存层。以后只改代码不换依赖,构建时直接跳过pip install,速度提升明显。
  • 工作目录内不要放venv、.git、__pycache__,所以同时写好.dockerignore,不然构建时会打包一堆只占空间没用的东西。

docker-compose.yml 继续加一层:

version: "3.8" services: api: build: . container_name: myfastapi restart: always ports: - "127.0.0.1:8000:8000" environment: - SECRET_KEY=${SECRET_KEY} - DATABASE_URL=postgresql://user:pass@db:5432/mydb depends_on: - db volumes: - ./logs:/app/logs db: image: postgres:15 restart: always environment: - POSTGRES_USER=user - POSTGRES_PASSWORD=pass - POSTGRES_DB=mydb volumes: - pgdata:/var/lib/postgresql/data volumes: pgdata:

这里做了两件关键事。第一,ports写127.0.0.1:8000:8000而不是8000:8000,等于把容器的8000端口只暴露给本机,外面还是只能通过Nginx或云平台的负载均衡访问。第二,数据库用depends_on和 volumn一起拉住:数据库一挂,API不会启动,数据放在命名卷里,容器重建不丢库。

3.3 容器与Nginx的协作姿势

容器化之后Nginx的写法不需要大改,唯一区别是proxy_pass仍然指向127.0.0.1:8000,跟裸机时一样。

有一点必须讲清楚:很多人以为docker镜像里自带Nginx就够用了,其实要让一个镜像同时跑Python和Nginx,违反了“单容器单进程”的最佳实践。保持两个组件分离,改动Nginx配置不用重新build镜像,来回折腾成本低得多。

启动项目:

docker compose up -d --build docker compose logs -f api

一套起不来?优先看docker compose logs api的输出,绝大多数问题都是依赖没装全或环境变量没配。这个是容器化排错的第一入口。

4. 上线前预检:比开始部署更关键的安全步骤

4.1 翻出settings文件,把开发模式关干净

本地和生产的最大差异不是系统版本,而是“默认行为”。FastAPI生产上线前,有几个开关必须手动关清理干净:

  • DEBUG = False:这个不关,错误页会直接把堆栈、代码路径、环境变量全打印出来,等于把服务器内部结构免费送给攻击者。
  • SECRET_KEY 换掉默认值:很多教程用"changeme"这种占位符,结果上线没改,JWT签名的密钥等于公之于众,任何能注册的用户都可以伪造任意身份信息。生成方式用secrets.token_hex(32)让它随机。
  • CORS设置:如果你本地为了调试设了allow_origins=["*"],上线前必须改为你真实的前端域名。在网上被任意站点了跨域攻击,把用户数据拖走,还不带声响的。
  • 数据库连接:用SQLAlchemy或Tortoise的,检查连接池配置。生产上每次请求都新建连接,连接数一多数据库直接拒绝服务。务必设置pool_size和max_overflow的上限值。

4.2 环境变量:把密码从代码里抠出来

把密钥、数据库密码、API Key写死在代码里,是这个阶段排名第一的禁忌。理由很简单:代码一旦被push到git上,那行密码就会永远留在git历史里,就算后面删掉,任何人都可以扒出来。

最简洁的做法是项目根目录放一个.env:

SECRET_KEY=xxxx DATABASE_URL=postgresql://... REDIS_URL=redis://...

然后在代码里用Pydantic Settings读取:

from pydantic_settings import BaseSettings class Settings(BaseSettings): secret_key: str database_url: str redis_url: str class Config: env_file = ".env"

上线后在服务器上手动创建.env,并确保.gitignore包含.env。如果团队协作,把字段格式放进.env.example提交,但不要提交真实密钥。用systemd部署时,可以在EnvironmentFile=/var/www/myfastapi/.env指定,让服务启动时自动加载。

4.3 小并发和静态资源最容易被忽略的压测点

至少做一次并发测试,不需要复杂的压测工具,用ApacheBench一条命令就能看个大概:

ab -n 1000 -c 50 http://127.0.0.1:8000/api/health

看两个指标:Requests per second 和 Time per request。如果一看吞吐低得可怜,优先怀疑是Python单进程GIL或其他CPU密集操作。如果你有图片、JS、CSS这种静态资源,一定把它们交给Nginx服务,别让FastAPI去render。配置静态目录:

location /static/ { alias /var/www/myfastapi/static/; expires 30d; add_header Cache-Control "public"; }

这样一来,请求直接由Nginx快速响应,不占用后端资源,并发能力强好几倍。

5. 日志、监控、日常维护:部署完不是终点,是起点

5.1 日志分级:不要只在print里做爱

开发时用print爽快,上线后你就发现它有多裸:没有时间戳、没有分级、控制台一滚动啥都覆盖了。生产环境至少用logging封装一层,按严重程度输出。

在main.py里加一个快速配置:

import logging from logging.handlers import RotatingFileHandler handler = RotatingFileHandler("app.log", maxBytes=10 * 1024 * 1024, backupCount=5) logging.basicConfig(level=logging.INFO, handlers=[handler])

同时把systemd的日志加上journalctl -u myfastapi也能看。分级别看一视同仁,info、error、critical分开,排查时会省下一半时间。另外启动容器时用环境变量规定日志级别,别在生产环境打DEBUG级日志,日志量会以恐怖速度冲刷磁盘。

5.2 监控一次502就行:手写一个黑白盒小检查

很多入门者跳过监控直接上线,等到用户电话打过来了才知道服务凉了。不想一开始就引入重型监控系统的话,写一个几十行的Python脚本,每30秒对http://127.0.0.1:8000/health发一个检测请求:

import requests import time while True: try: resp = requests.get("http://127.0.0.1:8000/health", timeout=5) if resp.status_code != 200: print(f"[{time.time()}] bad status: {resp.status_code}") except Exception as e: print(f"[{time.time()}] error: {e}") time.sleep(30)

然后用cron或systemd timer包装,出问题就把告警发到钉钉机器人或邮件。这里建议在应用里加一个不依赖数据库的/health接口,专门给监控脚本黑盒探测。数据库挂了、Redis挂了都要能反馈具体状态码,方便排错。

5.3 升级发布:别再用“服务器上直接改代码”这种原始操作

做项目的必然要迭代。最粗糙、但也是零基础最好上手的一套流程:

git pull origin main source venv/bin/activate pip install -r requirements.txt sudo systemctl restart myfastapi

这套流程有坑:直接在生产分支上git操作容易冲突,且没有回滚机制。上了Docker之后简单一些:

docker compose up -d --build docker compose exec api alembic upgrade head # 先跑迁移

升级数据库迁移永远放在启动新代码之前,顺序反的后果是:旧代码对打了迁移的库提前兼容,新代码对没迁移的库直接报“表不存在”。

这里强调:上线后的每一次改动,尽量有回滚预案。哪怕最简单的是“git revert后重启”,也比裸改强。我踩过最大的坑是直接在服务器上把某个配置文件顺手改了,没记录,后来查问题怎么都对不上本地,防不胜防。

6. 常见问题与排查技巧实录

6.1 部署时分频遇踩坑速查表

以下都是实际项目里被问得最多、踩得最狠的坑,整理成表方便你直接查:

现象可能原因排查/解法
外网IP访问不到服务云安全组没开端口,或Uvicorn只监听了localhost检查云控制台安全组/防火墙;用ss -tlnp看是否监听0.0.0.0
502 Bad GatewayNginx连不上后端端口先curl 127.0.0.1:8000看是否通;看systemctl status myfastapi
503 Service Unavailableworker数太少或后端启动失败查看/var/log/nginx/error.log,查后端日志
上传文件报413Nginx请求体大小限制加client_max_body_size 20m;并reload
改了代码不生效服务没有重启执行sudo systemctl restart myfastapi而不是只传文件
请求IP全是127.0.0.1Nginx没传代理头或FastAPI没开proxy_headers检查X-Real-IP配置和proxy_headers=True
ModuleNotFoundError虚拟环境或容器未激活或依赖没装全source venv/bin/activate再pip show fastapi确认
端口已被占用上次启动的进程还残余fuser -k 8000/tcp或查看进程kill
服务跑半天挂了内存不足被OOM Killer干掉free -m查看内存,使用journalctl -b查Kernel日志
Docker容器重启后数据丢了没挂载volume检查docker-compose.yml里是否配置了命名卷或bind挂载

6.2 排错方法论:永远从日志入口开始,不要瞎翻

会不会排错,本质是能不能高效定位“日志里的线索”。按这个顺序来,不要跳步:

  1. 先看进程还在不在:systemctl status myfastapi或docker ps。不在就到了自动拉起重启的状态,确认为何重启见第2步。
  2. 看服务/容器日志:systemd用journalctl -u myfastapi -n 50 --no-pager;容器用docker compose logs -f api。翻最后50行,绝大多数报错都在这里。
  3. 看系统错误日志:/var/log/nginx/error.log和sudo dmesg | tail -20。如果是Segmentation fault或OOM,日志可以准确告诉你凶手是谁。
  4. 手动复现:本地或服务器直接curl -v http://127.0.0.1:8000/health,看返回什么。这个比看一堆堆栈更直观,能快速区分是网络层问题还是应用层问题。

这套顺序的核心逻辑是“由外及内,由进程到代码”。会排错的人不是在代码里瞎翻,而是顺着日志一层层剥。

6.3 别逗比去裸奔:一些安全底线操作

部署完了,有几件事建议立刻做,别拖:

  • SSH改用密钥登录并禁用密码登录,同时PermitRootLogin no。
  • 服务器的安全组只放行必需端口(通常是22,80,443)。数据库端口(5432、3306)绝对不能对公网放行,否则你的数据库等于挂在门口。
  • Nginx上尽快配HTTPS。现在的浏览器基本把纯HTTP标注成“不安全”,而部署了HTTPS之后,让Let‘s Encrypt自动续期的证书会比手动维护省心很多。用certbot一行命令就能搞定:
sudo apt install certbot python3-certbot-nginx -y sudo certbot --nginx -d yourdomain.com

证书到期前它会自动续期,但记得装好 cron 或 systemd timer(certbot自带timer了),不然三个月后突然发现打不开。

  • Gunicorn/Uvicorn不要直接监听80/443端口,让它们监听8000、8080这种高位端口,由Nginx统一处理,这样做的好处是权限和职责都更清晰。

写在最后的一些实操体会

一路写到这里,让我想起来了自己第一次部署的狼狈相。在服务器上对着一个404页面发呆了一晚上,最后发现是忘记reload nginx,配置根本没生效。那时候就悟出一个道理:部署这玩意儿,不是敲多少命令的问题,而是建立一套“排查链条”的顺序感。

你不需要一次把所有知识点吃透,但建议第一次部署时把“VPS裸机 + Nginx反向代理 + systemd守护进程”这一整套流程完完整整走一遍。这个流程里面包含的概念,在你未来接触Docker、K8s、各种云平台时全都是复用基础。等你走完一遍,再回头看Docker容器化部署,你会发现所有概念都是相通的,只是换了一种打包和隔离的手段。

按照本教程部署好之后,下一步建议给自己一个真实域名,配上HTTPS,放一个小的demo接口上去打几个请求试试。只有当你把一个“真实项目”暴露在公网并持续维护一周之后,才真正能感受到“上线了”三个字的含义:不是一次性的动作,而是从那一刻开始,这台服务器的每一分钟运行都在为你提供价值,也在考验你的工程素养。加油,跑起来。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进