FastAPI项目上线全流程:gunicorn+systemd+Nginx与Docker实战
发布时间:2026/10/17 12:38:54 锦皓数字建站

把项目部署上线这件事听起来特别高大上但本质就一句话——让你的 FastAPI 应用不再只在你电脑的 localhost 上运行而是长期稳定地跑在一台别人也能访问的服务器上。这篇是 FastAPI 系列教程的第 7 篇也是很多人最想跳过却又最绕不开的一篇。我会尽量跨过花哨的概念和炫技的命令用最接近实际操作的方式把 FastAPI 从“写完了代码”推向“真正上线”这段路程讲清楚。整篇内容面向 0 基础读者如果你之前只在本地跑过 uvicorn main:app --reload看到服务器、Nginx、systemd、Docker 这些词还比较陌生那这篇文章正是为你准备的。文章里所有步骤都带参数说明和踩坑提示看完之后你可以直接照着在自己服务器上试一遍。1. 部署前先想明白这些事本地能跑不代表服务器能跑1.1 FastAPI 部署链路的基本认知ASGI、服务器与进程管理很多初学者第一次部署时习惯性在服务器上敲下那条熟悉的开发命令uvicorn main:app --reload然后发现好像也能访问就以为大功告成了。说实话这条命令在服务器上确实能跑但它漏掉了生产环境几乎所有的关键问题没有多进程并发处理能力、没有崩溃自动重启、没有开机自启、没有日志留存而且 --reload 会在文件变化时反复重启服务这在生产环境是纯负面作用。更致命的是开发模式下的 uvicorn 默认单进程一旦并发请求上来再多 CPU 核心也发挥不出来任何一个未捕获的异常都会让服务直接退出用户看到的就是“无法连接”。搞清楚 FastAPI 的应用服务器链路是理解部署的前提。FastAPI 本身是一个 ASGI 框架代码里的 app 对象是一个 ASGI 应用它并不是能直接对外提供稳定服务的进程。你需要一个 ASGI 服务器来加载这个 app接收 HTTP 请求、调用应用逻辑、再返回响应。uvicorn 就是这样一个 ASGI 服务器但它更适合开发调试。生产环境里更常见的组合是gunicorn 作为进程管理入口通过 --worker-class 参数指定 uvicorn 的 worker 来跑 ASGI 应用。gunicorn 负责管理 worker 数量、崩溃重启、信号转发uvicorn 负责执行 ASGI 协议。两者配合才构成一个相对健壮的 FastAPI 应用服务器。我见过不少刚接触部署的人在这一步绕远路。一听说生产环境用 gunicorn就只装了一个 gunicorn启动后发现根本无法加载 FastAPI报错里出现 ASGI、coroutine 这样的词。原因很简单gunicorn 默认的同步 worker 只支持 WSGI 协议FastAPI 是异步框架必须用 uvicorn 的 worker。后面实操部分我会给出完整命令这里先把原理讲透终端里的启动命令本质上是在告诉 gunicorn“用哪种 worker、去加载哪个模块里的哪个应用对象”。比如 main:app 指的就是 main.py 文件里的 app 对象你在本地一定见过只是以前没留意这句话的含义。提示不要在服务器上直接执行 uvicorn main:app --reload 当作生产启动方式。虽然它能跑但它没有进程守护、多进程和开机自启的能力一次崩溃就让服务彻底不可用。1.2 依赖与环境的规范化虚拟环境和 requirements.txt部署前第一件要做的不是买服务器而是把项目的依赖环境整理干净。很多 0 基础项目在本地都是“裸奔”状态直接在全局 Python 环境里 pip install 了一堆包谈不上依赖管理。这样到了服务器上你很难复现一致的运行环境——本地装的是某几个版本服务器上装的是另几个版本跑起来出现各种奇怪问题。正确做法是为项目创建独立的虚拟环境然后用 requirements.txt 锁定依赖清单。在本地项目目录执行python3 -m venv venv source venv/bin/activate pip install -r requirements.txtrequirements.txt 的生成方式值得说一下。新手最容易犯的错误是直接执行 pip freeze requirements.txt。这个命令会把当前环境里所有已安装的包全部导出里面可能包含一堆跟项目无关的包比如你之前为了其他测试装的工具包。如果带着这些多余依赖去服务器上安装不但安装慢还可能因为版本冲突引入不必要的故障。比较推荐的做法有两种。第一种如果项目依赖本来很简单就手动维护一份干净清单只写直接依赖的核心包比如fastapi0.104.1 uvicorn[standard]0.24.0 gunicorn21.2.0 SQLAlchemy2.0.23 pydantic2.5.0 pydantic-settings2.1.0第二种用 pipreqs 这类工具根据代码里的 import 自动生成依赖清单pip install pipreqs pipreqs ./ --force我个人的倾向是对于教程项目或中小型项目手动维护就够用每次上线前核对一遍。为版本加上 锁死版本号是必要的因为第三方库升级有可能静默改变行为比如 pydantic 从 1.x 升到 2.x很多类型转换逻辑会报错。部署上线是一个求稳的场景版本锁死带来的确定性强过“最新版”带来的新鲜感。1.3 环境变量与敏感信息绝不硬编码到代码里部署过程中第二个要提前补的短板是敏感信息管理。项目里多半有数据库连接串、JWT 密钥、第三方 API Key 这类配置如果直接写在 .py 文件里第一次可能觉得方便等代码传到服务器甚至推进仓库问题就来了。要么在不同环境之间频繁改代码要么一不留神把密钥带进版本库后面只能紧急轮换所有凭据。正确做法是使用环境变量。写代码时用 pydantic-settings 读取配置这是 FastAPI 生态里比较规范的方案。先建一个 config.pyfrom pydantic_settings import BaseSettings, SettingsConfigDict class Settings(BaseSettings): app_name: str My FastAPI App database_url: str sqlite:///./dev.db secret_key: str change-me model_config SettingsConfigDict(env_file.env, env_file_encodingutf-8) settings Settings()然后代码里通过 settings.database_url 读取配置。本地开发时在 .env 文件里写具体值生产环境则通过 systemd 的 EnvironmentFile 或在启动命令前注入环境变量不需要 .env 文件随代码一起上线。记得把 .env 加入 .gitignore避免误提交。这里有一个比较隐蔽的坑pydantic-settings 读取环境变量时字段名默认不区分大小写所以代码里用 database_urlLinux 环境变量里写成 DATABASE_URL它能正确匹配。反而容易翻车的是数据库连接串里带了特殊字符比如密码里有 或 #这种字符在 URL 里需要做 URL 编码否则解析连接串时会拦腰截断报出“无法连接到数据库”这种误导性错误。排查时容易让人满头问号。2. 部署方案怎么选裸机进程、容器化还是平台托管2.1 最直接的思路gunicorn 结合 uvicorn worker 跑在服务器裸机上部署方案的选择没有绝对标准取决于项目规模、维护能力和你的学习目标。先说最简单、也最接近服务器运维本质的方案在一台 Linux 云服务器上直接安装 Python、创建虚拟环境、安装依赖然后用 gunicorn 启动 FastAPI再用 systemd 管理进程、Nginx 做反向代理。这个方案的思路是完全不引入容器技术所有进程直接跑在操作系统上出了问题排查路径短适合 0 基础入门和中小型项目。有人会问为什么不直接用 uvicorn可以但 gunicorn 作为更成熟的进程管理器在多 worker 管理、信号处理和崩溃重启的细节上更完善。而且 gunicorn 的 worker 数可以按 CPU 核心数弹性调整这是单进程 uvicorn 很难做到的。配合 systemd可以实现开机自启和崩溃自动拉起这三者合起来才像“正规军”。裸机方案的短板也很明显环境隔离靠虚拟环境踩坑要自己趟换一台服务器要把系统依赖、Python 版本、项目依赖全部手动配一遍。如果你以后要频繁交付到多个环境这套流程会显得繁琐。2.2 可移植性优先用 Docker 封装运行环境如果你开始觉得环境问题让人头疼就该考虑 Docker 了。Docker 把 Python 版本、系统依赖、项目代码、启动命令全部打在一个镜像里交付物从“一堆文件加一堆说明”变成了“一个镜像”在任何装有 Docker 的主机上都能以相同方式运行。对 FastAPI 项目来说一个最小的 Dockerfile 大概是这样的FROM python:3.11-slim WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY . . EXPOSE 8000 CMD [gunicorn, -w, 4, -k, uvicorn.workers.UvicornWorker, -b, 0.0.0.0:8000, main:app]构建镜像后运行docker build -t my-fastapi-app . docker run -d --name my-app -p 8000:8000 --env-file .env my-fastapi-app有几处细节值得提醒。Dockerfile 里把 COPY requirements.txt 放在 COPY . 之前是利用了 Docker 的分层缓存机制只要 requirements.txt 没变后续安装依赖的镜像层就可以复用重建镜像会快很多。启动命令里的 -b 0.0.0.0:8000 和裸机方案里的 127.0.0.1:8000 不同因为在容器内部你希望 gunicorn 监听容器的所有网卡再由宿主机通过端口映射访问。如果这里写 127.0.0.1容器外就无法访问了这是很多初学者第一次用 Docker 部署 FastAPI 时最常踩的坑。讲究一点的团队还会用 Docker Compose 把应用服务和数据库、Redis 编排到一起一条 docker compose up -d 拉起整套依赖。需要注意新版 Docker Compose 命令中间有个空格老写法 docker-compose 带横杠两种命令都常见取决于安装的插件版本。要说 Docker 的代价主要是概念多镜像、容器、网络、卷、Compose每一项都是新知识。但一旦跨过学习曲线环境问题带来的试错成本会低很多。2.3 三种方案的适用场景对比与实际选择建议把三种主流方案放在一起看会更直观方案适用场景优点缺点学习成本裸机 gunicorn systemd0基础入门、学习部署原理链路清晰排查问题直观环境迁移繁琐标准化差低Docker 容器化多环境交付、团队协作、微服务环境一致可移植性强概念多网络和卷需要理解中平台托管 PaaS快速上线原型、不想管服务器免运维自动伸缩定价不透明定制受限低如果是纯学习强烈建议先从裸机方案完整走一遍因为这套流程里你会亲手处理端口、权限、防火墙、反向代理这些基本功这些知识在日后任何一次部署中都派得上用场。如果项目已经进入正式运营并且要频繁发布建议尽快引入 Docker至少让构建和运行环境统一。平台托管适合预算充足、想省掉运维精力的人但对于想真正搞懂部署的人而言黑盒体验反而会削弱对链路的感觉。我个人常用的做法是折中本地和服务器上统一用 Docker 构建运行镜像但在单台服务器部署时依然用 systemd 管理容器或进程再用 Nginx 统一入口。这样既有环境一致性又保留了系统级的进程守护与日志管理。3. 从零开始部署基于 gunicorn Nginx 的完整实操3.1 服务器初始化系统选择、用户创建与安全加固一般来说购买云服务器时选择 Ubuntu 22.04 LTS 是比较稳妥的默认项。LTS 版本维护周期长软件源里各种包比较全网上遇到问题能搜到的资料也最多。拿到服务器后第一件事不是急着装 Python而是用 SSH 登录并做基础安全配置。以 root 身份登录后先创建一个普通用户用于部署adduser wwwuser usermod -aG sudo wwwuser然后切换到这个用户继续操作。这样做的原因很简单生产服务器上不应长期以 root 身份直接操作或运行应用万一代码里有漏洞被利用应用进程的权限也会被限制在普通用户范围内不至于把整个系统都暴露出去。接着更新系统软件包sudo apt update sudo apt upgrade -y再安装部署需要的组件Python、venv、pip、nginx、git 等一次性装好sudo apt install -y python3 python3-venv python3-pip nginx git这里还要提醒一下云厂商控制台的安全组或防火墙。这一步经常被忽略后面大概率出现“明明服务起来了外部却连不上”的诡异现象。云服务器一般有两层防火墙概念一层是控制台上的安全组规则一层是服务器内部的 ufw 或 iptables。你至少要放行 SSH22、HTTP80、HTTPS443这三个端口其他端口依据项目需要再开。不要图省事把所有端口都放开。3.2 代码上线与依赖安装虚拟环境里把项目跑起来把代码放到服务器上的方式有很多种最简单的是用 git 从仓库拉取。如果你习惯本地打包上传也可以用 scp 或 rsync但对于后续更新迭代git 是更干净的选择。假设项目目录是 /var/www/myappsudo mkdir -p /var/www sudo chown wwwuser:wwwuser /var/www cd /var/www git clone 你的仓库地址 myapp cd myapp接着创建虚拟环境并安装依赖python3 -m venv venv source venv/bin/activate pip install -r requirements.txt pip install gunicorn安装完依赖后先手动启动一次确认代码在服务器上能正常工作。这一步很关键别急着配 systemd 和 Nginx先看应用本身能不能起来gunicorn -w 4 -k uvicorn.workers.UvicornWorker -b 127.0.0.1:8000 main:app看到打印出的启动日志、没有 ERROR 之后可以用 curl 验证curl http://127.0.0.1:8000/health如果项目有数据库迁移要执行比如使用 Alembic就顺手跑一次alembic upgrade head我见过一个项目部署后服务一切正常但登录接口一直报错排查了很久才发现是忘了跑迁移表结构根本没建出来。上线前把迁移、初始化数据这类动作做成一个清单比事后排查省事得多。这一步里还有个容易忽略的点如果启用虚拟环境后pip 安装包时出现网络超时或某些包编译失败可以先试试换镜像源比如在 pip install 时加上 -i 参数指定镜像地址很多纯 Python 包能大幅加速C 扩展包也能减少下载超时概率。3.3 用 systemd 管理应用崩溃自动重启与开机自启手动启动的 gunicorn 只是一个前台进程SSH 一断进程就没了。要让应用开机自启、崩溃自动重启正确的做法是为应用创建一个 systemd 服务。这是生产环境里一个基础的稳定性保障也是很多人第一次部署时容易漏掉的关键环节。创建服务文件 /etc/systemd/system/myapp.service内容如下[Unit] DescriptionMy FastAPI Application Afternetwork.target [Service] Userwwwuser Groupwwwuser WorkingDirectory/var/www/myapp EnvironmentFile/var/www/myapp/.env ExecStart/var/www/myapp/venv/bin/gunicorn -w 4 -k uvicorn.workers.UvicornWorker --timeout 60 -b 127.0.0.1:8000 main:app Restartalways RestartSec5 [Install] WantedBymulti-user.target逐行解释几个关键配置。ExecStart 里写了 venv 下 gunicorn 的绝对路径而不是直接写 gunicorn因为 systemd 启动环境不经过虚拟环境的 activate只有用绝对路径才能保证拉起的是项目虚拟环境里的那个 gunicorn。EnvironmentFile 用来加载 .env 文件相当于在服务启动前注入环境变量代码里的 settings 读取时才有值。Restartalways 表示无论什么原因退出5 秒后自动重启RestartSec 是重启间隔。写好后重新加载 systemd 配置并启动服务sudo systemctl daemon-reload sudo systemctl enable myapp sudo systemctl start myapp用以下命令检查状态sudo systemctl status myapp journalctl -u myapp -fjournalctl 会打印服务日志这条命令在部署阶段几乎是必备工具因为大多数启动期错误都能从这里看出来。这里有一个我一直在用的小技巧代码更新后只需要运行 sudo systemctl restart myapp服务就会用新代码重启。配合 git pull就形成了一个很简单的发布流程登录服务器git pull重启服务完事。3.4 Nginx 反向代理配置让访问路径干净且安全为什么还需要 Nginx因为 gunicorn 只监听 127.0.0.1:8000外部流量根本摸不到它。Nginx 作为 Web 服务器监听 80/443 端口把请求转发给 gunicorn同时还能处理静态文件、缓存、限流、HTTPS 终止等任务。FastAPI 业务上虽然可以挂载 StaticFiles但生产环境里把静态资源交给 Nginx 处理性能更好逻辑也更清晰。先创建一个站点配置文件 /etc/nginx/sites-available/myappserver { listen 80; server_name example.com; location /static/ { alias /var/www/myapp/static/; expires 7d; } location / { 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_connect_timeout 60s; proxy_read_timeout 60s; } }server_name 要替换成你的域名或服务器公网 IP。如果只有 IP 没有域名这里直接写 IP 也行就是申请 HTTPS 证书时稍微麻烦一点。然后启用配置并测试语法sudo ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/ sudo nginx -t sudo systemctl reload nginx配置里几行 proxy_set_header 要特别说明Host 让后端知道客户端访问的域名X-Real-IP 和 X-Forwarded-For 让后端拿到真实客户端 IPX-Forwarded-Proto 让后端知道请求是 HTTP 还是 HTTPS。如果丢掉这几个头FastAPI 里的 request.client.host 会变成 127.0.0.1日志里全是内网 IP排查问题时非常误导人。还有两个容易忽视的细节。第一个是 client_max_body_sizeNginx 默认允许上传的请求体上限是 1MB如果你的系统有文件上传功能文件稍大一点就会收到 413 Request Entity Too Large。按需在 server 块里加上 client_max_body_size 10m; 这样的配置。第二个是 proxy_read_timeout如果接口有长耗时任务超出默认 60 秒没返回Nginx 会主动断开并返回 504需要根据业务调整这个值。3.5 开启 HTTPS给网站加上锁HTTP 裸奔时代早就该结束了很多浏览器对非 HTTPS 页面直接标“不安全”接口跨域请求也会受限。给 Nginx 开启 HTTPS 最无痛的方式是使用 certbot 申请免费证书并自动配置续期。安装很简单sudo apt install -y certbot python3-certbot-nginx sudo certbot --nginx -d example.com如果只需要证书不自动改 Nginx 配置也可以先执行 certbot certonly --nginx -d example.com拿到证书文件后再手动写 Nginx 的 SSL 配置。执行时需要注意 80 端口必须能正常访问验证文件要通过 HTTP 方式取回所以前面防火墙的配置就派上了用场。申请成功后certbot 会自动改写 Nginx 配置加入 SSL 证书路径并把 80 端口请求 301 跳转到 HTTPS。之后基本不用管certbot 自带的定时任务会在到期前自动续期。上线后顺手验证一下sudo certbot renew --dry-run这个命令会模拟一次续期确认证书到期前能正常续上。我知道有人配置完 HTTPS 后跳过了续期验证三个月之后网站突然挂掉检查才发现证书过期了教训挺深刻。对于纯 IP 访问的服务器申请免费证书会比较麻烦因为签发机构通常要求验证域名所有权。这种场景下先用 HTTP 跑着也说得过去但建议尽早准备域名。4. 部署现场踩坑记录常见问题与排查思路4.1 502 Bad Gateway 的定位套路502 Bad Gateway 是 FastAPI 部署后最常见的错误页面看到它时大多数人第一反应是“后端挂了”但实际原因五花八门。我的排查套路是先在服务器上直接请求后端端口curl -v http://127.0.0.1:8000/health如果 curl 能正常返回 JSON说明后端进程没问题问题在 Nginx 与后端的沟通环节如果 curl 也失败再分两种情况端口没监听去看 systemd 服务和日志端口有监听但连接被拒去看 gunicorn 是否监听在了 127.0.0.1 而 Nginx 代理目标写错了地址。还有一种容易被忽略的情况gunicorn 的 worker 数设置不合理导致资源耗尽。比如服务器只有 1 核却配了 8 个 worker内存耗尽后触发 OOM进程被杀systemd 虽然会重新拉起但频繁重启之间会穿插大量 502。看 journalctl 里有没有 “Killed” 或 “Out of memory” 关键字能快速定位这类问题。4.2 端口不通与防火墙先看监听再看放行“服务器上服务起来了但公网就是访问不了”是 0 基础部署时出现频率极高的问题。按照从内到外的顺序排查先确认进程监听地址再 curl 测本地再测服务器公网 IP最后看云平台安全组和服务器内防火墙。第一步看监听地址ss -tlnp | grep 8000如果监听地址是 127.0.0.1:8000那只有服务器本机能访问公网要访问必须通过 Nginx 转发如果项目不经过 Nginx 直接对外提供 8000 端口就需要把监听改成 0.0.0.0:8000同时确认防火墙放行了 8000。这一步能排除至少一半的网络不通问题。第二步检查防火墙。Ubuntu 自带的 ufw 如果处于开启状态默认会拒绝外部流量sudo ufw status verbose sudo ufw allow 22,80,443/tcp如果还是不通去云厂商控制台看安全组入方向规则确认端口是否开放。很多人只改了服务器内部防火墙没动安全组或者反过来两边都要对得上才行。4.3 静态资源 404 与上传超限容易被忽视的细节FastAPI 项目如果用了 StaticFiles 挂载静态目录本地测试时一切正常部署到 Nginx 后可能出现两种情况静态资源全部 404或者页面能打开但样式全丢。404 的原因通常是前端请求的静态资源路径带 /static/xxx但 Nginx 里 alias 指定的目录路径不对或者目录权限不足Nginx 进程读不了文件。权限问题是这类问题里最隐蔽的。如果代码是用 root 用户克隆或上传的静态文件的属主就是 root而 Nginx 的 worker 进程通常以 www-data 用户运行读不了只有 root 才有权限的文件返回的同样是 404 或 403。解决方式是把静态目录的属主调整一下或者用 chmod 设置到可读。还记得前面说的“不要用 root 跑应用”吗这就是一个活生生的理由权限错乱会带来一堆诡异问题。上传功能相关的坑是另一个高频点。本地 FastAPI 直接跑没有 Nginx 层上传大文件毫无压力上了 Nginx 后先是 413这是 client_max_body_size 没调调完又发现接口处理到一半返回 504那是 proxy_read_timeout 太短。遇到这种问题先想清楚请求经过了哪些层每层各有什么限制再逐层去调整。4.4 性能与稳定性参数worker 数、超时和日志的合理设置部署上线之后工作并没有结束调优才刚刚开始。先说 worker 数经验公式是 2 × CPU 核心数 1。比如 2 核服务器设 5 个 worker 比较合适。但公式只是起点还要看业务类型如果是 IO 密集型的 API比如大量数据库查询和外部请求worker 可以多一些如果是 CPU 密集型的计算任务worker 太多反而会频繁切换上下文。实际调优要靠压测和观察公式给的是起步值。gunicorn 的超时参数 --timeout 也需要说明。默认值可能不够某些慢接口使用如果你有需要 30 秒以上的同步计算逻辑记得在启动命令里调高。但也不要随便设成很大超时本质上是为了防止某个卡住的 worker 永久占用资源。另外新版 uvicorn worker 已经能正常兼容 FastAPI 的 lifespan 启动与关闭事件如果看到相关警告检查一下启动命令里是否有多余的 --lifespan off一般不需要手动关闭。日志是部署后的眼睛。gunicorn 可以配合 logrotate 做日志轮转避免日志文件无限膨胀把磁盘写满。一个简单的 logrotate 配置就能实现按天切分和保留指定份数sudo tee /etc/logrotate.d/myapp EOF /var/log/myapp/access.log /var/log/myapp/error.log { daily rotate 14 compress missingok notifempty sharedscripts } EOF从第一天部署就养成看日志的习惯比什么都重要。别人问“服务是不是挂了”第一反应永远是先看 journalctl 和错误日志而不是登录服务器用肉眼观察进程是不是活着。日志会让你少走很多弯路。最后再分享一个我自己的体会。第一次部署 FastAPI 时我花了好几个小时在环境配置、权限、防火墙这些琐碎的事情上一度觉得为什么这么简单一个接口上线这么麻烦。但后来才想明白部署的意义不只是让接口能访问而是让你建立的系统真正具备可以被别人依赖的稳定性。你可能不需要在第一天就学会 Docker、Kubernetes 这些容器编排手段但从 gunicorn、systemd、Nginx 这条路完整走一遍会让你对整个请求链路有第一手的体感。以后哪怕面对再复杂的架构很多问题的排查思路都是从这条最朴素的链路里长出来的。建议你把这台服务器当作实验场反复部署、故意制造故障来观察现象这个过程学到的比任何教程都深刻。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。