尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Python项目部署全指南:从环境配置到Docker与Nginx实战

Python项目部署全指南:从环境配置到Docker与Nginx实战 做后端这几年关于“Python项目部署”的提问几乎每周都能在私信、评论区或者技术群里看到几回。本地跑得好好的代码一到服务器上就变了样依赖装不上、进程无端退出、访问全是 502、日志翻半天看不到报错。你要是也被这些问题卡住过那这篇文章就是为你准备的。这篇文章我不想写一堆理论而是按我自己真实部署项目的路径把一套完整的思路和实操过程拆开来讲从 Python 环境怎么装、虚拟环境怎么管到用 systemd 托管进程、再用 Docker 做容器化部署最后用 Nginx 把多个项目挂在同一台机器上。无论你是刚学 Python 的入门选手、负责项目上线的后端工程师还是自己租了台小服务器做个人站和工具站的独立开发者这套东西都能直接照着落地。1. 部署思路与方案选型1.1 先想清楚部署到底在解决什么问题很多同学第一次做部署下意识把“部署”等同于“把代码传上去跑一下”。这么理解不能说错但如果你没想清楚背后的几件事基本会在上线阶段把自己逼疯。我习惯把部署拆成五个问题环境一致性、依赖管理、进程守护、流量入口、日志与监控。环境一致性说的是开发机和服务器在系统版本、Python 小版本、底层库上都要对得上不然很容易出现“本地好好的线上崩了”这种玄学问题。依赖管理解决的是项目用了哪些第三方库、什么版本得在服务器上可复现地装出来。进程守护解决的是服务挂着挂着挂了怎么办能不能自动拉起。流量入口决定外部请求怎么打到你的服务上是直接暴露端口还是经过一层反向代理。日志与监控则决定线上出问题时你能不能快速定位根因。如果你部署的是一个定时脚本那只需要考虑环境和执行计划如果你部署的是 Web 服务Django、Flask、FastAPI 都算那上面五件事基本全要覆盖。理解了这一点再看市面上各种部署方案的差异就会很清楚它们无非是在这五个问题上给出的解法不同而已。1.2 主流部署方案横向对比我接触过的项目部署方案基本收敛在四条路线上。裸机部署直接在服务器上装 Python 环境用 systemd 或 supervisor 管理进程前面再套 Nginx 做反向代理。优势是简单直接、排查链路短特别适合中小型项目和第一次接触部署的新手。缺点是每次换机器都得手动重来一遍环境。虚拟环境叠加方案裸机部署时配合 venv 或 virtualenv 隔离项目依赖避免多个项目共用一套全局 Python 环境导致互相污染。本质上还是裸机思路只是把依赖隔离这件事做了。Docker 容器化把项目连同运行环境整个打进镜像运行成容器。最大的收益是环境一致性一步到位多项目之间隔离干净迁移和扩容都方便。缺点是学习成本高一些宿主机磁盘、网络出问题时排查路径比裸机长。Kubernetes 集群方案到了多服务、高并发、需要自动伸缩的阶段交给负责运维的团队用 K8s 编排是合理的。但对个人开发者或者内部系统来说单机上 K8s 大概率是杀鸡用牛刀运维成本会盖过收益。方案环境一致性运维成本适合场景裸机 systemd需手动保证低个人项目、小流量系统venv 隔离环境依赖可复现低多 Python 项目共存Docker 容器化强中需要环境统一、快速迁移K8s 编排极强高大规模服务化体系1.3 我推荐的上手路径给新人的建议是不要一上来就上 Docker也不要听人说 K8s 是趋势就直接扑过去。先把裸机部署走一遍亲手把环境配出来、把代码拉下来、把服务跑起来再用 systemd 托管。这样你对后面所有抽象概念的理解都会扎实得多。等你在裸机上踩过坑熟悉了系统目录、用户权限、进程这些概念再切到 Docker 就很快。你会发现 Dockerfile 里的每一层指令对应的其实就是你之前手动敲过的命令docker-compose 里的 services对应的就是你之前手动装好的各种软件。到那个阶段你才算真正理解“容器”这个东西的价值而不是停留在背概念。如果项目确定要交给别人接手、或者要频繁跨机器部署我才会建议直接走容器化路线。选型不要追新要追当前阶段最顺手的方案。2. 环境准备Python 安装与虚拟环境管理2.1 Windows 和 Linux 下装 Python 的正确姿势多数 Python 部署最终落在 Linux 服务器上但开发机上不少同学用的是 Windows两边安装思路不太一样我分开说。Windows 上装 Python我只有一个核心建议安装包下载解压运行时记得勾选“Add python.exe to PATH”。这个勾选导致的坑技术群里几乎每周都有人踩最常见的报错是命令行输入 python 后提示“Python was not found; run without arguments to install from the Microsoft Store”。典型原因就是 PATH 没配好系统跑到应用商店去找 Python 了。重新配置 PATH 也简单打开系统设置里的环境变量在 Path 中加入 Python 安装目录通常是C:\Users\用户名\AppData\Local\Programs\Python\Python312还有它下面的Scripts子目录按实际版本调整。加完记得重开命令行再运行python --version验证。Linux 上装 Python我首推用系统包管理器装比如 Ubuntu 下的sudo apt install python3 python3-venv python3-pip。如果系统自带的版本不满足要求再去官网下载源码编译。编译前先把依赖装齐Ubuntu 上一般是 build-essential、libssl-dev、zlib1g-dev、libffi-dev 这些。少装了后面装第三方库大概率会踩编译失败我就遇到过在 CentOS 上编译 Python 3.11 时少了 bzip2-devel结果装 pandas 时报一堆错回头补上依赖重来一遍才好。2.2 venv 虚拟环境让依赖不再互相打架Python 部署里有一个几乎所有人都会踩、但又完全能避免的坑项目之间的依赖冲突。A 项目用 Flask 2.3B 项目要用 Flask 3.0如果都装进系统全局环境那酸爽谁试谁知道。解决办法就是虚拟环境每个项目一套独立的依赖目录。Python 3.3 以后官方自带 venv不用再装 virtualenv。创建和激活命令直接放这里# 进入项目目录 cd /path/to/your_project # 创建虚拟环境目录名通常叫 venv 或 .venv python3 -m venv venv # 激活虚拟环境Linux / macOS source venv/bin/activate激活成功后命令行前面会出现(venv)前缀这时候你pip install的所有包都会装进这个环境不污染全局。依赖导出这一块也要养成好习惯。不要直接pip freeze requirements.txt了事因为 freeze 会把当前环境里所有包全部导出来包括间接依赖换台机器恢复时很容易出现版本错乱。规范做法是使用 pipreqs按项目源码里实际 import 到的模块生成依赖文件pip install pipreqs # 在当前目录扫描项目依赖 pipreqs . --encodingutf8 --force这样生成的 requirements.txt 更接近项目真实依赖。锁版本时建议写精确版本号比如flask2.3.3而不是flask2.3避免部署时拉到不兼容的新版本把项目搞挂。2.3 PyCharm 和 VSCode 里配置 Python 环境开发机上的 IDE 配置看似不涉及部署但很多人栽在“本地能跑提交到服务器就崩”根源之一就是本地根本没有用虚拟环境来开发。PyCharm 里配置很简单File → Settings → Project → Python Interpreter点击 Add Interpreter 选择 Existing找到刚才创建的 venv 目录下的 python.exe。VSCode 则用 CtrlShiftP 打开命令面板输入 Python: Select Interpreter选择虚拟环境里的解释器。这样 IDE 的终端也会自动激活 venv在终端里跑 pip install 和运行命令用的都是同一套环境。还有一个我常用的细节在项目根目录放一个说明文件写清楚 Python 版本和关键依赖版本。不管换 IDE 还是换同事接手都知道该用哪个解释器。很多部署事故其实就是版本不一致造成的这种能提前约定的事千万不要省。3. 传统部署从开发机到服务器3.1 代码怎么上服务器先把代码传到服务器这一步做好后面会省很多事。备选方案有三个按推荐程度排序git 拉取、rsync、scp。如果项目已经推到 Git 仓库Gitea、GitHub 都行那部署的常用操作就是登录服务器进入部署目录然后git pull。这个方式最大的好处是更新代码只要一条命令也方便回滚到上一个 commit。cd /srv/www/myproject git pull origin main如果服务器上还没有代码先用 clone 拉下来记得指定分支git clone -b main https://github.com/yourname/yourproject.git /srv/www/myproject要是代码在本地、暂时不想推到仓库可以用 rsync 同步。它比 scp 强在支持增量重复执行只传差异部分# 本地执行把项目同步到服务器 rsync -avz --delete ./ rootyour_server:/srv/www/myproject/这类命令我建议在服务器上把代码目录的属主设置成普通用户别一股脑用 root 跑应用。后面讲 systemd 服务权限时会展开解释。3.2 用 systemd 把 Python 服务变成常驻进程代码到了服务器、依赖装好、虚拟环境激活后python app.py能跑通这只是成功第一步。因为关掉终端服务就没了服务器重启后也不会自动恢复。这就是进程守护要解决的问题。在主流 Linux 发行版上我首选 systemd它系统自带、功能完整能管理重启策略、日志输出、环境变量。下面是一个用 Gunicorn 托管 Flask 或 FastAPI 项目的 unit 文件假设部署路径是/srv/www/myproject虚拟环境在/srv/www/myproject/venv[Unit] DescriptionMy Python Web Application Afternetwork.target [Service] Userwww-data Groupwww-data WorkingDirectory/srv/www/myproject EnvironmentPATH/srv/www/myproject/venv/bin EnvironmentFLASK_ENVproduction ExecStart/srv/www/myproject/venv/bin/gunicorn -w 4 -b 127.0.0.1:8000 app:app Restartalways RestartSec3 [Install] WantedBymulti-user.target文件放在/etc/systemd/system/myproject.service然后用下面几条命令生效sudo systemctl daemon-reload sudo systemctl enable myproject sudo systemctl start myproject几个参数为什么这么写。User 和 Group 指定运行身份建议新建专门用户或者用 www-data不要让服务以 root 身份运行。万一代码有漏洞被利用root 权限的后果比普通用户严重得多。Restartalways 和 RestartSec3 解决进程意外退出后的自动拉起间隔 3 秒避免疯狂重启把系统打满。ExecStart 里 Gunicorn 绑定的是127.0.0.1:8000而不是0.0.0.0这是故意的内网监听避免服务直接暴露公网前面再用 Nginx 做公网入口整体更安全。查看服务状态和日志的命令也顺手列一下# 看服务状态是否 active (running)、最近日志 sudo systemctl status myproject # 看完整日志支持 -f 跟随输出 sudo journalctl -u myproject -f3.3 单独说一下 supervisor另一套守护方案有些老系统或者团队习惯用 supervisor 管理进程它跟 systemd 定位重合二选一就行。我的经验是能用 systemd 就用 systemd因为它跟系统初始化结合得更紧密开机自启、日志收集天然对接supervisor 的优势是配置语法对程序员更友好个别老系统上兼容性更好。supervisor 的配置大概长这样如果你要在没有 systemd 的容器或者老系统上复用可以参考[program:myproject] command/srv/www/myproject/venv/bin/gunicorn -w 4 -b 127.0.0.1:8000 app:app directory/srv/www/myproject userwww-data autostarttrue autorestarttrue stdout_logfile/var/log/myproject.out.log stderr_logfile/var/log/myproject.err.log配置写好后用supervisorctl reread、supervisorctl update加载并启动。本质上跟 systemd 的 Restart 参数做的是同一件事理解透一个另一个自然就会了。4. 容器化部署Docker 实战4.1 为什么要容器化等你在裸机上把服务跑顺再去看 Docker 就会觉得豁然开朗。容器化的核心价值是把“代码 运行环境 系统依赖 配置”打包成一个镜像部署时只需要把镜像拉到目标机器上运行环境差异的问题被封死在镜像里。举个实际场景。你的 Python 项目依赖 libpq 连接 PostgreSQL本地开发机装过系统级依赖所以能跑但部署到一台新开的云服务器上忘记装 libpq-dev 就可能会报错。传统流程你得对着文档一个个装系统包Docker 方式则在构建镜像时就把这些依赖固化进去了换任何一台装好 Docker 的机器都能一键跑起来。多个项目放同一台服务器时容器隔离也很能解决焦虑A 项目需要 Python 3.8B 项目需要 Python 3.12裸机很难同时满足Docker 下每个容器拥有独立运行环境互不影响。4.2 写一个生产可用的 Dockerfile很多人第一次写 Dockerfile要么把所有指令塞进一层要么完全不考虑缓存和安全性。我分享一个基于python:3.12-slim的最小可用模板FROM python:3.12-slim # 设置环境变量避免生成 __pycache__让 Python 日志实时输出 ENV PYTHONDONTWRITEBYTECODE1 ENV PYTHONUNBUFFERED1 # 安装系统级依赖 RUN apt-get update \ apt-get install -y --no-install-recommends build-essential libpq-dev \ rm -rf /var/lib/apt/lists/* # 设置工作目录 WORKDIR /app # 先复制依赖文件再全量复制代码 COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY . . # 非 root 用户运行 RUN useradd -m appuser USER appuser EXPOSE 8000 CMD [gunicorn, -w, 4, -b, 0.0.0.0:8000, app:app]几个设计细节逐步解释一下。PYTHONUNBUFFERED1是为了让 Python 的 print 输出不是缓冲式地囤到一块而是实时打到日志里排查线上问题时非常关键。先COPY requirements.txt再COPY .利用 Docker 镜像层缓存机制只要依赖文件没变后面每次构建就只重新复制代码pip install 那一步会用缓存构建速度快很多。USER appuser改成非 root 运行和 systemd 里不用 root 跑服务是同一个安全考量。构建和运行# 在项目根目录 docker build -t myproject:latest . docker run -d --name myproject -p 8000:8000 myproject:latest这时候访问服务器公网 IP 的 8000 端口如果 8000 没有其他东西占用应该能看见服务响应。4.3 docker-compose把 Web 服务、数据库、前端串起来单容器跑 Python 后端还好实际项目往往还要数据库、Redis可能还有前端静态资源。如果你还在手动docker run一个个起容器我建议尽快换成 docker-compose用一个 YAML 文件描述整套服务一条命令全部拉起。我拿一个带登录注册功能的全栈项目举例假设后端是 FastAPI、数据库是 PostgreSQL、前端是 Vue 打包后的静态文件。项目根目录下 docker-compose.yml 内容services: db: image: postgres:16-alpine restart: always environment: POSTGRES_USER: appuser POSTGRES_PASSWORD: change_me POSTGRES_DB: appdb volumes: - pgdata:/var/lib/postgresql/data networks: - appnet backend: build: ./backend restart: always depends_on: - db environment: DATABASE_URL: postgresqlpsycopg2://appuser:change_medb:5432/appdb networks: - appnet frontend: image: nginx:alpine restart: always ports: - 80:80 volumes: - ./frontend/dist:/usr/share/nginx/html - ./frontend/nginx.conf:/etc/nginx/conf.d/default.conf:ro depends_on: - backend networks: - appnet volumes: pgdata: networks: appnet:全套服务在项目根目录执行docker-compose up -d就能后台启动。几个要点强调一下depends_on只保证数据库容器先启动不保证数据库进程已就绪所以后端代码里最好有连接重试逻辑否则容器启动时连库失败可能直接退出。DATABASE_URL的主机名写的是db因为 compose 里服务名就是容器间的 DNS 名称不能写成 127.0.0.1。frontend 服务直接用 Nginx 镜像挂载打包好的 dist用现成的 Nginx 方案处理前端静态页面。容器日志也得管。容器内 Nginx 或 Gunicorn 的日志默认写到 stdout/stderrDocker 会存到宿主机 json-file 里。不限制大小长时间运行后/var/lib/docker/containers会越来越大把磁盘撑爆。在 compose 里加日志限制是好习惯services: backend: logging: driver: json-file options: max-size: 10m max-file: 3单个服务日志文件最大 10MB、保留 3 份日志自动滚动不会再出现日志无限膨胀的问题。4.4 Docker Desktop 部署的常见坑Windows 上很多人用 Docker Desktop 做本地模拟部署。这条路本身没毛病但我遇到的坑真不少。第一个是路径挂载。Docker Desktop 在 Windows 上通过 WSL2 后端运行Windows 的盘符路径和容器内路径容易对不上。比如执行docker run -v D:\myapp:/app这种命令有时会得到不存在的目录。解法是统一用相对路径在项目目录下运行用.挂载或者到 Docker Desktop 的 Settings → Resources → File sharing 里先把 D 盘共享。第二个是换行符问题。Windows 下编写的 Dockerfile、shell 脚本默认是 CRLF 换行容器内 bash 执行时会报类似$\r: command not found的错误。处理方法是确保相关文件用 LF 换行保存或者在 .gitattributes 里强制文本文件转成 LF。第三个是端口占用。Windows 上 80、443 经常被 IIS 或其他软件占用容器启动时会报端口已被占用。排查用netstat -ano | findstr :80找到占用进程 PID再到任务管理器结束它或者换个宿主机端口映射。5. Nginx 反向代理与多项目部署5.1 为什么 Python 服务前面要套一层 NginxGunicorn 本身就能处理 HTTP 请求直接暴露端口也能访问为什么还要在前面放 Nginx因为 Nginx 擅长的事情恰好是 Python 应用服务器不擅长、或不该花时间做的。大致有三点。第一是静态资源处理Python 后端处理图片、JS、CSS 这类静态文件效率远不如 Nginx让 Nginx 直接返回静态文件后端能腾出手专注动态请求。第二是并发连接和负载分发Nginx 基于事件驱动模型抗高并发能力强可以把请求合理分发到多个 Gunicorn worker或多台后端机器。第三是 HTTPS、域名、安全策略的统一入口SSL 证书、请求头清洗、限流都放这一层后端专注于业务逻辑。用一句话解释就是Nginx 是门卫Gunicorn 才是屋里干活的人门卫帮你挡掉大部分杂活屋里的人才能专心做事。5.2 一套典型的 Python 前端 后端配置这里给出一套典型配置Vue 项目编译后的 dist 目录挂在一个 Nginx server 下接口请求通过/api前缀反向代理到 Python 后端。server { listen 80; server_name example.com; # 前端静态文件 root /srv/www/myproject/frontend/dist; index index.html; # 单页应用路由重写 location / { try_files $uri $uri/ /index.html; } # API 反向代理 location /api/ { 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; } }把配置放/etc/nginx/conf.d/myproject.conf先nginx -t测语法通了再nginx -s reload生效。几个容易错的地方说一下。proxy_pass后面带不带尾斜杠在有路径重写需求时行为不同。如果不改路径建议直接写http://127.0.0.1:8000不带尾斜杠少踩坑。前端路由如果是 BrowserRouter 模式刷新非首页路由会 404所以必须有 try_files 那段把请求都交给 index.html。5.3 一台服务器挂多个 Python 项目的玩法一台服务器部署多个项目的需求很常见可能是个人博客、内部工具、客户的演示站。用 Nginx 区分它们主要有两种思路。思路一是同一端口、不同域名。在 Nginx 里加多个 server 块每个 server_name 对应一个项目域名80 端口统一入口根据域名把流量转到各自后端的 Gunicorn 端口。这个方案需要你有多个域名或至少能把子域名解析到这台服务器。思路二是不同端口。多个 server 块分别 listen 不同端口适合没有多域名、只需要 IP:端口 访问的场景。配置上就是 listen 换一下后面的 root 和 proxy_pass 指向各自项目即可。还有更进阶的 location 前缀区分方式比如 example.com 是项目 Aexample.com/tool 是项目 B。但这种方式对前端路由不太友好因为子路径对打包路径、路由 base 都有要求我更推荐前两种。我实际部署两个 Flask 项目的例子项目 A 监听 8001 端口项目 B 监听 8002 端口都只绑定 127.0.0.1。Nginx 里两个 server 块分别对应 a.example.com 和 b.example.com。两个项目用两个 systemd service 或两个 Docker 容器都行互不干扰。如果是 Docker 环境Nginx 也可以作为容器跑在 compose 网络里用服务名作为反代目标比如proxy_pass http://backend:8000不写 IP。这是容器环境下最常用的联调方式。5.4 日志切分与保留策略日志是很容易被忽略、但线上必备的东西。Gunicorn 默认把请求日志打到 stderrsystemd 会接到 journaldNginx 默认写到/var/log/nginx/access.log和 error.log。长期不处理日志文件会涨到几个 G既占磁盘又让排查变慢。日志轮转最经典的工具是 logrotate主流 Linux 发行版都自带。如果希望日志只保留 90 天、每天切分一次可以在/etc/logrotate.d/myproject里写/srv/www/myproject/logs/*.log { daily rotate 90 compress delaycompress missingok notifempty copytruncate }参数含义不复杂daily 表示每天轮转一次rotate 90 表示保留最近 90 份日志更早的自动删除compress 把轮转出的旧日志用 gzip 压缩delaycompress 延迟一天压缩方便当天日志还能直接查看copytruncate 用于那些一直打开着写日志文件的进程先复制内容再清空文件避免因为重命名造成进程写不进日志。用 logrotate 比手写 cron 脚本轮转可靠得多锁、权限、状态管理都考虑得周全。上线时花两分钟配好就不用操心日志只保留多久这种事。6. 常见问题与排查技巧6.1 部署问题速查表把自己运维以来遇到的高频问题整理成速查表遇到问题先对号入座现象可能原因排查命令 / 方案python 命令找不到PATH 未配置或未重启终端检查 PATH补充 Python 目录502 Bad Gateway后端进程未启动或崩溃systemctl statuscurl 127.0.0.1:8000504 Gateway Timeout上游处理超时加大 proxy_read_timeout优化后端耗时端口被占用其他进程占用 8000/80ss -lntp | grep 8000Docker 卷权限不足uid/gid 不一致映射 user id修改目录权限静态资源 404静态目录路径或 try_files 问题检查 rootcurl 静态文件地址日志里全是 Traceback代码或依赖问题看 journalctl 或容器日志定位页面能开但接口 404反代前缀或路由 base 不匹配抓包看实际请求路径CRLF 换行报错Windows 编辑脚本导致用 dos2unix 或 git 配置 LF6.2 一个真实的 502 排查过程分享一次实际排查过程完整理清思路。有次同事报线上 502我进入服务器后没有直接看 Nginx而是先把链路拆成“客户端 → Nginx → Gunicorn → 应用 → 数据库”逐段验证。第一步确认 Nginx 状态正常nginx -t没问题systemctl status nginx是 active。第二步直接 curl 后端地址127.0.0.1:8000发现连不上说明问题不在 Nginx 而在后端。第三步systemctl status myproject看到服务处于 failed 状态用journalctl -u myproject -n 50查看最后几十行日志发现是连接数据库超时。第四步再查数据库发现 PostgreSQL 所在磁盘满了导致数据库拒绝新连接。根因是磁盘满表象是后端 failed最终表现是用户看到 502。这个案例想说的是排查时一层层剥从现象往根因走别在最外层瞎猜。工具层面curl、ss、journalctl、df -h 这四个命令能解决绝大多数部署问题。6.3 我总结的几条部署纪律最后把这么多年踩坑换来的几条纪律列一下都是很朴素但极其有用的经验。第一部署前先锁定依赖和环境版本Python 小版本、关键库版本、系统版本全部记录。否则出问题时你都不知道该怀疑谁。第二服务不要用 root 跑无论 systemd 还是 Docker 容器都换成普通用户或专用用户。五分钟的事安全收益很大。第三任何配置改动都要走“先验证、再应用”的流程。Nginx 配置改完先nginx -tsystemd 文件改完先daemon-reloadDockerfile 改完先确认能 build 过再考虑操作生产服务。第四把部署过程文档化。哪怕只是在 README 里写一段“如何上线”对几个月后的自己和接手同事都是巨大帮助。第五定时检查磁盘和内存跑 Docker 的服务器尤其注意。容器日志、镜像、悬空容器都会占空间养成定期docker system prune -f的习惯。我个人最大的体会是部署这件事第一次上手最痛苦总觉得步骤又多又杂处处是大坑。但只要你正经走完一遍裸机部署再玩通一遍 Docker后面做任何项目上线都只是重复“准备环境、拉代码、起服务、配反代、看日志”这几个环节而已。真正让人崩溃的从来不是部署本身而是没有建立一套可复用的流程。这个流程一旦沉淀下来后面每次上线都会越来越顺。
返回列表