
很多Python开发者都会遇到同样一个问题本地开发时程序跑得好好的一旦要部署到服务器就冒出一堆莫名其妙的坑——依赖装不上、环境对不上、端口起不来、重启就失效。这篇文章我想把一套我反复使用、实测稳定的部署方案完整分享出来就是Docker加Nginx的组合把Python Web应用打包成镜像用容器跑起来再由Nginx做反向代理和静态资源处理。整个过程会从项目结构设计说起一直到服务器上的最终上线每个步骤都能直接照着操作。这套方案适合已经能写Python Web项目、但对部署比较陌生的人也适合想在团队里建立标准化交付流程的开发者。读懂之后你会理解为什么Docker镜像能解决环境不一致的问题为什么Nginx要站在应用前面而不是直接用Python服务对外暴露端口以及工作中常见的部署问题到底怎么排查。1. 部署方案的整体设计与选型思路1.1 为什么选择Docker加Nginx的组合先说Docker。Python应用最让人头疼的就是环境问题一个项目可能用到Python 3.9另一个要3.11还有各自的依赖版本要求。以前部署靠人肉在服务器上配今天装一个包、明天改一个版本服务器时间久了和操作系统环境乱成一锅粥。Docker把应用和它的运行环境一起打包进镜像镜像在任何装好Docker的服务器上运行结果都一致。你在自己电脑上模拟部署过没问题服务器上就不会因为系统差异而翻车。再说Nginx。很多人会问Flask或FastAPI自己就能跑起来直接访问某个端口不行吗在开发环境可以但生产环境不合适。Python自带的服务不管是Flask的app.run还是uvicorn本质上都不是为高并发调优过的处理并发的能力有限。更关键的问题是生产环境通常有几个非常现实的需求一是SSL证书要挂在某个入口上让所有流量先经过加密再转发给后端二是静态文件如图片、CSS、JS的请求量很大Python服务逐字节读文件效率低Nginx处理这类静态请求的性能好得多三是后端可能需要多开几个进程分担压力需要个入口做负载均衡。这些问题用一个Nginx放在应用前面都能解决。还有一个实际考虑是安全。服务器上如果只开放80或443端口Python应用本身不用暴露给外部网络业务端口只在内网通信攻击面小了不少。Nginx本身成熟、稳定、性能好作为入口有丰富的配置能力和防护手段。1.2 整体架构和请求流转路径用这套方案部署完之后一次用户请求的完整路径是这样的用户访问域名DNS解析到服务器IP请求到达服务器上的Nginx。Nginx监听着标准的80和443端口。Nginx根据自己的配置要么直接处理比如用户请求的是图片、CSS这类静态资源Nginx用磁盘上的文件直接响应要么把动态请求反向代理给后端容器。Nginx把请求转发到Docker容器里的应用服务Python应用处理完业务逻辑后返回响应Nginx再把响应交给用户。后端容器跑在Docker的内部网络中我们为它分配一个业务端口比如8000但这个端口不直接对外开放只有Nginx能访问到它。整个架构分成三层最外层Nginx负责入口和静态响应中间是Docker容器网络负责隔离和连通最里面是Python应用负责业务。1.3 这套方案解决了什么问题最大的问题是标准化。我做过一个项目三个人同时开发提交代码后总会出现我本地没问题啊应该往服务器上装一下XX包的对话。容器化之后交付的是一个镜像这个镜像里已经装好了运行需要的一切服务器不需要预装Python、不需要手动安装依赖只要Docker能跑就行。其次是隔离性。服务器上可能同时跑着多个项目有的用Python 3.10有的用Node还有的用MySQL。如果都在系统层面直接装冲突是迟早的事。Docker把每个服务装进独立的容器互不干扰。第三是可迁移性。今天部署在阿里云明天想迁到腾讯云或者本地机架式服务器上只要新机器装好Docker和Nginx把镜像上传、配置拉下来几分钟就能恢复运行。这对于没有完整运维团队的开发小组来说意义很大。2. Docker化改造与核心配置解析2.1 项目结构设计动手打包Docker镜像之前得先把项目结构整理好。一个适合Docker部署的Python项目通常是这样组织的project/ ├── app/ │ ├── __init__.py │ ├── main.py # 应用入口 │ ├── models.py │ ├── routes/ │ └── static/ # 静态文件如CSS、JS、图片 ├── requirements.txt ├── Dockerfile ├── docker-compose.yml ├── .dockerignore └── nginx/ └── app.conf # Nginx站点配置这里有一个容易踩的坑很多人把静态文件放在应用目录里然后让Python框架自己服务。在开发阶段可以但要对Docker镜像大小有概念时就会发现镜像里带着源代码和静态资源构建出来的镜像大得惊人。比较好的做法是如果静态文件很多发布时把静态文件直接挂载到Nginx能访问的目录在Nginx层面直接服务静态资源。2.2 Dockerfile编写和各层含义一个生产可用、贴合Python生态实际情况的Dockerfile长这样# 基础镜像使用slim版体积小且包含常用系统依赖 FROM python:3.11-slim # 设置工作目录 WORKDIR /app # 设置环境变量 ENV PYTHONDONTWRITEBYTECODE1 \ PYTHONUNBUFFERED1 \ PIP_NO_CACHE_DIR1 # 先拷贝依赖清单利用Docker缓存机制加速重建 COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt # 再拷贝应用代码 COPY . . # 创建一个非root用户运行应用提升容器安全性 RUN adduser --disabled-password --no-create-home appuser USER appuser # 暴露应用端口 EXPOSE 8000 # 启动命令按项目实际情况调整 CMD [uvicorn, app.main:app, --host, 0.0.0.0, --port, 8000]关于每一层的设计有几点值得展开。为什么用slim基础镜像而不是完整版python镜像完整版镜像动辄三四百MBslim版一百多MB构建和拉取都快很多而且生产环境中不需要编译器和开发工具。为什么先把requirements.txt拷贝进去单独构建依赖层而不是直接把整个项目COPY进去再装依赖Docker构建镜像时每一层都会做缓存判断如果该层涉及的文件没有变化就直接复用缓存。把requirements.txt单独COPY只要依赖清单不变pip install这层就会命中缓存不会每改一次代码就把所有依赖重新装一遍。还有一点要特别提醒不要用root用户跑Python容器。有些文章为了省事默认就是root运行这在生产环境有安全风险。如果应用被攻破攻击者拿到的是容器里的root权限加上容器逃逸的手段后果会很严重。2.3 .dockerignore和镜像体积优化技巧有人可能会忽略.dockerignore觉得Dockerfile里只是COPY了该拷贝的东西没什么影响。事实不是这样的。如果你的项目文件夹里有一个.git目录一个本地虚拟环境venv目录还有一些本地缓存执行Docker build时构建上下文要把这些文件全传给Docker守护进程。项目一大会发现构建慢如蜗牛镜像体积也虚胖。.dockerignore的语法类似.gitignore__pycache__/ *.pyc .git/ .venv/ venv/ .env .vscode/ .idea/ Dockerfile docker-compose.yml这里要注意.env文件一般不该打进镜像因为里面可能有数据库密码、密钥这类敏感信息。正确的做法是运行时通过环境变量注入传给容器而不是构建到镜像里。我实测过一个Django项目加了.dockerignore之后镜像从排查上下文后构建的800MB降到340MB构建时间缩短了一半以上。还有一点可以用多阶段构建进一步瘦身比如第一阶段装依赖、第二阶段只保留运行时代码和依赖适合对镜像体积有极致要求的场景。2.4 docker-compose.yml的作用和配置方法实际部署时很少有服务器上只跑一个Python容器的情况。至少还要有Nginx容器大概率还要有数据库容器。如果手动一个一个docker run参数会非常长且容易搞混。docker-compose.yml用声明式的方式把整个服务组定义好一条命令拉起全部服务。version: 3.8 services: app: build: . container_name: myapp_app_1 restart: unless-stopped expose: - 8000 environment: - DATABASE_URLmysqlpymysql://user:passdb:3306/prod_db - SECRET_KEY${SECRET_KEY} volumes: - static_data:/app/static depends_on: - db db: image: mysql:8.0 container_name: myapp_db_1 restart: unless-stopped environment: - MYSQL_DATABASEprod_db - MYSQL_USERuser - MYSQL_PASSWORDpass - MYSQL_ROOT_PASSWORDroot_pass volumes: - db_data:/var/lib/mysql nginx: image: nginx:1.25 container_name: myapp_nginx_1 restart: unless-stopped ports: - 80:80 - 443:443 volumes: - ./nginx/app.conf:/etc/nginx/conf.d/app.conf:ro - static_data:/usr/share/nginx/html/static:ro - ./ssl:/etc/nginx/ssl:ro depends_on: - app volumes: static_data: db_data:注意app服务里面用的是expose而不是portsexpose只是声明容器内部的端口不会映射到宿主机上。Nginx容器和app容器在同一个自定义网络中Nginx可以通过服务名myapp_app_1直接访问app的8000端口。db服务的数据通过命名卷db_data持久化容器删掉重建数据也不会丢。2.5 构建镜像时常见的坑第一个坑是pip下载慢。国内服务器直接pip install官方源经常卡到超时解决方案是配置国内镜像源。可以在Dockerfile里加这一行也可以构建时传参数RUN pip install -i https://pypi.tuna.tsinghua.edu.cn/simple --no-cache-dir -r requirements.txt第二种做法更通用不把源写死在镜像里构建时用--build-arg。我通常推荐写死在基础镜像或requirements里毕竟生产环境需要可重复性。第二个坑是某些Python包需要系统依赖。比如Pillow需要libjpegpsycopg2需要libpq-dev基础镜像里没有这些pip install会直接失败。遇到这种情况需要在安装依赖之前先通过apt安装系统库RUN apt-get update apt-get install -y \ libjpeg-dev \ libpq-dev \ gcc \ rm -rf /var/lib/apt/lists/*最后一个坑是时区问题。默认容器是UTC时区如果项目里有需要和北京时间打交道的地方日志时间会少8小时。解决办法是在Dockerfile或compose里设置环境变量environment: - TZAsia/Shanghai3. Nginx配置与反向代理实操3.1 Nginx在部署中承担的职责聊完Docker部分Nginx的配置也值得仔细说。Nginx在整套架构里不只是当一个简单的流量入口它实际做了四件事一是TLS终结。SSL证书装在Nginx上对外是HTTPS协议内网里Nginx和后端应用之间走HTTP减少后端对TLS握手和对加解密处理的负担。二是反向代理。后端容器可能扩容成多个副本Nginx可以通过upstream配置负载均衡把请求分发给不同的后端实例。三是静态文件服务。图片、CSS、JS这些请求从磁盘直接响应命中后不带去Python应用。对高并发场景来说这一条就能挡掉大量不必要的应用压力。四是安全策略。可以配置请求体大小限制、连接超时、访问限流有的还能做基础的WAF规则。3.2 一份生产可用的Nginx站点配置这里给出一个常用的配置模板配合上面说的Docker Compose方案直接使用upstream backend { least_conn; server myapp_app_1:8000; } server { listen 80; server_name example.com; location /.well-known/acme-challenge/ { root /usr/share/nginx/html; } location / { return 301 https://$host$request_uri; } } server { listen 443 ssl http2; server_name example.com; ssl_certificate /etc/nginx/ssl/example.com.crt; ssl_certificate_key /etc/nginx/ssl/example.com.key; ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers HIGH:!aNULL:!MD5; ssl_session_timeout 1d; ssl_session_cache shared:SSL:10m; client_max_body_size 20m; location /static/ { alias /usr/share/nginx/html/static/; expires 30d; add_header Cache-Control public, immutable; } location /media/ { alias /usr/share/nginx/html/media/; expires 7d; } location / { proxy_pass http://backend; 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; } }80端口做了强制跳转HTTPS静态资源的缓存策略也配置好了。这里尤其想提一下客户端真实IP的问题。如果后端应用需要拿到用户的真实IP比如做访问日志、限频策略必须依赖Nginx传过去的X-Forwarded-For和X-Real-IP头。Python的框架中比如Flask需要配置ProxyFix中间件来读取这些头Django也自带类似的信任代理逻辑部署后要用这个方法验证一下真实IP是否透传正确。3.3 反向代理配置的关键细节和为什么Nginx配置表面上看就是几十行内容但改动一行都可能引发线上问题值得逐条说说几个容易出问题的点。proxy_pass http://backend;那行末尾没有多余路径指的是把请求原样转发给后端。如果写成proxy_pass http://backend/;由于最后多了斜杠URI中的location匹配部分会被替换掉行为就大变样了。曾经见过很多人在这上面出问题请求是过来了但路径全乱了。只要理解了代理规则就能明白怎么定位。client_max_body_size 20m这个配置直接影响上传文件功能。如果后端有个上传接口提交的文件超过Nginx默认的1MB限制就会返回413错误。按业务需要调大即可。proxy_set_header Host $host;这一行让后端知道用户访问的是哪个域名。有些项目会根据Host判断当前站点甚至用不同域名做多租户如果没有正确传Host后端会当成默认域名处理甚至直接返回404。3.4 HTTPS证书申请和自动续期证书方面用Lets Encrypt免费证书完全够用配置过程比想象中简单。早期的做法是手动去网站生成证书再传到服务器现在推荐直接用certbot自动签发和续期。# 先安装certbot和nginx插件 sudo apt install certbot python3-certbot-nginx # 签发证书certbot会自动修改nginx配置 sudo certbot --nginx -d example.com -d www.example.com # 验证自动续期任务是否存在 sudo systemctl status certbot.timer如果证书签发的域名比较多注意Lets Encrypt对同一主域有每周签5次的限制调试时别反复去签。另外证书是90天有效期的一定要确认自动续期任务正常运行否则到期后HTTPS会突然失效线上会直接炸掉。如果服务器在国内Lets Encrypt的访问偶尔不稳定备选方案是使用国内云厂商提供的证书服务原理一样只是签发渠道不同。4. 完整部署流程与实操记录4.1 服务器初始化假设手上是一台全新的Linux服务器以Ubuntu 22.04为例从零开始操作。安装Dockersudo apt update sudo apt install -y ca-certificates curl gnupg sudo install -m 0755 -d /etc/apt/keyrings curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg echo deb [arch$(dpkg --print-architecture) signed-by/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(. /etc/os-release echo $VERSION_CODENAME) stable | sudo tee /etc/apt/sources.list.d/docker.list /dev/null sudo apt update sudo apt install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin安装之后验证版本sudo docker version sudo docker compose version不推荐直接使用docker run来部署docker compose才是正经的编排工具。如果系统自带python不要去动它的版本也不要顺手装什么宝塔之类的面板越干净的环境越稳定出了问题也越容易排查。4.2 构建镜像和启动服务项目上传到服务器后进入项目目录构建镜像sudo docker compose build构建完成后启动sudo docker compose up -d查看容器状态sudo docker compose ps查看日志sudo docker compose logs -f app用docker compose up -d把服务拉起来后容器在后台运行。restart策略如果配置的是unless-stopped服务器重启后服务也会自动恢复。4.3 验证整个链路启动完成后按下面几条路径逐层验证第一验证Nginx是否正常响应curl -I http://localhost第二验证后端容器是否可访问直接进Nginx容器里测试代理目标连通性sudo docker exec -it myapp_nginx_1 curl http://myapp_app_1:8000/health第三从外网访问域名看是否正常打开浏览器输入域名应该看到应用首页。第四验证HTTPS证书是否生效访问https://域名浏览器显示安全锁。如果某一步失败按4.1到4.3的顺序排查一般能定位到是哪一层出了问题。4.4 数据库和其他中间件的部署用docker compose把MySQL也纳入编排后数据库的备份和恢复变得很清爽。推荐的做法是在宿主机上配置一个定时任务每天凌晨把数据库数据从容器里导出来压缩后存到别的位置。sudo docker exec myapp_db_1 mysqldump -u root -p$DB_ROOT_PASSWORD prod_db | gzip /backup/db_$(date %Y%m%d).sql.gz配合crontab定时执行这套备份方案足够应对绝大多数小项目。恢复时也很直接把dump出来的SQL文件灌进容器即可。5. 常见部署问题与排查经验实录这个是重头戏。部署过程中踩过的坑这里做个详细的排查合集按问题出现的频率和影响程度排序。5.1 后端一直502 Bad Gateway502几乎是最常见的上线事故了第一次上线遇到502基本是必由之路。Nginx返回502意味着它连不上后端。常见原因有这么几类一是后端容器没起来或崩溃了。排查命令是先看容器状态sudo docker ps -a如果容器的STATUS显示Exited用docker logs看退出原因。大多数是代码里引用了不存在的环境变量、数据库连不上导致启动时异常退出。二是容器起来了但端口不对。python应用启动时绑定的是8000端口compose里expose的也是8000但Nginx配置文件里写成了myapp_app_1:8080就必然不通。核对三处端口容器CMD里监听的端口、compose文件里expose的端口、Nginx upstream里写的端口三处必须一致。三是依赖关系问题。数据库容器还没初始化好app容器先启动了app连不上数据库直接崩溃。解决方法是compose里用depends_on加condition或者给app容器加一个健康检查脚本等数据库就绪再启动。更稳妥的方式是应用启动时做重试逻辑多等几秒。四是网络问题。如果容器不在同一个自定义网络上服务名解析不到。确认compose文件里没有手动给每个容器指定network: host而是让它们用compose自动创建的默认网络。5.2 构建镜像时pip安装依赖失败这个问题在服务器上构建镜像时很常见尤其是国内服务器pip默认源访问速度不稳定经常超时。用国内镜像源后问题基本解决。如果还是失败看具体的error信息如果是某个包需要编译缺了系统依赖就在Dockerfile里补上对应apt包。另一个典型的情况是基础镜像本身拉不下来。服务器如果访问不了Docker Hub可以在Docker的守护进程配置里设置registry-mirrors配置国内镜像加速地址。5.3 静态文件404项目上线后发现页面样式全丢F12一看一堆静态资源404。原因多数是两个一个是Nginx的static location配置路径和后端框架实际产出的静态目录对不上。Django有collectstaticFlask要把static目录明确告知跑一遍确认目录里确实有文件再对照Nginx的alias路径。另一个是sqlite或小项目从不用collectstatic直接引用开发服务器上的文件上线后根本没有静态文件目录自然全部404。更好的方案是在构建阶段用Django的collectstatic生成静态文件然后直接挂载给Nginx。具体操作看项目框架的文档但方向是让静态文件独立于应用进程。5.4 端口占用或权限问题Nginx监听80或443时提示address already in use说明服务器上已有别的进程占了这两个端口。排查sudo lsof -i:80 sudo netstat -tlnp | grep :80确认是哪个进程占用如果有旧服务先停掉。如果服务器上另有web服务占用了80端口要改架构让Nginx换端口接收流量或者把现有服务迁到其他端口。5.5 容器日志乱串和时间不对时常有人问容器里的日志时间总是不对比北京时间少8小时。这就是时区设置问题。在compose里给每个服务加上环境变量TZAsia/Shanghai然后重建容器。有的镜像默认没有装tzdata还需要在Dockerfile里先配好。日志乱串的问题一般出在多个服务用了同一个容器名或日志驱动配置不对。建议open一个规范每个服务的container_name固定成项目名_服务名日志单独输出到文件或采集到集中的日志平台。5.6 常见问题速查表现象可能原因排查命令/思路502 Bad Gateway后端容器崩溃/端口配置不一致docker ps -a、docker logs504 Gateway Timeout后端处理超时/请求量过大看Nginx error.log调大proxy_read_timeout413 Request Entity Too Large上传文件超过Nginx限制设置client_max_body_size404静态文件alias路径错误/未生成静态目录对比目录路径确认静态文件存在容器无法重启启动命令错误/退出码非0docker logs查看具体报错数据库连接拒绝数据库未就绪/密码不对depends_on加条件检查环境变量外网访问不通安全组未开端口/防火墙拦截检查云安全组规则和ufw状态HTTPS证书过期自动续期任务异常certbot renew --dry-run手动测试5.7 排查反向代理的一个不常用但好用的小技巧Nginx的access.log和error.log是排查问题最重要的入口。配置时给Nginx挂载一个日志目录volumes: - ./nginx/logs:/var/log/nginx这样日志直接落在宿主机用起来方便很多。查看后端有没有接到正常请求直接看error.log里upstream相关的行能看到完整的请求转发路径和耗时数据。6. 上线之后的几件事服务一旦跑起来并不代表工作结束。有几件事我在实际部署项目中是一定要做的。配置一套健康检查。compose里给app和nginx都加上healthcheck让编排工具能知道服务是否真的健康。比如app的健康检查可以用curl请求一个固定的/health路径healthcheck: test: [CMD, curl, -f, http://localhost:8000/health] interval: 30s timeout: 5s retries: 3把服务日志接入集中收集。如果只有一两个容器docker compose logs已经够用等容器变多就需要ELK或Loki这类方案否则整个排查成本会指数级上升。设置好备份计划。数据库每天备份一次加上异地存储恢复演练要真的做过一次光写了个脚本放那不看到需要的时候发现恢复不了那才是真灾难。我跟很多开发者交流时发现大家担心部署复杂其实真正难的地方在于对整体架构缺乏体系化理解。Docker加Nginx这套组合已经存在了很多年是经过生产环境反复锤炼的成熟方案。照着上面的步骤走一遍把这个流程跑通之后你会发现后续无论是部署新项目还是迁移旧项目整个过程会顺畅得多。我自己的经验是这些配置看十遍都不如动手部署一次在服务器上真实操作过一遍很多之前觉得抽象的概念就全都通了。